Initial commit: project structure, scripts and documentation

This commit is contained in:
2026-08-22 13:17:29 +03:00
commit 5042305df0
54 changed files with 14166 additions and 0 deletions
View File
@@ -0,0 +1,303 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "O6Ly_P7Rs5Pq"
},
"outputs": [],
"source": [
"# Установка библиотеки MedMNIST\n",
"!pip install -q medmnist"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "FNCBYGtd5wnF"
},
"outputs": [],
"source": [
"\n",
"\n",
"# Импорт библиотек\n",
"import numpy as np\n",
"import tensorflow as tf\n",
"from tensorflow.keras import layers, models\n",
"import torchvision.transforms as transforms\n",
"from torch.utils.data import DataLoader\n",
"import medmnist\n",
"from medmnist import INFO\n",
"\n",
"# Загружаем датасет ChestMNIST (multi-label: 14 классов)\n",
"data_flag = 'chestmnist'\n",
"info = INFO[data_flag]\n",
"DataClass = getattr(medmnist, info['python_class'])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "onPt-ifPcb70",
"outputId": "413ff073-3fae-465c-fb82-2ea52d4bc967"
},
"outputs": [],
"source": [
"\n",
"# Трансформации: PyTorch-style -> Tensor\n",
"transform = transforms.Compose([\n",
" transforms.ToTensor()\n",
"])\n",
"\n",
"# Загрузка данных\n",
"train_dataset = DataClass(split='train', transform=transform, download=True)\n",
"test_dataset = DataClass(split='test', transform=transform, download=True)\n",
"\n",
"# DataLoader для PyTorch-совместимых датасетов\n",
"train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)\n",
"test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)\n",
"\n",
"# Преобразование в NumPy: (batch, 28, 28, 1), labels в one-hot\n",
"def convert_dataset_to_numpy(dataloader):\n",
" images_all = []\n",
" labels_all = []\n",
"\n",
" for images, labels in dataloader:\n",
" images = images.permute(0, 2, 3, 1).numpy() # (B, H, W, C)\n",
" labels = labels.numpy()\n",
" images_all.append(images)\n",
" labels_all.append(labels)\n",
"\n",
" X = np.concatenate(images_all, axis=0)\n",
" y = np.concatenate(labels_all, axis=0)\n",
"\n",
" return X, y\n",
"\n",
"X_train, y_train = convert_dataset_to_numpy(train_loader)\n",
"X_test, y_test = convert_dataset_to_numpy(test_loader)\n",
"\n",
"print(\"X_train shape:\", X_train.shape) # (n_samples, 28, 28, 1)\n",
"print(\"y_train shape:\", y_train.shape) # (n_samples, 14)\n",
"\n",
"# Масштабирование изображений в диапазон [0, 1]\n",
"X_train = X_train.astype(\"float32\") / 255.0\n",
"X_test = X_test.astype(\"float32\") / 255.0\n",
"\n",
"# Определение модели (sigmoid + binary_crossentropy!)\n",
"model = tf.keras.Sequential([\n",
" layers.Input(shape=(28, 28, 1)),\n",
" layers.Conv2D(32, kernel_size=3, activation='relu'),\n",
" layers.MaxPooling2D(),\n",
" layers.Conv2D(64, kernel_size=3, activation='relu'),\n",
" layers.MaxPooling2D(),\n",
" layers.Flatten(),\n",
" layers.Dense(128, activation='relu'),\n",
" layers.Dense(14, activation='sigmoid') # multi-label output\n",
"])\n",
"\n",
"# Компиляция модели\n",
"model.compile(\n",
" optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4), # уменьшенный lr\n",
" loss='binary_crossentropy', # Ключевой момент!\n",
" metrics=[\n",
" tf.keras.metrics.BinaryAccuracy(),\n",
" tf.keras.metrics.AUC(name='AUC'),\n",
" tf.keras.metrics.Precision(name='Precision'),\n",
" tf.keras.metrics.Recall(name='Recall')\n",
" ]\n",
")\n",
"\n",
"# Обучение\n",
"history = model.fit(\n",
" X_train, y_train,\n",
" epochs=10,\n",
" batch_size=32,\n",
" validation_data=(X_test, y_test)\n",
")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 141
},
"id": "KRDuSl4GNf0i",
"outputId": "9d4d18c6-08f1-458e-c846-5a373afcc52a"
},
"outputs": [],
"source": [
"model.summary()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 506
},
"id": "9epuUiVJ5z2u",
"outputId": "888f592f-b2db-435a-fdf3-d788ba41f7b6"
},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"\n",
"# Визуализация первых n изображений и соответствующих меток\n",
"def show_sample_images(dataset, class_names, n=9):\n",
" plt.figure(figsize=(5, 5))\n",
" for i in range(n):\n",
" image, label = dataset[i]\n",
" img_np = image.permute(1, 2, 0).numpy().squeeze() # (H, W)\n",
"\n",
" # Преобразуем one-hot в список меток\n",
" active_labels = [class_names[j] for j in range(len(label)) if label[j] == 1]\n",
"\n",
" plt.subplot(int(n**0.5 + 0.5), int(n**0.5 + 0.5), i + 1)\n",
" plt.imshow(img_np, cmap='gray')\n",
" plt.axis('off')\n",
" plt.title('\\n'.join(active_labels), fontsize=8)\n",
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"# Названия меток (по ключу из INFO)\n",
"class_names = list(info['label'].values())\n",
"\n",
"# Вызов визуализации\n",
"show_sample_images(train_dataset, class_names, n=9)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "0NDTDphUgpMi",
"outputId": "9735171b-9d5a-425d-91d4-1eac2da6de7c"
},
"outputs": [],
"source": [
"# 2. Предсказания\n",
"y_pred_probs = model.predict(X_test)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "mKpWlhC0t45X",
"outputId": "ad6e3b10-d232-465d-e6f5-df71eadc51ae"
},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"\n",
"# Функция для отрисовки метрик\n",
"def plot_training_history(history):\n",
" metrics = ['loss', 'val_loss', 'AUC', 'val_AUC', 'Precision', 'val_Precision', 'Recall', 'val_Recall']\n",
" plt.figure(figsize=(16, 10))\n",
"\n",
" for i, metric in enumerate(metrics):\n",
" plt.subplot(4, 2, i + 1)\n",
" plt.plot(history.history[metric])\n",
" plt.title(metric)\n",
" plt.xlabel('Epoch')\n",
" plt.ylabel(metric)\n",
" plt.grid(True)\n",
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"# Вызов функции\n",
"plot_training_history(history)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "wECO7vr_gukD",
"outputId": "e704d146-d224-43b3-a2f1-d2d40b2cc492"
},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"\n",
"from sklearn.metrics import roc_curve, auc, precision_recall_curve, average_precision_score\n",
"\n",
"def plot_all_roc_pr_curves(y_true, y_probs, class_names):\n",
" num_classes = y_true.shape[1]\n",
" fig_roc, axs_roc = plt.subplots(4, 4, figsize=(20, 20))\n",
" fig_pr, axs_pr = plt.subplots(4, 4, figsize=(20, 20))\n",
"\n",
" for i in range(num_classes):\n",
" fpr, tpr, _ = roc_curve(y_true[:, i], y_probs[:, i])\n",
" precision, recall, _ = precision_recall_curve(y_true[:, i], y_probs[:, i])\n",
" roc_auc = auc(fpr, tpr)\n",
" pr_auc = average_precision_score(y_true[:, i], y_probs[:, i])\n",
"\n",
" row, col = divmod(i, 4)\n",
"\n",
" # ROC Curve\n",
" axs_roc[row, col].plot(fpr, tpr, label=f'AUC = {roc_auc:.2f}')\n",
" axs_roc[row, col].plot([0, 1], [0, 1], 'k--')\n",
" axs_roc[row, col].set_title(f'ROC — {class_names[i]}')\n",
" axs_roc[row, col].set_xlabel('FPR')\n",
" axs_roc[row, col].set_ylabel('TPR')\n",
" axs_roc[row, col].legend()\n",
"\n",
" # PR Curve\n",
" axs_pr[row, col].plot(recall, precision, label=f'AP = {pr_auc:.2f}')\n",
" axs_pr[row, col].set_title(f'PR — {class_names[i]}')\n",
" axs_pr[row, col].set_xlabel('Recall')\n",
" axs_pr[row, col].set_ylabel('Precision')\n",
" axs_pr[row, col].legend()\n",
"\n",
" # Отключаем лишние пустые графики, если классов < 16\n",
" for j in range(num_classes, 16):\n",
" fig_roc.axes[j].axis('off')\n",
" fig_pr.axes[j].axis('off')\n",
"\n",
" fig_roc.tight_layout()\n",
" fig_pr.tight_layout()\n",
" plt.show()\n",
"\n",
"# Вызов визуализации ROC и PR кривых\n",
"plot_all_roc_pr_curves(y_test, y_pred_probs, list(info['label'].values()))\n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
+271
View File
@@ -0,0 +1,271 @@
{
"cells": [
{
"cell_type": "raw",
"id": "0",
"metadata": {},
"source": [
"Установка библиотек"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"%pip install pydicom matplotlib nibabel gdown medmnist Pillow"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2",
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"# Download the 56 zip files in Images_png in batches\n",
"import urllib.request\n",
"\n",
"# URLs for the zip files\n",
"links = [\n",
" 'https://nihcc.box.com/shared/static/vfk49d74nhbxq3nqjg0900w5nvkorp5c.gz',\n",
" 'https://nihcc.box.com/shared/static/i28rlmbvmfjbl8p2n3ril0pptcmcu9d1.gz',\n",
" 'https://nihcc.box.com/shared/static/f1t00wrtdk94satdfb9olcolqx20z2jp.gz',\n",
"\t'https://nihcc.box.com/shared/static/0aowwzs5lhjrceb3qp67ahp0rd1l1etg.gz',\n",
" 'https://nihcc.box.com/shared/static/v5e3goj22zr6h8tzualxfsqlqaygfbsn.gz',\n",
"\t'https://nihcc.box.com/shared/static/asi7ikud9jwnkrnkj99jnpfkjdes7l6l.gz',\n",
"\t'https://nihcc.box.com/shared/static/jn1b4mw4n6lnh74ovmcjb8y48h8xj07n.gz',\n",
" 'https://nihcc.box.com/shared/static/tvpxmn7qyrgl0w8wfh9kqfjskv6nmm1j.gz',\n",
"\t'https://nihcc.box.com/shared/static/upyy3ml7qdumlgk2rfcvlb9k6gvqq2pj.gz',\n",
"\t'https://nihcc.box.com/shared/static/l6nilvfa9cg3s28tqv1qc1olm3gnz54p.gz',\n",
"\t'https://nihcc.box.com/shared/static/hhq8fkdgvcari67vfhs7ppg2w6ni4jze.gz',\n",
"\t'https://nihcc.box.com/shared/static/ioqwiy20ihqwyr8pf4c24eazhh281pbu.gz'\n",
"]\n",
"\n",
"for idx, link in enumerate(links):\n",
" fn = 'images_%02d.tar.gz' % (idx+1)\n",
" print('downloading'+fn+'...')\n",
" urllib.request.urlretrieve(link, fn) # download the zip file\n",
"\n",
"print(\"Download complete. Please check the checksums\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3",
"metadata": {},
"outputs": [],
"source": [
"import tarfile\n",
"import os\n",
"\n",
"# Распаковка\n",
"with tarfile.open('images_02.tar.gz', 'r:gz') as tar:\n",
" tar.extractall(path='images_01')\n",
"print(\"✅ Распаковано в папку 'images_01'\")\n",
"print(\"Количество файлов в папке:\", len(os.listdir('images_01')))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"from PIL import Image\n",
"import random\n",
"import os\n",
"\n",
"image_folder = 'images_01/images'\n",
"all_images = [f for f in os.listdir(image_folder) if f.endswith('.png')]\n",
"print(f\"Всего PNG-файлов: {len(all_images)}\")\n",
"\n",
"# Покажем 5 случайных\n",
"sample = random.sample(all_images, min(5, len(all_images)))\n",
"plt.figure(figsize=(15, 10))\n",
"for i, img_name in enumerate(sample):\n",
" img = Image.open(os.path.join(image_folder, img_name))\n",
" plt.subplot(1, 5, i+1)\n",
" plt.imshow(img, cmap='gray')\n",
" plt.title(img_name[:15])\n",
" plt.axis('off')\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"!wget -O dataset.zip 'https://storage.yandexcloud.net/aie20.selftest/production/Dataset_for_self-testing_of_AI_services_%28AIE20_SELFTEST%29.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=J0yLHxYnLMK5SrRghejm%2F20260222%2Fru-central1%2Fs3%2Faws4_request&X-Amz-Date=20260222T111520Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=b135a1c05ed66ade15b59a73bc29aef7380f9da179abd74267c4ec235462992a'"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6",
"metadata": {},
"outputs": [],
"source": [
"%pip install pylibjpeg pylibjpeg-libjpeg"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import os\n",
"import random\n",
"import pydicom\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ------------------------------------------------------------\n",
"# 1. Настройки\n",
"# ------------------------------------------------------------\n",
"# Путь к корневой папке с данными CT (измените, если структура иная)\n",
"ct_root = 'Dataset_for_self-testing_of_AI_services_(AIE20_SELFTEST)/AIE20_SELFTEST/FLG'\n",
"\n",
"# Количество случайных исследований для показа\n",
"num_samples = 5\n",
"\n",
"# ------------------------------------------------------------\n",
"# 2. Функция для рекурсивного поиска всех DICOM-файлов в папке\n",
"# ------------------------------------------------------------\n",
"def find_dicom_files(folder):\n",
" \"\"\"Возвращает список путей ко всем файлам (не папкам) внутри folder.\"\"\"\n",
" all_files = []\n",
" for root, dirs, files in os.walk(folder):\n",
" for f in files:\n",
" if not f.startswith('.'): # игнорируем скрытые файлы\n",
" all_files.append(os.path.join(root, f))\n",
" return all_files\n",
"\n",
"# ------------------------------------------------------------\n",
"# 3. Собираем все папки исследований (производитель/исследование)\n",
"# ------------------------------------------------------------\n",
"if not os.path.exists(ct_root):\n",
" print(f\"Ошибка: папка {ct_root} не найдена.\")\n",
"else:\n",
" # Получаем список производителей\n",
" manufacturers = [m for m in os.listdir(ct_root) \n",
" if os.path.isdir(os.path.join(ct_root, m))]\n",
" if not manufacturers:\n",
" print(\"Нет папок производителей.\")\n",
" else:\n",
" # Список всех полных путей к папкам исследований\n",
" study_folders = []\n",
" for man in manufacturers:\n",
" man_path = os.path.join(ct_root, man)\n",
" studies = [s for s in os.listdir(man_path) \n",
" if os.path.isdir(os.path.join(man_path, s))]\n",
" for study in studies:\n",
" study_folders.append(os.path.join(man_path, study))\n",
"\n",
" print(f\"Всего найдено папок исследований: {len(study_folders)}\")\n",
"\n",
" if not study_folders:\n",
" print(\"Нет доступных исследований.\")\n",
" else:\n",
" # Выбираем случайные папки (не больше, чем есть)\n",
" selected = random.sample(study_folders, min(num_samples, len(study_folders)))\n",
"\n",
" # ------------------------------------------------------------\n",
" # 4. Для каждой выбранной папки показываем случайное изображение\n",
" # ------------------------------------------------------------\n",
" plt.figure(figsize=(15, 10))\n",
" for i, folder in enumerate(selected):\n",
" # Рекурсивно ищем все файлы в этой папке\n",
" dicom_files = find_dicom_files(folder)\n",
" if not dicom_files:\n",
" print(f\"В папке {folder} нет файлов, пропускаем.\")\n",
" continue\n",
"\n",
" # Выбираем случайный файл\n",
" chosen = random.choice(dicom_files)\n",
"\n",
" try:\n",
" ds = pydicom.dcmread(chosen)\n",
" # Проверяем, есть ли массив пикселей\n",
" if not hasattr(ds, 'pixel_array'):\n",
" print(f\"Файл {chosen} не содержит изображения, пропускаем.\")\n",
" continue\n",
"\n",
" # Извлекаем метаданные для подписи\n",
" manufacturer = ds.get('Manufacturer', 'Unknown')\n",
" modality = ds.get('Modality', 'Unknown')\n",
" patient_id = ds.get('PatientID', 'Unknown')\n",
" study_desc = ds.get('StudyDescription', 'No description')\n",
" series_desc = ds.get('SeriesDescription', '')\n",
"\n",
" # Формируем подпись\n",
" title = (f\"{manufacturer}\\n\"\n",
" f\"Modality: {modality}\\n\"\n",
" f\"Patient: {patient_id}\\n\"\n",
" f\"Study: {study_desc[:30]}\\n\"\n",
" f\"Series: {series_desc[:20]}\\n\"\n",
" f\"File: {os.path.basename(chosen)}\")\n",
"\n",
" # Отображаем\n",
" plt.subplot(1, num_samples, i+1)\n",
" plt.imshow(ds.pixel_array, cmap='gray')\n",
" plt.title(title, fontsize=8)\n",
" plt.axis('off')\n",
"\n",
" except Exception as e:\n",
" print(f\"Ошибка при обработке файла {chosen}: {e}\")\n",
" continue\n",
"\n",
" plt.tight_layout()\n",
" plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "DataSphere Kernel",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+928
View File
@@ -0,0 +1,928 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "0",
"metadata": {},
"outputs": [],
"source": [
"%pip install nibabel monai"
]
},
{
"cell_type": "raw",
"id": "1",
"metadata": {},
"source": [
"Task09_Spleen (селезёнка) :\n",
"\n",
"Размер: всего ~1.5 ГБ (скачается быстро)\n",
"\n",
"Структура: простая бинарная сегментация (есть селезёнка / нет селезёнки)\n",
"\n",
"Количество файлов: 41 тренировочное + 20 тестовых изображений"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2",
"metadata": {},
"outputs": [],
"source": [
"# Task09_Spleen (рекомендую для начала) - 1.5 ГБ\n",
"!wget -O Task09_Spleen.tar 'https://msd-for-monai.s3-us-west-2.amazonaws.com/Task09_Spleen.tar'"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3",
"metadata": {},
"outputs": [],
"source": [
"import tarfile\n",
"import os\n",
"import nibabel as nib\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"# Распаковка\n",
"with tarfile.open('Task09_Spleen.tar', 'r') as tar:\n",
" tar.extractall()\n",
"\n",
"# Посмотрим содержимое\n",
"base_dir = 'Task09_Spleen'\n",
"print(\"Содержимое:\", os.listdir(base_dir))\n",
"# Ожидается: imagesTr (тренировочные изображения), labelsTr (маски), imagesTs (тестовые)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"base_dir = 'Task09_Spleen'\n",
"print(\"Папки в датасете:\", os.listdir(base_dir))\n",
"# Ожидаем: ['imagesTr', 'labelsTr', 'imagesTs', 'dataset.json']"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5",
"metadata": {},
"outputs": [],
"source": [
"train_images = sorted(os.listdir(os.path.join(base_dir, 'imagesTr')))\n",
"train_labels = sorted(os.listdir(os.path.join(base_dir, 'labelsTr')))\n",
"\n",
"print(f\"Тренировочных изображений: {len(train_images)}\")\n",
"print(f\"Масок: {len(train_labels)}\")\n",
"print(f\"Пример имени файла изображения: {train_images[0]}\")\n",
"print(f\"Пример имени файла маски: {train_labels[0]}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import os\n",
"import nibabel as nib\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"base_dir = 'Task09_Spleen'\n",
"\n",
"# Получаем списки файлов, исключая служебные (начинающиеся с ._)\n",
"train_images = sorted([\n",
" f for f in os.listdir(os.path.join(base_dir, 'imagesTr')) \n",
" if not f.startswith('._') and f.endswith('.nii.gz')\n",
"])\n",
"train_labels = sorted([\n",
" f for f in os.listdir(os.path.join(base_dir, 'labelsTr')) \n",
" if not f.startswith('._') and f.endswith('.nii.gz')\n",
"])\n",
"\n",
"print(f\"Тренировочных изображений (реальных): {len(train_images)}\")\n",
"print(f\"Масок (реальных): {len(train_labels)}\")\n",
"print(f\"Пример имени файла изображения: {train_images[0]}\")\n",
"print(f\"Пример имени файла маски: {train_labels[0]}\")\n",
"\n",
"# Проверим, совпадают ли имена (в MSD они могут немного отличаться, например, у изображений может быть суффикс '_0000')\n",
"# Для простоты возьмём первый файл из изображений и попробуем найти соответствующую маску\n",
"# Обычно в Task09_Spleen имена совпадают (без суффиксов)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7",
"metadata": {},
"outputs": [],
"source": [
"# Выбираем первый реальный файл изображения и соответствующую маску\n",
"# Предполагаем, что имена совпадают\n",
"img_name = train_images[0]\n",
"# Ищем маску с таким же именем (в Task09_Spleen они одинаковые)\n",
"mask_name = img_name # если имена совпадают\n",
"\n",
"img_path = os.path.join(base_dir, 'imagesTr', img_name)\n",
"mask_path = os.path.join(base_dir, 'labelsTr', mask_name)\n",
"\n",
"# Загрузка\n",
"img_nii = nib.load(img_path)\n",
"mask_nii = nib.load(mask_path)\n",
"\n",
"img_data = img_nii.get_fdata()\n",
"mask_data = mask_nii.get_fdata()\n",
"\n",
"print(f\"Размер изображения: {img_data.shape}\")\n",
"print(f\"Уникальные значения в маске: {np.unique(mask_data)}\") # должно быть [0., 1.] (фон и селезёнка)\n",
"\n",
"# Покажем несколько срезов\n",
"slices = np.linspace(0, img_data.shape[2]-1, 5, dtype=int)\n",
"\n",
"plt.figure(figsize=(15, 10))\n",
"for i, idx in enumerate(slices):\n",
" plt.subplot(2, 5, i+1)\n",
" plt.imshow(img_data[:, :, idx], cmap='gray')\n",
" plt.title(f'Срез {idx}')\n",
" plt.axis('off')\n",
"\n",
" plt.subplot(2, 5, i+6)\n",
" plt.imshow(mask_data[:, :, idx], cmap='hot', alpha=0.7)\n",
" plt.imshow(img_data[:, :, idx], cmap='gray', alpha=0.3)\n",
" plt.title(f'Маска срез {idx}')\n",
" plt.axis('off')\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import os\n",
"import torch\n",
"import nibabel as nib\n",
"import numpy as np\n",
"from monai.networks.nets import UNet\n",
"from monai.losses import DiceCELoss\n",
"from monai.metrics import DiceMetric\n",
"from monai.inferers import sliding_window_inference\n",
"from monai.transforms import (\n",
" Compose, LoadImaged, EnsureChannelFirstd, ScaleIntensityd,\n",
" RandRotate90d, RandFlipd, RandShiftIntensityd, ToTensord,\n",
" SpatialPadd\n",
")\n",
"from monai.data import CacheDataset, DataLoader\n",
"\n",
"# --- 1. Подготовка данных ---\n",
"base_dir = 'Task09_Spleen'\n",
"images_dir = os.path.join(base_dir, 'imagesTr')\n",
"labels_dir = os.path.join(base_dir, 'labelsTr')\n",
"\n",
"train_images = sorted([os.path.join(images_dir, f) \n",
" for f in os.listdir(images_dir) \n",
" if f.endswith('.nii.gz') and not f.startswith('._')])\n",
"train_labels = sorted([os.path.join(labels_dir, f) \n",
" for f in os.listdir(labels_dir) \n",
" if f.endswith('.nii.gz') and not f.startswith('._')])\n",
"\n",
"assert len(train_images) == len(train_labels), \"Количество изображений и масок не совпадает\"\n",
"print(f\"Найдено пар изображение-маска: {len(train_images)}\")\n",
"\n",
"# --- 2. Определение максимального размера и округление до кратного 16 ---\n",
"print(\"Определяем максимальный размер по всем осям...\")\n",
"max_shape = [0, 0, 0]\n",
"for img_path in train_images:\n",
" img = nib.load(img_path)\n",
" shape = img.shape\n",
" max_shape = [max(max_shape[i], shape[i]) for i in range(3)]\n",
"print(f\"Максимальный размер (H, W, D): {max_shape}\")\n",
"\n",
"# Округляем до ближайшего числа, кратного 16\n",
"aligned_shape = [((dim + 15) // 16) * 16 for dim in max_shape]\n",
"print(f\"Размер после выравнивания (кратно 16): {aligned_shape}\")\n",
"\n",
"# --- 3. Создание словарей данных и разбиение на train/val ---\n",
"data_dicts = [{\"image\": img, \"label\": lbl} for img, lbl in zip(train_images, train_labels)]\n",
"split_idx = int(0.8 * len(data_dicts))\n",
"train_dicts = data_dicts[:split_idx]\n",
"val_dicts = data_dicts[split_idx:]\n",
"print(f\"Тренировочных: {len(train_dicts)}, Валидационных: {len(val_dicts)}\")\n",
"\n",
"# --- 4. Трансформы (используем aligned_shape) ---\n",
"train_transforms = Compose([\n",
" LoadImaged(keys=[\"image\", \"label\"]),\n",
" EnsureChannelFirstd(keys=[\"image\", \"label\"]),\n",
" ScaleIntensityd(keys=[\"image\"]),\n",
" SpatialPadd(keys=[\"image\", \"label\"], spatial_size=aligned_shape, method=\"symmetric\"),\n",
" RandRotate90d(keys=[\"image\", \"label\"], prob=0.5, spatial_axes=(0, 1)),\n",
" RandFlipd(keys=[\"image\", \"label\"], prob=0.5, spatial_axis=0),\n",
" RandShiftIntensityd(keys=[\"image\"], offsets=0.1, prob=0.5),\n",
" ToTensord(keys=[\"image\", \"label\"])\n",
"])\n",
"\n",
"val_transforms = Compose([\n",
" LoadImaged(keys=[\"image\", \"label\"]),\n",
" EnsureChannelFirstd(keys=[\"image\", \"label\"]),\n",
" ScaleIntensityd(keys=[\"image\"]),\n",
" SpatialPadd(keys=[\"image\", \"label\"], spatial_size=aligned_shape, method=\"symmetric\"),\n",
" ToTensord(keys=[\"image\", \"label\"])\n",
"])\n",
"\n",
"# --- 5. Датасеты и загрузчики ---\n",
"train_ds = CacheDataset(data=train_dicts, transform=train_transforms, cache_rate=0.5)\n",
"val_ds = CacheDataset(data=val_dicts, transform=val_transforms, cache_rate=0.5)\n",
"\n",
"batch_size = 2\n",
"train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=2)\n",
"val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=2)\n",
"\n",
"# --- 6. Модель, функция потерь, оптимизатор ---\n",
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
"print(f\"Используется устройство: {device}\")\n",
"\n",
"model = UNet(\n",
" spatial_dims=3,\n",
" in_channels=1,\n",
" out_channels=2,\n",
" channels=(16, 32, 64, 128, 256),\n",
" strides=(2, 2, 2, 2),\n",
" num_res_units=2,\n",
").to(device)\n",
"\n",
"loss_function = DiceCELoss(to_onehot_y=True, softmax=True)\n",
"optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\n",
"dice_metric = DiceMetric(include_background=False, reduction=\"mean\")\n",
"\n",
"roi_size = (96, 96, 96)\n",
"sw_batch_size = 4\n",
"\n",
"# --- 7. Цикл обучения ---\n",
"max_epochs = 50\n",
"val_interval = 2\n",
"best_metric = -1\n",
"best_metric_epoch = -1\n",
"\n",
"for epoch in range(max_epochs):\n",
" print(f\"Эпоха {epoch+1}/{max_epochs}\")\n",
" model.train()\n",
" epoch_loss = 0\n",
" step = 0\n",
" for batch_data in train_loader:\n",
" inputs, labels = batch_data[\"image\"].to(device), batch_data[\"label\"].to(device)\n",
" optimizer.zero_grad()\n",
" outputs = model(inputs)\n",
" loss = loss_function(outputs, labels)\n",
" loss.backward()\n",
" optimizer.step()\n",
" epoch_loss += loss.item()\n",
" step += 1\n",
" \n",
" epoch_loss /= step\n",
" print(f\" Средняя потеря: {epoch_loss:.4f}\")\n",
"\n",
" if (epoch + 1) % val_interval == 0:\n",
" model.eval()\n",
" with torch.no_grad():\n",
" for val_data in val_loader:\n",
" val_inputs, val_labels = val_data[\"image\"].to(device), val_data[\"label\"].to(device)\n",
" val_pred = sliding_window_inference(\n",
" val_inputs, roi_size, sw_batch_size, model, overlap=0.5\n",
" )\n",
" dice_metric(y_pred=val_pred, y=val_labels)\n",
" metric = dice_metric.aggregate().item()\n",
" dice_metric.reset()\n",
" print(f\" Dice на валидации: {metric:.4f}\")\n",
" if metric > best_metric:\n",
" best_metric = metric\n",
" best_metric_epoch = epoch + 1\n",
" torch.save(model.state_dict(), \"best_spleen_model.pth\")\n",
" print(\" ✅ Модель сохранена\")\n",
"\n",
"print(f\"🎉 Лучший Dice: {best_metric:.4f} на эпохе {best_metric_epoch}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9",
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"from monai.networks.nets import UNet\n",
"\n",
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
"\n",
"# Создаём архитектуру (те же параметры, что и при обучении)\n",
"model = UNet(\n",
" spatial_dims=3,\n",
" in_channels=1,\n",
" out_channels=2,\n",
" channels=(16, 32, 64, 128, 256),\n",
" strides=(2, 2, 2, 2),\n",
" num_res_units=2,\n",
").to(device)\n",
"\n",
"# Загружаем веса\n",
"checkpoint = torch.load(\"best_spleen_model.pth\", map_location=device)\n",
"model.load_state_dict(checkpoint)\n",
"model.eval()\n",
"print(\"✅ Модель загружена и готова к инференсу.\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "10",
"metadata": {},
"outputs": [],
"source": [
"from monai.transforms import Compose, LoadImage, EnsureChannelFirst, ScaleIntensity, SpatialPad, ToTensor\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"# Параметры (те же, что использовались при обучении)\n",
"target_shape = [512, 512, 176] # выровненный размер\n",
"\n",
"# Трансформы для инференса\n",
"infer_transform = Compose([\n",
" LoadImage(image_only=True),\n",
" EnsureChannelFirst(),\n",
" ScaleIntensity(),\n",
" SpatialPad(spatial_size=target_shape, method=\"symmetric\"),\n",
" ToTensor()\n",
"])\n",
"\n",
"# Выберите тестовый файл (например, первый из imagesTs)\n",
"import os\n",
"test_dir = 'Task09_Spleen/imagesTs'\n",
"if os.path.exists(test_dir):\n",
" test_files = [f for f in os.listdir(test_dir) if f.endswith('.nii.gz') and not f.startswith('._')]\n",
" if test_files:\n",
" test_path = os.path.join(test_dir, test_files[0])\n",
" else:\n",
" # Если нет тестовых, возьмём валидационный из тренировочных (например, 35-й)\n",
" train_dir = 'Task09_Spleen/imagesTr'\n",
" train_files = [f for f in os.listdir(train_dir) if f.endswith('.nii.gz') and not f.startswith('._')]\n",
" test_path = os.path.join(train_dir, train_files[-5]) # последний файл\n",
"else:\n",
" train_dir = 'Task09_Spleen/imagesTr'\n",
" train_files = [f for f in os.listdir(train_dir) if f.endswith('.nii.gz') and not f.startswith('._')]\n",
" test_path = os.path.join(train_dir, train_files[-5])\n",
"\n",
"print(f\"Тестовый файл: {test_path}\")\n",
"\n",
"# Применяем трансформации и добавляем batch dimension\n",
"input_tensor = infer_transform(test_path).unsqueeze(0).to(device) # [1,1,H,W,D]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "11",
"metadata": {},
"outputs": [],
"source": [
"from monai.inferers import sliding_window_inference\n",
"\n",
"roi_size = (96, 96, 96) # размер окна (можно подобрать)\n",
"sw_batch_size = 4\n",
"\n",
"with torch.no_grad():\n",
" # Прямой проход (если изображение целиком влезает, можно использовать model(input_tensor))\n",
" # Но для 3D лучше sliding window:\n",
" pred_logits = sliding_window_inference(\n",
" input_tensor, roi_size, sw_batch_size, model, overlap=0.5\n",
" )\n",
" pred_mask = torch.argmax(pred_logits, dim=1).cpu().numpy()[0] # [H,W,D]\n",
"\n",
"print(\"Предсказание выполнено.\")\n",
"print(f\"Уникальные значения в маске: {np.unique(pred_mask)}\") # должно быть [0,1]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "12",
"metadata": {},
"outputs": [],
"source": [
"# Загружаем оригинальное изображение (без трансформаций)\n",
"import nibabel as nib\n",
"orig_img_nii = nib.load(test_path)\n",
"orig_img = orig_img_nii.get_fdata() # [H,W,D] исходный размер\n",
"\n",
"# Размеры\n",
"H, W, D = orig_img.shape\n",
"print(f\"Исходный размер: {orig_img.shape}\")\n",
"\n",
"# Обрежем предсказанную маску до исходного размера (если нужно)\n",
"pred_mask_cropped = pred_mask[:H, :W, :D]\n",
"\n",
"# Выберем несколько срезов по оси Z\n",
"slices = np.linspace(0, D-1, 5, dtype=int)\n",
"\n",
"plt.figure(figsize=(15, 10))\n",
"for i, idx in enumerate(slices):\n",
" plt.subplot(2, 5, i+1)\n",
" plt.imshow(orig_img[:, :, idx], cmap='gray')\n",
" plt.title(f'Срез {idx}')\n",
" plt.axis('off')\n",
"\n",
" plt.subplot(2, 5, i+6)\n",
" plt.imshow(pred_mask_cropped[:, :, idx], cmap='hot', alpha=0.7)\n",
" plt.imshow(orig_img[:, :, idx], cmap='gray', alpha=0.3)\n",
" plt.title(f'Маска срез {idx}')\n",
" plt.axis('off')\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "13",
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"import numpy as np\n",
"from monai.networks.nets import UNet\n",
"from monai.transforms import Compose, LoadImage, EnsureChannelFirst, ScaleIntensity, SpatialPad, ToTensor\n",
"from monai.inferers import sliding_window_inference\n",
"from monai.metrics import DiceMetric, HausdorffDistanceMetric\n",
"import os\n",
"import nibabel as nib\n",
"\n",
"# 1. Загружаем модель (как вы уже сделали)\n",
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
"model = UNet(\n",
" spatial_dims=3,\n",
" in_channels=1,\n",
" out_channels=2,\n",
" channels=(16, 32, 64, 128, 256),\n",
" strides=(2, 2, 2, 2),\n",
" num_res_units=2,\n",
").to(device)\n",
"checkpoint = torch.load(\"best_spleen_model.pth\", map_location=device)\n",
"model.load_state_dict(checkpoint)\n",
"model.eval()\n",
"\n",
"# 2. Параметры (те же, что при обучении)\n",
"target_shape = [512, 512, 176] # выровненный размер\n",
"roi_size = (96, 96, 96)\n",
"sw_batch_size = 4\n",
"\n",
"# Трансформы для инференса\n",
"infer_transform = Compose([\n",
" LoadImage(image_only=True),\n",
" EnsureChannelFirst(),\n",
" ScaleIntensity(),\n",
" SpatialPad(spatial_size=target_shape, method=\"symmetric\"),\n",
" ToTensor()\n",
"])\n",
"\n",
"# 3. Собираем все валидационные пары (изображение + маска)\n",
"base_dir = 'Task09_Spleen'\n",
"val_images = sorted([os.path.join(base_dir, 'imagesTr', f) \n",
" for f in os.listdir(os.path.join(base_dir, 'imagesTr')) \n",
" if f.endswith('.nii.gz') and not f.startswith('._')])\n",
"val_labels = sorted([os.path.join(base_dir, 'labelsTr', f) \n",
" for f in os.listdir(os.path.join(base_dir, 'labelsTr')) \n",
" if f.endswith('.nii.gz') and not f.startswith('._')])\n",
"\n",
"# Предположим, что последние 9 файлов были валидационными (как в вашем разбиении)\n",
"val_images = val_images[-9:]\n",
"val_labels = val_labels[-9:]\n",
"\n",
"print(f\"Валидационных образцов: {len(val_images)}\")\n",
"\n",
"# 4. Метрики\n",
"dice_metric = DiceMetric(include_background=False, reduction=\"mean\")\n",
"hausdorff_metric = HausdorffDistanceMetric(include_background=False, percentile=95)\n",
"\n",
"# 5. Цикл по всем валидационным образцам\n",
"all_dice = []\n",
"all_hd95 = []\n",
"\n",
"with torch.no_grad():\n",
" for img_path, mask_path in zip(val_images, val_labels):\n",
" # Подготовка изображения\n",
" input_tensor = infer_transform(img_path).unsqueeze(0).to(device)\n",
" \n",
" # Предсказание\n",
" pred_logits = sliding_window_inference(\n",
" input_tensor, roi_size, sw_batch_size, model, overlap=0.5\n",
" )\n",
" pred_mask = torch.argmax(pred_logits, dim=1).cpu().numpy()[0] # [H,W,D]\n",
" \n",
" # Загрузка истинной маски (без паддинга)\n",
" true_mask_nii = nib.load(mask_path)\n",
" true_mask = true_mask_nii.get_fdata()\n",
" \n",
" # Обрезаем предсказание до исходного размера\n",
" H, W, D = true_mask.shape\n",
" pred_mask_cropped = pred_mask[:H, :W, :D]\n",
" \n",
" # Преобразуем в тензоры для MONAI метрик\n",
" pred_tensor = torch.from_numpy(pred_mask_cropped).unsqueeze(0).unsqueeze(0).long() # [1,1,H,W,D]\n",
" true_tensor = torch.from_numpy(true_mask).unsqueeze(0).unsqueeze(0).long()\n",
" \n",
" # Вычисляем Dice\n",
" dice = dice_metric(pred_tensor, true_tensor)\n",
" all_dice.append(dice.item())\n",
" \n",
" # Вычисляем Hausdorff Distance 95%\n",
" hd = hausdorff_metric(pred_tensor, true_tensor)\n",
" all_hd95.append(hd.item())\n",
" \n",
" print(f\"Файл: {os.path.basename(img_path)} -> Dice: {dice.item():.4f}, HD95: {hd.item():.2f} мм\")\n",
"\n",
"# 6. Итоговая статистика\n",
"print(\"\\n\" + \"=\"*40)\n",
"print(f\"Средний Dice по валидации: {np.mean(all_dice):.4f} ± {np.std(all_dice):.4f}\")\n",
"print(f\"Средний HD95 по валидации: {np.mean(all_hd95):.2f} ± {np.std(all_hd95):.2f} мм\")\n",
"print(\"=\"*40)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "14",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import torch\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from monai.networks.nets import UNet\n",
"from monai.transforms import Compose, LoadImage, EnsureChannelFirst, ScaleIntensity, SpatialPad, ToTensor\n",
"from monai.inferers import sliding_window_inference\n",
"import nibabel as nib\n",
"import os\n",
"\n",
"# --- 1. Загрузка модели ---\n",
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
"model = UNet(\n",
" spatial_dims=3,\n",
" in_channels=1,\n",
" out_channels=2,\n",
" channels=(16, 32, 64, 128, 256),\n",
" strides=(2, 2, 2, 2),\n",
" num_res_units=2,\n",
").to(device)\n",
"checkpoint = torch.load(\"best_spleen_model.pth\", map_location=device)\n",
"model.load_state_dict(checkpoint)\n",
"model.eval()\n",
"print(\"✅ Модель загружена\")\n",
"\n",
"# --- 2. Вывод структуры model.model для выбора целевого слоя ---\n",
"print(\"\\nСтруктура model.model:\")\n",
"for idx, module in enumerate(model.model):\n",
" print(f\"[{idx}]: {module.__class__.__name__}\")\n",
"\n",
"# --- 3. Выбор целевого слоя ---\n",
"# Обычно для Grad‑CAM берут последний сверточный слой энкодера.\n",
"# В U‑Net от MONAI энкодер состоит из блоков `Down`.\n",
"# Выберите индекс блока, который вы хотите использовать (например, последний Down перед bottleneck).\n",
"# Попробуйте индекс 2 или 3, если они есть.\n",
"# В вашем случае, возможно, подойдёт индекс 2.\n",
"# Если не уверены, начните с последнего Down блока.\n",
"target_layer_idx = 2 # Попробуйте изменить на 2 или 3, в зависимости от вывода выше\n",
"target_layer = model.model[target_layer_idx]\n",
"print(f\"\\nВыбран целевой слой: model.model[{target_layer_idx}] -> {target_layer.__class__.__name__}\")\n",
"\n",
"# --- 4. Класс Grad-CAM ---\n",
"class GradCAM:\n",
" def __init__(self, model, target_layer):\n",
" self.model = model\n",
" self.target_layer = target_layer\n",
" self.gradients = None\n",
" self.activations = None\n",
" \n",
" target_layer.register_forward_hook(self.save_activation)\n",
" target_layer.register_full_backward_hook(self.save_gradient) # обратите внимание на register_full_backward_hook\n",
" \n",
" def save_activation(self, module, input, output):\n",
" self.activations = output.detach()\n",
" \n",
" def save_gradient(self, module, grad_input, grad_output):\n",
" self.gradients = grad_output[0].detach()\n",
" \n",
" def generate_cam(self, input_tensor, target_class=1):\n",
" self.model.zero_grad()\n",
" output = self.model(input_tensor)\n",
" \n",
" target = output[:, target_class].sum()\n",
" target.backward()\n",
" \n",
" weights = torch.mean(self.gradients, dim=(2, 3, 4), keepdim=True)\n",
" cam = torch.sum(weights * self.activations, dim=1, keepdim=True)\n",
" cam = torch.relu(cam)\n",
" cam = cam / (cam.max() + 1e-8)\n",
" return cam.squeeze().cpu().numpy()\n",
"\n",
"# --- 5. Подготовка тестового изображения ---\n",
"test_dir = 'Task09_Spleen/imagesTs'\n",
"if os.path.exists(test_dir):\n",
" test_files = [f for f in os.listdir(test_dir) if f.endswith('.nii.gz') and not f.startswith('._')]\n",
" if test_files:\n",
" test_path = os.path.join(test_dir, test_files[0])\n",
" else:\n",
" train_dir = 'Task09_Spleen/imagesTr'\n",
" train_files = [f for f in os.listdir(train_dir) if f.endswith('.nii.gz') and not f.startswith('._')]\n",
" test_path = os.path.join(train_dir, train_files[-5])\n",
"else:\n",
" train_dir = 'Task09_Spleen/imagesTr'\n",
" train_files = [f for f in os.listdir(train_dir) if f.endswith('.nii.gz') and not f.startswith('._')]\n",
" test_path = os.path.join(train_dir, train_files[-5])\n",
"\n",
"print(f\"\\nТестовый файл: {test_path}\")\n",
"\n",
"target_shape = [512, 512, 176]\n",
"infer_transform = Compose([\n",
" LoadImage(image_only=True),\n",
" EnsureChannelFirst(),\n",
" ScaleIntensity(),\n",
" SpatialPad(spatial_size=target_shape, method=\"symmetric\"),\n",
" ToTensor()\n",
"])\n",
"\n",
"input_tensor = infer_transform(test_path).unsqueeze(0).to(device)\n",
"\n",
"# --- 6. Получение Grad-CAM ---\n",
"grad_cam = GradCAM(model, target_layer)\n",
"cam_3d = grad_cam.generate_cam(input_tensor, target_class=1)\n",
"\n",
"# --- 7. Визуализация ---\n",
"orig_img = nib.load(test_path).get_fdata()\n",
"H, W, D = orig_img.shape\n",
"\n",
"slices = np.linspace(0, D-1, 5, dtype=int)\n",
"\n",
"plt.figure(figsize=(15, 10))\n",
"for i, idx in enumerate(slices):\n",
" cam_slice = cam_3d[:H, :W, idx]\n",
" \n",
" plt.subplot(2, 5, i+1)\n",
" plt.imshow(orig_img[:, :, idx], cmap='gray')\n",
" plt.title(f'Срез {idx}')\n",
" plt.axis('off')\n",
" \n",
" plt.subplot(2, 5, i+6)\n",
" plt.imshow(orig_img[:, :, idx], cmap='gray', alpha=0.5)\n",
" plt.imshow(cam_slice, cmap='jet', alpha=0.5, interpolation='bilinear')\n",
" plt.title(f'Grad-CAM срез {idx}')\n",
" plt.axis('off')\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "15",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import torch\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from monai.networks.nets import UNet\n",
"from monai.transforms import Compose, LoadImage, EnsureChannelFirst, ScaleIntensity, SpatialPad, ToTensor\n",
"import nibabel as nib\n",
"import os\n",
"\n",
"# --- 1. Загрузка модели ---\n",
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
"model = UNet(\n",
" spatial_dims=3,\n",
" in_channels=1,\n",
" out_channels=2,\n",
" channels=(16, 32, 64, 128, 256),\n",
" strides=(2, 2, 2, 2),\n",
" num_res_units=2,\n",
").to(device)\n",
"checkpoint = torch.load(\"best_spleen_model.pth\", map_location=device)\n",
"model.load_state_dict(checkpoint)\n",
"model.eval()\n",
"print(\"✅ Модель загружена\")\n",
"\n",
"# --- 2. Вывод структуры model.model ---\n",
"print(\"\\n📋 Доступные слои в model.model:\")\n",
"for idx, module in enumerate(model.model):\n",
" print(f\" [{idx}]: {module.__class__.__name__}\")\n",
"\n",
"# --- 3. Класс Grad-CAM ---\n",
"class GradCAM:\n",
" def __init__(self, model, target_layer):\n",
" self.model = model\n",
" self.target_layer = target_layer\n",
" self.gradients = None\n",
" self.activations = None\n",
" \n",
" target_layer.register_forward_hook(self.save_activation)\n",
" target_layer.register_full_backward_hook(self.save_gradient)\n",
" \n",
" def save_activation(self, module, input, output):\n",
" self.activations = output.detach()\n",
" \n",
" def save_gradient(self, module, grad_input, grad_output):\n",
" self.gradients = grad_output[0].detach()\n",
" \n",
" def generate_cam(self, input_tensor, target_class=1):\n",
" self.model.zero_grad()\n",
" output = self.model(input_tensor)\n",
" \n",
" target = output[:, target_class].sum()\n",
" target.backward()\n",
" \n",
" weights = torch.mean(self.gradients, dim=(2, 3, 4), keepdim=True)\n",
" cam = torch.sum(weights * self.activations, dim=1, keepdim=True)\n",
" cam = torch.relu(cam)\n",
" cam = cam / (cam.max() + 1e-8)\n",
" return cam.squeeze().cpu().numpy() # [H, W, D]\n",
"\n",
"# --- 4. Подготовка тестовых данных ---\n",
"base_dir = 'Task09_Spleen'\n",
"img_dir = os.path.join(base_dir, 'imagesTr') # используем тренировочные, если нет test\n",
"label_dir = os.path.join(base_dir, 'labelsTr')\n",
"\n",
"# Получаем списки файлов\n",
"img_files = sorted([f for f in os.listdir(img_dir) if f.endswith('.nii.gz') and not f.startswith('._')])\n",
"label_files = sorted([f for f in os.listdir(label_dir) if f.endswith('.nii.gz') and not f.startswith('._')])\n",
"\n",
"print(f\"\\n📂 Найдено изображений: {len(img_files)}, масок: {len(label_files)}\")\n",
"\n",
"# Создаём словарь соответствий: имя маски (без пути) -> полный путь\n",
"label_dict = {os.path.basename(f): os.path.join(label_dir, f) for f in label_files}\n",
"\n",
"# Выбираем файл изображения (например, 10-й)\n",
"idx_file = 10 # можно изменить\n",
"img_filename = img_files[idx_file]\n",
"img_path = os.path.join(img_dir, img_filename)\n",
"\n",
"# Ищем соответствующую маску: возможно, имя совпадает или отличается суффиксом\n",
"mask_path = None\n",
"if img_filename in label_dict:\n",
" mask_path = label_dict[img_filename]\n",
"else:\n",
" # Попробуем убрать суффикс _0000, если он есть\n",
" base_name = img_filename.replace('_0000.nii.gz', '.nii.gz')\n",
" if base_name in label_dict:\n",
" mask_path = label_dict[base_name]\n",
"\n",
"if mask_path is None:\n",
" print(f\"❌ Маска для {img_filename} не найдена. Будет показано только КТ.\")\n",
" has_mask = False\n",
"else:\n",
" has_mask = True\n",
" print(f\"✅ Найдена маска: {mask_path}\")\n",
"\n",
"print(f\"📂 Тестовое изображение: {img_path}\")\n",
"\n",
"# --- 5. Трансформации для инференса ---\n",
"target_shape = [512, 512, 176] # выровненный размер\n",
"infer_transform = Compose([\n",
" LoadImage(image_only=True),\n",
" EnsureChannelFirst(),\n",
" ScaleIntensity(),\n",
" SpatialPad(spatial_size=target_shape, method=\"symmetric\"),\n",
" ToTensor()\n",
"])\n",
"\n",
"input_tensor = infer_transform(img_path).unsqueeze(0).to(device)\n",
"\n",
"# Если есть маска, загрузим её (без паддинга)\n",
"if has_mask:\n",
" true_nii = nib.load(mask_path)\n",
" true_mask = true_nii.get_fdata() # [H, W, D]\n",
" H, W, D = true_mask.shape\n",
"else:\n",
" # Если маски нет, используем размеры изображения (после паддинга мы не знаем исходный размер, поэтому загрузим оригинал)\n",
" orig_nii = nib.load(img_path)\n",
" orig_img = orig_nii.get_fdata()\n",
" H, W, D = orig_img.shape\n",
"\n",
"# --- 6. Выбор слоёв для Grad-CAM ---\n",
"# Запрашиваем у пользователя индексы через ввод (в Jupyter можно использовать input)\n",
"print(\"\\n🔍 Введите индексы слоёв для анализа через пробел (например: 2 3 4):\")\n",
"user_input = input(\"Индексы: \").strip()\n",
"if user_input:\n",
" layer_indices = [int(x) for x in user_input.split()]\n",
"else:\n",
" # Если ничего не введено, используем последний слой\n",
" layer_indices = [len(model.model) - 1]\n",
" print(f\" Используется последний слой: {layer_indices[0]}\")\n",
"\n",
"# Проверяем, что индексы в допустимом диапазоне\n",
"max_idx = len(model.model) - 1\n",
"layer_indices = [idx for idx in layer_indices if 0 <= idx <= max_idx]\n",
"if not layer_indices:\n",
" raise ValueError(f\"Нет допустимых индексов. Доступны 0..{max_idx}\")\n",
"\n",
"print(f\"\\n🔍 Выбранные слои: {layer_indices}\")\n",
"\n",
"cam_maps = {}\n",
"for idx in layer_indices:\n",
" target_layer = model.model[idx]\n",
" grad_cam = GradCAM(model, target_layer)\n",
" cam = grad_cam.generate_cam(input_tensor, target_class=1)\n",
" cam_maps[idx] = cam\n",
" print(f\" Слой {idx} ({target_layer.__class__.__name__}) – карта получена\")\n",
"\n",
"# --- 7. Визуализация ---\n",
"# Выбираем несколько равномерно распределённых срезов по оси Z\n",
"num_slices = 5\n",
"slices = np.linspace(0, D-1, num_slices, dtype=int)\n",
"\n",
"# Если есть маска, визуализируем её контуром, иначе только КТ + Grad-CAM\n",
"plt.figure(figsize=(4*num_slices, 4*len(layer_indices)))\n",
"\n",
"for i, layer_idx in enumerate(layer_indices):\n",
" cam_full = cam_maps[layer_idx] # [H_pad, W_pad, D_pad]\n",
" for j, slice_idx in enumerate(slices):\n",
" # Обрезаем карту до исходного размера\n",
" cam_slice = cam_full[:H, :W, slice_idx]\n",
" \n",
" # Загружаем срез КТ (можно из оригинального файла, но проще из уже загруженного)\n",
" # Для КТ используем orig_img (если нет маски, мы его уже загрузили)\n",
" if has_mask:\n",
" img_slice = true_nii.dataobj[:, :, slice_idx].T # может потребоваться транспонирование\n",
" else:\n",
" img_slice = orig_img[:, :, slice_idx].T\n",
" \n",
" # Позиция в подграфике\n",
" plt.subplot(len(layer_indices), num_slices, i*num_slices + j + 1)\n",
" \n",
" # Показываем КТ подложкой\n",
" plt.imshow(img_slice, cmap='gray')\n",
" # Накладываем Grad-CAM\n",
" plt.imshow(cam_slice, cmap='jet', alpha=0.4, interpolation='bilinear')\n",
" \n",
" # Если есть маска, добавляем её контур\n",
" if has_mask:\n",
" mask_slice = true_mask[:, :, slice_idx]\n",
" plt.contour(mask_slice, colors='white', linewidths=0.5, alpha=0.8)\n",
" \n",
" plt.title(f'Слой {layer_idx}, срез {slice_idx}')\n",
" plt.axis('off')\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "16",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "DataSphere Kernel",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}