From 5042305df02715f8154d8b1a6baffe86bd7a240b Mon Sep 17 00:00:00 2001 From: IUnknown-work Date: Sat, 22 Aug 2026 13:17:29 +0300 Subject: [PATCH] Initial commit: project structure, scripts and documentation --- .gitattributes | 7 + .gitignore | 46 + README.md | 467 +++++ ai-use-program.doc | 3 + collect_for_ai.py | 242 +++ data/example/README.md | 48 + data/example/images/.gitkeep | 0 data/example/images_labels/.gitkeep | 0 data/example/images_labels/polygons/.gitkeep | 0 data/example/images_results/.gitkeep | 0 labs/lab#1.pdf | 3 + labs/lab#1.tex | 975 ++++++++++ labs/lab#2.pdf | 3 + labs/lab#2.tex | 660 +++++++ labs/lab#3.pdf | 3 + labs/lab#3.tex | 1064 +++++++++++ labs/lab#4.pdf | 3 + labs/lab#4.tex | 868 +++++++++ labs/logo.png | Bin 0 -> 32842 bytes labs/Тема 2.1/Лабораторная работа.docx | 3 + lectures/.gitkeep | 0 lectures/К26_1_1.pdf | 3 + lectures/К26_2_1.pdf | 3 + lectures/К26_2_2.pdf | 3 + references.txt | 1 + scripts/check_PyTorch_gpu.py | 13 + scripts/check_tf_gpu.py | 21 + scripts/config/data.yaml | 25 + scripts/train_yolo.py | 99 + scripts/train_yolo_seg.py | 104 + scripts/ttv_freq2.py | 353 ++++ scripts/Тема 2.1/analysis.py | 991 ++++++++++ scripts/Тема 2.1/config_3d.py | 150 ++ scripts/Тема 2.1/feature_3d.py | 593 ++++++ scripts/Тема 2.1/segmentation.py | 734 +++++++ scripts/Тема 2.2/nrrd_to_yolo.py | 246 +++ seminars/.gitkeep | 0 seminars/FeatureExtractionClient.ipynb | 518 +++++ seminars/Тема 1.1/.gitkeep | 0 seminars/Тема 1.1/Dataset.ipynb | 175 ++ seminars/Тема 1.1/Noise.ipynb | 1521 +++++++++++++++ seminars/Тема 1.1/Resource1.ipynb | 274 +++ seminars/Тема 1.1/resource2.ipynb | 204 ++ seminars/Тема 1.1/segm2.ipynb | 1684 +++++++++++++++++ seminars/Тема 1.2/.gitkeep | 0 seminars/Тема 1.2/Практикум1.ipynb | 303 +++ seminars/Тема 1.2/Т1.ipynb | 271 +++ seminars/Тема 1.2/Т2.ipynb | 928 +++++++++ seminars/Тема 2.1/.gitkeep | 0 seminars/Тема 2.1/radiomics_report.html | 237 +++ seminars/Тема 2.1/requirements_3d.txt | 23 + .../Тема 2.1/segmentation_full_report.html | 291 +++ seminars/Тема 2.1/Лабораторная работа.docx | 3 + seminars/Тема 2.2/.gitkeep | 0 54 files changed, 14166 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 README.md create mode 100644 ai-use-program.doc create mode 100644 collect_for_ai.py create mode 100644 data/example/README.md create mode 100644 data/example/images/.gitkeep create mode 100644 data/example/images_labels/.gitkeep create mode 100644 data/example/images_labels/polygons/.gitkeep create mode 100644 data/example/images_results/.gitkeep create mode 100644 labs/lab#1.pdf create mode 100644 labs/lab#1.tex create mode 100644 labs/lab#2.pdf create mode 100644 labs/lab#2.tex create mode 100644 labs/lab#3.pdf create mode 100644 labs/lab#3.tex create mode 100644 labs/lab#4.pdf create mode 100644 labs/lab#4.tex create mode 100644 labs/logo.png create mode 100644 labs/Тема 2.1/Лабораторная работа.docx create mode 100644 lectures/.gitkeep create mode 100644 lectures/К26_1_1.pdf create mode 100644 lectures/К26_2_1.pdf create mode 100644 lectures/К26_2_2.pdf create mode 100644 references.txt create mode 100644 scripts/check_PyTorch_gpu.py create mode 100644 scripts/check_tf_gpu.py create mode 100644 scripts/config/data.yaml create mode 100644 scripts/train_yolo.py create mode 100644 scripts/train_yolo_seg.py create mode 100644 scripts/ttv_freq2.py create mode 100644 scripts/Тема 2.1/analysis.py create mode 100644 scripts/Тема 2.1/config_3d.py create mode 100644 scripts/Тема 2.1/feature_3d.py create mode 100644 scripts/Тема 2.1/segmentation.py create mode 100644 scripts/Тема 2.2/nrrd_to_yolo.py create mode 100644 seminars/.gitkeep create mode 100644 seminars/FeatureExtractionClient.ipynb create mode 100644 seminars/Тема 1.1/.gitkeep create mode 100644 seminars/Тема 1.1/Dataset.ipynb create mode 100644 seminars/Тема 1.1/Noise.ipynb create mode 100644 seminars/Тема 1.1/Resource1.ipynb create mode 100644 seminars/Тема 1.1/resource2.ipynb create mode 100644 seminars/Тема 1.1/segm2.ipynb create mode 100644 seminars/Тема 1.2/.gitkeep create mode 100644 seminars/Тема 1.2/Практикум1.ipynb create mode 100644 seminars/Тема 1.2/Т1.ipynb create mode 100644 seminars/Тема 1.2/Т2.ipynb create mode 100644 seminars/Тема 2.1/.gitkeep create mode 100644 seminars/Тема 2.1/radiomics_report.html create mode 100644 seminars/Тема 2.1/requirements_3d.txt create mode 100644 seminars/Тема 2.1/segmentation_full_report.html create mode 100644 seminars/Тема 2.1/Лабораторная работа.docx create mode 100644 seminars/Тема 2.2/.gitkeep diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c14e4b9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +*.tif filter=lfs diff=lfs merge=lfs -text +*.pdf filter=lfs diff=lfs merge=lfs -text +*.docx filter=lfs diff=lfs merge=lfs -text +*.doc filter=lfs diff=lfs merge=lfs -text +*.tiff filter=lfs diff=lfs merge=lfs -text +*.ipynb filter=nbstripout +*.ipynb diff=ipynb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..201e045 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Виртуальные окружения +env/ +venv/ +.venv/ + +# Веса моделей +*.pt +*.pth +*.onnx + +# Результаты обучения +runs/ + +# Служебные файлы YOLO в корне +/test.txt +/train.txt +/val.txt +/labels.cache + +# Артефакты конвертаций в корне +/images/ +/labels/ + +# Данные: игнорируем всё, кроме example и README +data/** +!data/README.md +!data/example/ +!data/example/** + +# Python кэш +__pycache__/ +*.pyc +*.pyo +.ipynb_checkpoints/ + +# IDE и ОС +.vscode/ +.idea/ +.DS_Store +Thumbs.db + +# Дампы +ai-med-edu.txt + +# Файл классов в корне +/predefined_classes.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..00ba05e --- /dev/null +++ b/README.md @@ -0,0 +1,467 @@ +# AI Medical Education & Imaging (ai-med-edu) + +Образовательный проект по AI-обработке медицинских изображений: детекция и сегментация анатомических структур (почки, их сегменты, опухоли, кисты, абсцессы) на основе DICOM и 3D NRRD данных с использованием архитектур YOLO. + +## 🎯 Цели проекта + +- Конвертация медицинских 3D-данных (NRRD, DICOM) в 2D-срезы формата YOLO +- Обучение моделей детекции (yolov8s, yolo26n) и сегментации (yolov8s-seg) +- Автоматическая подготовка и стратифицированное разделение датасета на train/val/test +- Поддержка индивидуальных схем классов для каждого препарата +- Проверка доступности GPU (PyTorch + CUDA) +- Образовательные материалы: лекции, семинары, лабораторные работы + +## 📂 Структура репозитория + +``` +ai-med-edu/ +│ +├── README.md # Этот файл +├── ai-use-program.doc # Учебная программа +├── references.txt # Список литературы +├── labels.cache # Кэш лейблов YOLO +│ +├── data/ # Эксперименты (каждый препарат - отдельная папка) +│ ├── 1_2236040_zno_cyst_AF/ # Эксперимент 1: ZNO Cyst AF +│ │ ├── predefined_classes.txt # Схема классов (12 классов) +│ │ ├── data.yaml # Конфиг YOLO +│ │ ├── 501 arterial ax 1mm iDose (5).nrrd # Исходные 3D данные +│ │ ├── Segmentation.seg.nrrd # Сегментационная маска +│ │ ├── dcm/ # DICOM-срезы (1042 файла) +│ │ ├── images/ # PNG-срезы (300 файлов) +│ │ ├── images_labels/ # Bbox-разметка +│ │ │ └── polygons/ # Полигоны для сегментации +│ │ └── images_result/ # Визуализация результатов +│ │ └── masks_sam/ # Маски SAM +│ │ +│ ├── example/ # Пример структуры +│ │ ├── README.md +│ │ ├── images/ +│ │ ├── images_labels/ +│ │ │ └── polygons/ +│ │ └── images_results/ +│ │ +│ └── prep/ # Подготовительные данные +│ ├── predefined_classes.txt +│ ├── csv/ # CSV-файлы с разметкой +│ ├── images/ # DCM и JPG файлы +│ ├── images_labels/ # TXT-лейблы +│ │ └── polygons/ +│ └── images_results/ # Результаты +│ └── masks_sam/ +│ +├── scripts/ # Универсальные скрипты +│ ├── ttv_freq2.py # Сплиттер (стратифицированный) +│ ├── train_yolo.py # Обучение детекции +│ ├── train_yolo_seg.py # Обучение сегментации +│ ├── check_PyTorch_gpu.py # Проверка GPU +│ ├── check_tf_gpu.py # Проверка TensorFlow GPU +│ ├── config/ # Конфигурационные файлы +│ │ └── data.yaml +│ ├── Тема 2.1/ # Скрипты по теме 2.1 +│ │ ├── analysis.py +│ │ ├── config_3d.py +│ │ ├── feature_3d.py +│ │ └── segmentation.py +│ └── Тема 2.2/ # Скрипты по теме 2.2 +│ └── nrrd_to_yolo.py # Конвертация NRRD → YOLO +│ +├── env/ # Виртуальное окружение Python +│ ├── Include/ +│ ├── Lib/ +│ │ └── site-packages/ # Установленные пакеты +│ ├── Scripts/ # Исполняемые файлы +│ ├── etc/ +│ ├── share/ +│ └── pyvenv.cfg +│ +├── lectures/ # Лекционные материалы +│ ├── К26_1_1.pdf +│ ├── К26_2_1.pdf +│ └── К26_2_2.pdf +│ +├── seminars/ # Семинарские занятия +│ ├── FeatureExtractionClient.ipynb +│ ├── Тема 1.1/ # Jupyter ноутбуки +│ │ ├── Dataset.ipynb +│ │ ├── Noise.ipynb +│ │ ├── Resource1.ipynb +│ │ ├── resource2.ipynb +│ │ └── segm2.ipynb +│ ├── Тема 1.2/ +│ │ ├── Практикум1.ipynb +│ │ ├── Т1.ipynb +│ │ ├── Т2.ipynb +│ │ └── best_spleen_model.pth # Предобученная модель +│ ├── Тема 2.1/ +│ │ ├── radiomics_report.html +│ │ ├── segmentation_full_report.html +│ │ ├── requirements_3d.txt +│ │ └── Лабораторная работа.docx +│ └── Тема 2.2/ +│ +└── labs/ # Лабораторные работы + ├── Тема 1.1/ + ├── Тема 1.2/ + ├── Тема 2.1/ + │ └── Лабораторная работа.docx + └── Тема 2.2/ +``` + +## 🏷️ Схема классов (predefined_classes.txt) + +Каждый эксперимент имеет СВОЙ СОБСТВЕННЫЙ файл `predefined_classes.txt`, расположенный в папке эксперимента. Это позволяет использовать разные наборы классов для разных препаратов. + +Формат: один класс на строку, позиция в файле = YOLO index (0-based). + +### Пример: data/1_2236040_zno_cyst_AF/predefined_classes.txt (12 классов) + +```txt +right_kidney # ID 1 → YOLO idx 0 +right_kidney_upper_segment # ID 2 → YOLO idx 1 +right_kidney_middle_segment # ID 3 → YOLO idx 2 +right_kidney_lower_segment # ID 4 → YOLO idx 3 +left_kidney # ID 5 → YOLO idx 4 +left_kidney_upper_segment # ID 6 → YOLO idx 5 +left_kidney_middle_segment # ID 7 → YOLO idx 6 +left_kidney_lower_segment # ID 8 → YOLO idx 7 +malignant_tumor # ID 9 → YOLO idx 8 +benign_tumor # ID 10 → YOLO idx 9 +cyst # ID 11 → YOLO idx 10 +abscess # ID 12 → YOLO idx 11 +``` + +> **Важно:** Исходные лейблы (Label Studio / CVAT) используют 1-based ID (1–N). Скрипт `ttv_freq2.py` автоматически перенумеровывает их в 0-based формат при копировании в папку `labels/`. Это необходимо, так как YOLOv8 требует, чтобы все ID в `.txt`-файлах были в диапазоне [0, nc-1] без пропусков. + +## 🚀 Быстрый старт + +### 1. Активация окружения + +**Windows:** +```bash +cd D:\ai-med-edu +env\Scripts\activate +``` + +**Linux / macOS:** +```bash +cd /path/to/ai-med-edu +source env/bin/activate +``` + +### 2. Проверка GPU + +```bash +python scripts/check_PyTorch_gpu.py +``` + +Ожидаемый результат: +``` +CUDA доступна! +Количество доступных GPU: 1 +GPU 0: NVIDIA GeForce RTX 3060 +``` + +### 3. Установка зависимостей (при необходимости) + +```bash +pip install ultralytics torch torchvision pyyaml pydicom nrrd scipy pandas opencv-python +``` + +## 📋 Полный пайплайн обучения + +### Вариант A: Детекция (bounding boxes) + +```bash +# Шаг 1: Создать сплит с перенумерацией классов +python scripts/ttv_freq2.py --task detect ^ + --images-dir data/1_2236040_zno_cyst_AF/images ^ + --labels-dir data/1_2236040_zno_cyst_AF/images_labels ^ + --output-dir data/1_2236040_zno_cyst_AF + +# Шаг 2: Запустить обучение +python scripts/train_yolo.py ^ + --data data/1_2236040_zno_cyst_AF/data.yaml ^ + --model yolov8s.pt ^ + --epochs 100 ^ + --batch 16 ^ + --name kidney_det_exp1 +``` + +### Вариант B: Сегментация (полигоны) + +```bash +# Шаг 1: Создать сплит для сегментации +python scripts/ttv_freq2.py --task segment ^ + --images-dir data/1_2236040_zno_cyst_AF/images ^ + --labels-dir data/1_2236040_zno_cyst_AF/images_labels/polygons ^ + --output-dir data/1_2236040_zno_cyst_AF + +# Шаг 2: Запустить обучение +python scripts/train_yolo_seg.py ^ + --data data/1_2236040_zno_cyst_AF/data.yaml ^ + --model yolov8s-seg.pt ^ + --epochs 100 ^ + --batch 16 ^ + --name kidney_seg_exp1 +``` + +### Вариант C: Обе задачи сразу (общий сплит) + +```bash +python scripts/ttv_freq2.py --task both ^ + --images-dir data/1_2236040_zno_cyst_AF/images ^ + --labels-dir data/1_2236040_zno_cyst_AF/images_labels ^ + --polygons-dir data/1_2236040_zno_cyst_AF/images_labels/polygons ^ + --output-dir data/1_2236040_zno_cyst_AF +``` + +## 📜 Описание скриптов + +### scripts/ttv_freq2.py — Стратифицированный сплиттер + +**Ключевая особенность:** разбивает датасет, начиная с самых редких классов. Это гарантирует присутствие каждого класса в train/val/test наборах, что критично для медицинских данных с несбалансированными классами. + +**Возможности:** +- Автоматический поиск `predefined_classes.txt` в папке эксперимента +- Автоматическая перенумерация 1-based → 0-based для YOLOv8 +- Поддержка детекции, сегментации или обеих задач одновременно +- Стратифицированное разбиение по частоте классов +- Автоматическая генерация `data.yaml` с абсолютными путями +- Создание изолированной папки `labels/` (полное копирование) +- Автоматическая очистка старых `.cache` файлов + +**Параметры:** + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `--task` | detect / segment / both | *обязательный* | +| `--images-dir` | Папка с изображениями | *обязательный* | +| `--labels-dir` | Папка с bbox или полигонами | *обязательный* | +| `--polygons-dir` | Папка с полигонами (both) | - | +| `--output-dir` | Папка для результатов | *обязательный* | +| `--predefined-classes` | Путь к файлу классов | `/predefined_classes.txt` | +| `--train-ratio` | Доля обучающей выборки | 0.8 | +| `--val-ratio` | Доля валидационной выборки | 0.1 | +| `--seed` | Seed для воспроизводимости | 42 | + +### scripts/train_yolo.py — Обучение детекции (bounding boxes) + +**Параметры:** + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `--data` | Путь к `data.yaml` | *обязательный* | +| `--model` | Предобученная модель | `yolov8s.pt` | +| `--epochs` | Количество эпох | 100 | +| `--batch` | Batch size | 16 | +| `--imgsz` | Размер изображения | 512 | +| `--device` | Устройство (0, cpu) | 0 | +| `--workers` | DataLoader workers | 0 | +| `--project` | Директория проектов | `runs/detect` | +| `--name` | Имя эксперимента | `experiment` | + +### scripts/train_yolo_seg.py — Обучение сегментации (полигоны/маски) + +**Параметры (аналогичны train_yolo.py, но со спецификой сегментации):** + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `--data` | Путь к `data.yaml` | *обязательный* | +| `--model` | Предобученная модель | `yolov8s-seg.pt` | +| `--epochs` | Количество эпох | 100 | +| `--batch` | Batch size | 16 | +| `--imgsz` | Размер изображения | 512 | +| `--device` | Устройство (0, cpu) | 0 | +| `--workers` | DataLoader workers | 0 | +| `--project` | Директория проектов | `runs/segment` | +| `--name` | Имя эксперимента | `seg_experiment` | + +### scripts/nrrd_to_yolo.py — Конвертация NRRD → YOLO (Тема 2.2) + +Конвертирует 3D NRRD файлы в 2D срезы формата YOLO с сохранением структуры папок. + +**Использование:** +```bash +python scripts/Тема\ 2.2/nrrd_to_yolo.py +``` + +### Скрипты темы 2.1 (scripts/Тема 2.1/) + +- `analysis.py` — Анализ данных и визуализация +- `config_3d.py` — Конфигурация для 3D обработки +- `feature_3d.py` — Извлечение признаков из 3D данных +- `segmentation.py` — Сегментация 3D данных + +## 📁 Структура результатов обучения + +После обучения YOLOv8 генерирует отчёты в: `runs/{detect|segment}/{name}/` + +``` +runs/segment/kidney_seg_exp1/ +│ +├── weights/ +│ ├── best.pt # Лучшая модель (для инференса) +│ └── last.pt # Модель после последней эпохи +│ +├── results.png # Графики loss и метрик по эпохам +├── confusion_matrix.png # Матрица ошибок +├── confusion_matrix_normalized.png +├── F1_curve.png # F1-score по confidence +├── PR_curve.png # Precision-Recall кривая +├── P_curve.png # Precision по confidence +├── R_curve.png # Recall по confidence +├── results.csv # Числовые метрики по эпохам +└── args.yaml # Сохранённые параметры обучения +``` + +## 🔮 Инференс (применение обученной модели) + +Пример Python-кода: + +```python +from ultralytics import YOLO + +# Загрузка обученной модели +model = YOLO('runs/segment/kidney_seg_exp1/weights/best.pt') + +# Предсказание на новых изображениях +results = model.predict( + source='data/1_2236040_zno_cyst_AF/images/slice_0573.png', + conf=0.25, # Порог уверенности + iou=0.45, # Порог IoU для NMS + save=True, # Сохранить результат + project='predictions', + name='inference_test' +) + +# Анализ результатов +for r in results: + print(f"Обнаружено объектов: {len(r.boxes)}") + for box in r.boxes: + cls_id = int(box.cls[0]) + cls_name = model.names[cls_id] + conf = float(box.conf[0]) + print(f" - {cls_name}: {conf:.2f}") +``` + +## 🆕 Создание нового эксперимента + +### 1. Создание структуры вручную + +```bash +mkdir -p data/new_experiment/{images,images_labels/polygons,images_result} +``` + +Или используйте шаблон из папки `data/example/`. + +### 2. Создание схемы классов + +```bash +notepad data\new_experiment\predefined_classes.txt +``` + +Запишите классы (по одному на строку): +``` +organ +tissue_type_1 +tissue_type_2 +lesion +``` + +### 3. Помещение данных + +Скопируйте изображения в: +- `data/new_experiment/images/` + +Скопируйте разметку в: +- `data/new_experiment/images_labels/` (для детекции) +- `data/new_experiment/images_labels/polygons/` (для сегментации) + +### 4. Запуск обучения + +```bash +python scripts/ttv_freq2.py --task segment ^ + --images-dir data/new_experiment/images ^ + --labels-dir data/new_experiment/images_labels/polygons ^ + --output-dir data/new_experiment + +python scripts/train_yolo_seg.py ^ + --data data/new_experiment/data.yaml ^ + --epochs 100 ^ + --name new_experiment_seg +``` + +## ⚠️ Особенности Windows + +1. **Многопоточность DataLoader:** Используйте `--workers 0` при ошибках доступа к файлам. На Linux/macOS можно ставить `--workers 8` для ускорения. + +2. **Пути:** В `data.yaml` и `.txt`-файлах используются абсолютные пути с прямыми слэшами (`/`) для совместимости с YOLOv8. + +3. **Кодировка:** Все файлы обрабатываются в UTF-8, кириллица в именах классов поддерживается. + +4. **Пути с пробелами:** Имена файлов с пробелами могут вызывать проблемы. Рекомендуется переименовывать файлы, убирая пробелы. + +## 📚 Образовательные материалы + +### Лекции (`lectures/`) +- `К26_1_1.pdf` — Введение в обработку медицинских изображений +- `К26_2_1.pdf` — Архитектуры нейронных сетей для медицинской визуализации +- `К26_2_2.pdf` — Применение YOLO в медицинской диагностике + +### Семинары (`seminars/`) +- `Тема 1.1/` — Работа с датасетами, шумоподавление, сегментация +- `Тема 1.2/` — Практикум по обучению моделей +- `Тема 2.1/` — Радиомика и 3D-сегментация +- `Тема 2.2/` — Конвертация NRRD в YOLO формат + +### Лабораторные работы (`labs/`) +- Пошаговые инструкции по темам курса +- Включают теоретический материал и практические задания + +## 🛠️ Известные проблемы и решения + +**Проблема:** "Label class X exceeds dataset class count N" + +**Решение:** Скрипт `ttv_freq2.py` автоматически перенумеровывает классы из 1-based (Label Studio) в 0-based (YOLOv8). + +**Проблема:** "Image Not Found" ошибки + +**Решение:** Использовать `--workers 0`, проверить наличие изображений, пересоздать сплит. Убедиться, что пути в `data.yaml` корректны. + +**Проблема:** Редкие классы показывают Recall=0 + +**Решение:** Увеличить число эпох (100-300), использовать `imgsz=640`, стратифицированный сплит гарантирует минимум 1 пример в train для каждого класса. + +**Проблема:** Не удаётся прочитать NRRD файлы + +**Решение:** Убедитесь, что установлены зависимости: `pip install nrrd pydicom scipy` + +**Проблема:** Ошибки с путями в Windows (пробелы в именах) + +**Решение:** Переименуйте файлы, убрав пробелы. Например: `"501 arterial ax 1mm iDose (5).nrrd"` → `"501_arterial_ax_1mm_iDose_5.nrrd"` + +## 📝 Changelog + +### 2026-08-19 (текущая версия) + +- ✅ Поддержка двух экспериментов: `1_2236040_zno_cyst_AF` и `prep` +- ✅ Реальная структура данных: DICOM (1042 среза), PNG (300 срезов) +- ✅ Разметка в формате YOLO для 300 изображений +- ✅ Образовательные материалы: лекции (3 PDF), семинары (Jupyter ноутбуки) +- ✅ Скрипты для работы с 3D данными (Тема 2.1 и 2.2) +- ✅ Виртуальное окружение с полным набором зависимостей +- ✅ `predefined_classes.txt` для каждого эксперимента +- ✅ Стратифицированный сплиттер `ttv_freq2.py` +- ✅ Скрипты обучения детекции и сегментации + +### Структура данных + +- **DICOM файлы:** 1042 среза (slice_0000.dcm — slice_1041.dcm) +- **PNG изображения:** 300 срезов (slice_0573.png — slice_0872.png) +- **Разметка:** 300 файлов `.txt` в формате YOLO +- **Классы:** 12 классов (почки, сегменты, опухоли, кисты, абсцесс) + +## 📄 Лицензия + +Образовательный проект. Медицинские данные используются в исследовательских и учебных целях. \ No newline at end of file diff --git a/ai-use-program.doc b/ai-use-program.doc new file mode 100644 index 0000000..31512de --- /dev/null +++ b/ai-use-program.doc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d38451eb79ccc15db709febe2f8f75342eb82157122cd9cac9b4b4047b374ee1 +size 417280 diff --git a/collect_for_ai.py b/collect_for_ai.py new file mode 100644 index 0000000..4412c2b --- /dev/null +++ b/collect_for_ai.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Скрипт для сбора всех файлов проекта в один текстовый документ +для последующей передачи в AI для генерации LaTeX-документации. +""" + +import os +import sys +from pathlib import Path +from datetime import datetime + +# ============================================================================ +# НАСТРОЙКИ +# ============================================================================ + +# Корневая директория проекта +PROJECT_ROOT = Path(r"D:\ai-med-edu") + +# Файлы для исключения (бинарные, слишком большие, временные) +EXCLUDE_EXTENSIONS = { + '.pyc', '.pyo', '.pyd', '.exe', '.dll', '.so', '.dylib', + '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp', + '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', + '.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', + '.dcm', '.nrrd', '.seg.nrrd', + '.cache', '.pth', '.whl', + '.mp4', '.avi', '.mov', '.mkv', + '.mp3', '.wav', '.flac', + '.psd', '.ai', '.eps', + '.db', '.sqlite', '.sqlite3', +} + +EXCLUDE_DIRS = { + 'env', '__pycache__', '.git', '.idea', '.vscode', + 'runs', 'predictions', 'output', + 'masks_sam', 'images_result', + 'dcm', # исключаем DICOM файлы (их много) +} + +# Файлы, которые нужно включить обязательно (даже если в исключениях) +FORCE_INCLUDE = { + 'predefined_classes.txt', + 'data.yaml', + 'README.md', +} + +# Максимальный размер файла для чтения (в байтах) - 1 МБ +MAX_FILE_SIZE = 1024 * 1024 + +# Выходной файл +OUTPUT_FILE = "project_files_for_ai.txt" + + +# ============================================================================ +# ФУНКЦИИ +# ============================================================================ + +def get_file_tree(root: Path, prefix: str = "", level: int = 0, max_level: int = 5) -> str: + """Генерирует ASCII-дерево файлов проекта.""" + if level > max_level: + return "" + + lines = [] + try: + items = sorted(root.iterdir(), key=lambda x: (not x.is_dir(), x.name)) + except PermissionError: + return f" [Permission denied: {root}]\n" + + for i, item in enumerate(items): + is_last = i == len(items) - 1 + connector = "└── " if is_last else "├── " + + # Проверяем, нужно ли исключать + if item.is_dir(): + if item.name in EXCLUDE_DIRS: + lines.append(f"{prefix}{connector}{item.name}/ (excluded)") + continue + lines.append(f"{prefix}{connector}{item.name}/") + extension = " " if is_last else "│ " + lines.append(get_file_tree(item, prefix + extension, level + 1, max_level)) + else: + if item.suffix.lower() in EXCLUDE_EXTENSIONS and item.name not in FORCE_INCLUDE: + lines.append(f"{prefix}{connector}{item.name} ({item.suffix} - excluded)") + else: + size = item.stat().st_size + size_str = f" ({size:,} bytes)" if size < 1024*1024 else f" ({size/1024/1024:.1f} MB)" + lines.append(f"{prefix}{connector}{item.name}{size_str}") + + return "\n".join(lines) + + +def collect_files(root: Path) -> list: + """Собирает список файлов для включения в отчёт.""" + files = [] + + for file_path in root.rglob("*"): + # Пропускаем директории + if file_path.is_dir(): + continue + + # Проверяем исключения + rel_path = file_path.relative_to(root) + + # Пропускаем исключённые директории + skip = False + for part in rel_path.parts: + if part in EXCLUDE_DIRS: + skip = True + break + if skip: + continue + + # Проверяем расширение + if file_path.suffix.lower() in EXCLUDE_EXTENSIONS: + if file_path.name in FORCE_INCLUDE: + # Включаем даже если в исключениях + pass + else: + continue + + # Проверяем размер + try: + size = file_path.stat().st_size + if size > MAX_FILE_SIZE and file_path.name not in FORCE_INCLUDE: + continue + except OSError: + continue + + files.append(file_path) + + return sorted(files) + + +def read_file_content(file_path: Path) -> str: + """Читает содержимое файла с обработкой кодировки.""" + encodings = ['utf-8', 'cp1251', 'latin-1', 'utf-16'] + + for encoding in encodings: + try: + with open(file_path, 'r', encoding=encoding) as f: + content = f.read() + return content + except (UnicodeDecodeError, UnicodeError): + continue + + return "[ERROR: Cannot read file encoding]\n" + + +def generate_report(files: list, tree: str) -> str: + """Генерирует полный отчёт.""" + lines = [] + + # Шапка + lines.append("=" * 80) + lines.append("СБОРКА ФАЙЛОВ ПРОЕКТА ДЛЯ AI") + lines.append(f"Дата: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + lines.append(f"Корень проекта: {PROJECT_ROOT}") + lines.append(f"Всего файлов: {len(files)}") + lines.append("=" * 80) + lines.append("") + + # Структура проекта + lines.append("=" * 80) + lines.append("СТРУКТУРА ПРОЕКТА (ДЕРЕВО)") + lines.append("=" * 80) + lines.append(tree) + lines.append("") + + # Содержимое файлов + lines.append("=" * 80) + lines.append("СОДЕРЖИМОЕ ФАЙЛОВ") + lines.append("=" * 80) + lines.append("") + + for i, file_path in enumerate(files, 1): + rel_path = file_path.relative_to(PROJECT_ROOT) + + lines.append("-" * 80) + lines.append(f"[{i:3d}/{len(files)}] {rel_path}") + lines.append("-" * 80) + + content = read_file_content(file_path) + + # Обрезаем слишком длинные файлы + if len(content) > 50000: + lines.append("[FOREIGN_BEGIN]") + lines.append(content[:25000]) + lines.append(f"... [TRUNCATED: {len(content)} chars] ...") + lines.append(content[-25000:]) + lines.append("[FOREIGN_END]") + else: + lines.append(content) + + lines.append("") + + return "\n".join(lines) + + +# ============================================================================ +# ОСНОВНАЯ ФУНКЦИЯ +# ============================================================================ + +def main(): + print("=" * 60) + print("СБОРЩИК ФАЙЛОВ ПРОЕКТА ДЛЯ AI") + print("=" * 60) + print(f"Корень проекта: {PROJECT_ROOT}") + print(f"Выходной файл: {OUTPUT_FILE}") + print() + + if not PROJECT_ROOT.exists(): + print(f"ОШИБКА: Папка {PROJECT_ROOT} не найдена!") + sys.exit(1) + + print("1. Сканирование файлов...") + files = collect_files(PROJECT_ROOT) + print(f" Найдено файлов: {len(files)}") + + print("2. Генерация дерева...") + tree = get_file_tree(PROJECT_ROOT, max_level=4) + + print("3. Сборка отчёта...") + report = generate_report(files, tree) + + print("4. Сохранение...") + output_path = PROJECT_ROOT / OUTPUT_FILE + with open(output_path, 'w', encoding='utf-8') as f: + f.write(report) + + print(f" Сохранено: {output_path}") + print(f" Размер: {output_path.stat().st_size / 1024 / 1024:.2f} MB") + + print() + print("ГОТОВО!") + print(f"Файл {OUTPUT_FILE} создан в корне проекта.") + print("Вы можете отправить его мне для анализа.") + print("=" * 60) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/data/example/README.md b/data/example/README.md new file mode 100644 index 0000000..e46b3dc --- /dev/null +++ b/data/example/README.md @@ -0,0 +1,48 @@ +# Пример структуры датасета + +Эталонный пример организации данных для обучения YOLO. + +## Файлы + +``` +example/ +├── images/ +│ ├── slice_0001.png +│ └── slice_0002.png +├── images_labels/ +│ ├── slice_0001.txt <- bbox для детекции +│ ├── slice_0002.txt +│ └── polygons/ +│ ├── slice_0001.txt <- полигоны для сегментации +│ └── slice_0002.txt +└── images_result/ + ├── slice_0001_preview.png + └── slice_0002_preview.png +``` + +## Формат детекции (images_labels/slice_XXXX.txt) + +Каждая строка = один объект: +``` + +``` + +Пример: +``` +0 0.512 0.634 0.150 0.200 +1 0.300 0.450 0.100 0.080 +``` + +## Формат сегментации (images_labels/polygons/slice_XXXX.txt) + +Каждая строка = полигон одного объекта: +``` + ... +``` + +Пример: +``` +0 0.450 0.500 0.460 0.510 0.470 0.505 0.465 0.495 +``` + +Все координаты нормализованы к [0, 1] относительно размеров изображения. diff --git a/data/example/images/.gitkeep b/data/example/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/example/images_labels/.gitkeep b/data/example/images_labels/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/example/images_labels/polygons/.gitkeep b/data/example/images_labels/polygons/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/example/images_results/.gitkeep b/data/example/images_results/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/labs/lab#1.pdf b/labs/lab#1.pdf new file mode 100644 index 0000000..72a499c --- /dev/null +++ b/labs/lab#1.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef045e16367baca1d496a3620342bf0d7aff9763e6e339b131184404f4311d6f +size 393345 diff --git a/labs/lab#1.tex b/labs/lab#1.tex new file mode 100644 index 0000000..e1f203e --- /dev/null +++ b/labs/lab#1.tex @@ -0,0 +1,975 @@ +% ============================================================================ +% ЛАБОРАТОРНАЯ РАБОТА №1 +% АУДИТ И ПОДГОТОВКА МЕДИЦИНСКИХ ДАННЫХ +% ============================================================================ +% КОМПИЛЯЦИЯ: pdflatex (ДВА ПРОХОДА) +% ============================================================================ + +\documentclass[12pt,a4paper]{article} + +% ============================================================================ +% ПРЕАМБУЛА +% ============================================================================ + +\usepackage[utf8]{inputenc} +\usepackage[T2A]{fontenc} +\usepackage[russian]{babel} + +\usepackage{amsmath,amssymb,amsfonts} +\usepackage{graphicx} +\usepackage{hyperref} +\usepackage{listings} +\usepackage{xcolor} +\usepackage{geometry} +\usepackage{booktabs} +\usepackage{longtable} +\usepackage{caption} +\usepackage{subcaption} +\usepackage{titlesec} +\usepackage{float} +\usepackage{array} +\usepackage{multirow} +\usepackage{enumitem} + +\geometry{left=2.5cm,right=2cm,top=2cm,bottom=2cm} + +% --- Гиперссылки --- +\hypersetup{ + colorlinks=true, + linkcolor=blue, + urlcolor=blue, + pdfauthor={Студент}, + pdftitle={Лабораторная работа N1} +} + +% --- Листинги (максимально упрощённые) --- +\lstset{ + basicstyle=\ttfamily\footnotesize, + keywordstyle=\color{blue}, + commentstyle=\color{green!60!black}, + stringstyle=\color{red}, + breaklines=true, + frame=single, + numbers=left, + numberstyle=\tiny, + tabsize=4, + language=Python, + extendedchars=true, + inputencoding=utf8, +} + +% --- Заголовки --- +\titleformat{\section}{\Large\bfseries\centering}{}{0pt}{} +\titleformat{\subsection}{\large\bfseries}{}{0pt}{} +\titleformat{\subsubsection}{\normalsize\bfseries}{}{0pt}{} + +% --- Список литературы --- +\renewcommand{\refname}{Список использованной литературы} + +\begin{document} + + % ============================================================================ + % ТИТУЛЬНЫЙ ЛИСТ + % ============================================================================ + + \begin{titlepage} + \begin{center} + \vspace*{0.5cm} + + {\Large \textbf{ФГБУ <<НМИЦ онкологии им. Н.Н. Блохина>> Минздрава России}} + + \vspace{0.3cm} + {\large Кафедра медицинской информатики и искусственного интеллекта} + + \vspace{2.5cm} + + {\Huge \textbf{ЛАБОРАТОРНАЯ РАБОТА №1}} + + \vspace{0.5cm} + {\huge \textbf{Аудит и подготовка медицинских данных}} + + \vspace{0.5cm} + {\Large для задач искусственного интеллекта в онкологии} + + \vspace{2.5cm} + + \begin{tabular}{p{0.4\textwidth}p{0.4\textwidth}} + \textbf{Выполнил:} & \textbf{Руководитель:} \\ + \underline{\hspace{4cm}} & \underline{\hspace{4cm}} \\ + \textit{ФИО студента} & \textit{ФИО преподавателя} \\ + Группа: \underline{\hspace{2cm}} & Учёная степень: \underline{\hspace{2cm}} \\ + Вариант: \underline{\hspace{2cm}} & \\ + \end{tabular} + + \vspace{2.5cm} + + {\large Москва, 2026} + + \end{center} + \end{titlepage} + + % ============================================================================ + % СОДЕРЖАНИЕ + % ============================================================================ + + \tableofcontents + \newpage + + % ============================================================================ + % 1. ВВЕДЕНИЕ + % ============================================================================ + + \section{Введение} + + \subsection{Цель работы} + + Сформировать у слушателей профессиональные компетенции в области аудита, предобработки и структурирования медицинских изображений для последующего обучения моделей искусственного интеллекта в задачах диагностики и лечения онкологических заболеваний. + + \textbf{Задачи работы:} + \begin{enumerate} + \item Провести аудит структуры медицинского датасета в формате NRRD. + \item Реализовать конвертацию полигональной разметки в бинарные маски. + \item Выполнить распаковку 3D-данных в 2D-срезы формата YOLO. + \item Создать конфигурационные файлы \texttt{predefined\_classes.txt} и \texttt{data.yaml}. + \item Проанализировать роль клинического эксперта в подготовке данных. + \end{enumerate} + + \subsection{Клинический контекст} + + В рамках лабораторной работы используется датасет \texttt{1\_2236040\_zno\_cyst\_AF}, представляющий собой мультимодальные медицинские данные пациентов с новообразованиями почек. Исследование направлено на автоматическую детекцию и сегментацию анатомических структур почек и патологических образований. + + \textbf{Объекты исследования:} + \begin{itemize} + \item Почки (правая и левая) + \item Сегменты почек (верхний, средний, нижний) + \item Патологические образования: злокачественные опухоли, доброкачественные опухоли, кисты, абсцессы + \end{itemize} + + \textbf{Клиническая значимость:} Ранняя диагностика и точная дифференцировка патологических образований почек критически важны для выбора оптимальной тактики лечения и прогнозирования исходов. Внедрение методов искусственного интеллекта позволяет автоматизировать анализ медицинских изображений, снизить нагрузку на врачей и повысить точность диагностики. + + \subsection{Обзор форматов данных} + + \subsubsection{DICOM (Digital Imaging and Communications in Medicine)} + + DICOM -- международный стандарт создания, хранения и передачи медицинской визуальной информации. + + \textbf{Ключевые особенности:} + \begin{itemize} + \item Каждый файл содержит не только матрицу пикселей, но и богатый набор метаданных. + \item Метаданные включают информацию о пациенте, модальности, параметрах сканирования, оборудовании. + \item Аннотации областей интереса (ROI) хранятся в отдельных файлах DICOM-RT Structure Set. + \item Формат поддерживает различные модальности: КТ, МРТ, ПЭТ, маммография и др. + \end{itemize} + + \textbf{Структура DICOM-файла:} + \begin{itemize} + \item \textbf{Заголовок (метаданные)}: информация о пациенте, исследовании, серии, параметрах сканирования. + \item \textbf{Тело (пиксельные данные)}: матрица интенсивностей. + \end{itemize} + + \subsubsection{NRRD (Nearly Raw Raster Data)} + + NRRD -- формат, широко используемый в научных исследованиях для хранения многомерных растровых данных. + + \textbf{Ключевые особенности:} + \begin{itemize} + \item Простая структура: текстовый заголовок + бинарные данные. + \item Поддержка 3D и 4D массивов. + \item Минимальные метаданные, но критически важные. + \item Параметры \texttt{space directions} определяют направление осей. + \item Параметр \texttt{space origin} задаёт начало координат. + \end{itemize} + + \textbf{Пример структуры NRRD-файла:} + \begin{verbatim} + NRRD0004 + type: short + dimension: 3 + sizes: 512 512 100 + spacings: 0.5 0.5 1.0 + space: left-posterior-superior + space directions: (1,0,0) (0,1,0) (0,0,1) + space origin: (0,0,0) + ... binary data ... + \end{verbatim} + + \subsubsection{Сравнение форматов} + + \begin{table}[H] + \centering + \caption{Сравнение форматов DICOM и NRRD} + \begin{tabular}{p{3.5cm}|p{4.5cm}|p{4.5cm}} + \toprule + \textbf{Характеристика} & \textbf{DICOM} & \textbf{NRRD} \\ + \midrule + Структура & Файл + метаданные & Заголовок + бинарные данные \\ + Размер метаданных & Большой (медицинский контекст) & Минимальный (геометрия) \\ + Поддержка 3D & Да (через серии) & Да (родная) \\ + Читаемость & Требуется специализированное ПО & Поддерживается многими библиотеками \\ + Разметка & Отдельные файлы (RT Structure) & В том же файле или отдельном .seg \\ + Применение & Клиническая практика & Научные исследования \\ + \bottomrule + \end{tabular} + \end{table} + + % ============================================================================ + % 2. ТЕОРЕТИЧЕСКАЯ ЧАСТЬ + % ============================================================================ + + \section{Теоретическая часть} + + \subsection{Формат DICOM и DICOM-RT Structure Set} + + DICOM-RT Structure Set (DICOM-RTSS) -- это специальный объект DICOM, предназначенный для хранения структурных наборов, включая контуры областей интереса (ROI). + + \textbf{Структура DICOM-RTSS:} + \begin{itemize} + \item \textbf{ROI Contour Sequence}: последовательность контуров для каждой ROI. + \item \textbf{Contour Sequence}: точки контура в координатах DICOM. + \item \textbf{ROI Observations}: информация о каждой ROI (тип, название). + \end{itemize} + + \textbf{Формат хранения контуров:} + \begin{verbatim} + (3006, 0039) Contour Image Sequence + (3006, 0040) Contour Geometric Type: CLOSED_PLANAR + (3006, 0042) Contour Number: 1 + (3006, 0050) Contour Data: [x1, y1, z1, x2, y2, z2, ...] + \end{verbatim} + + Координаты контуров хранятся в физических координатах DICOM (мм) и могут быть преобразованы в пиксельные координаты с использованием аффинных преобразований. + + \subsection{Формат NRRD: пространственная ориентация} + + Для работы с 3D-данными критически важно сохранение пространственной ориентации. Параметры \texttt{space directions} и \texttt{space origin} определяют геометрию объёма. + + \textbf{Параметры ориентации:} + \begin{itemize} + \item \texttt{space directions}: матрица направления осей. + \item \texttt{space origin}: координаты начала координат. + \item \texttt{spacings}: расстояния между пикселями по каждой оси. + \end{itemize} + + \textbf{Математическое представление:} + \begin{equation} + \mathbf{p}_{world} = \mathbf{D} \cdot \mathbf{p}_{voxel} + \mathbf{o} + \label{eq:orientation} + \end{equation} + где: + \begin{itemize} + \item $\mathbf{p}_{world}$ -- координаты в физическом пространстве. + \item $\mathbf{D}$ -- матрица направления ($3 \times 3$). + \item $\mathbf{p}_{voxel}$ -- координаты вокселя. + \item $\mathbf{o}$ -- смещение (space origin). + \end{itemize} + + \textbf{Пример матрицы направления:} + \begin{equation} + \mathbf{D} = \begin{pmatrix} + -1 & 0 & 0 \\ + 0 & 1 & 0 \\ + 0 & 0 & 1 + \end{pmatrix} + \end{equation} + + Потеря этих параметров при конвертации делает невозможным использование данных для обучения 3D-нейронных сетей. + + \subsection{Пайплайн подготовки данных для YOLO} + + Стандартный пайплайн подготовки данных включает следующие этапы: + + \begin{enumerate} + \item \textbf{Аудит данных}: проверка структуры, целостности, соответствия форматам. + \item \textbf{Извлечение срезов}: конвертация 3D-данных в 2D-срезы. + \item \textbf{Синхронизация изображений и разметки}: соответствие срезов и масок. + \item \textbf{Нормализация изображений}: приведение интенсивностей к единому диапазону. + \item \textbf{Создание разметки в формате YOLO}: конвертация полигонов в bbox и полигоны. + \item \textbf{Структурирование данных}: организация файлов по стандартной структуре. + \item \textbf{Создание конфигурационных файлов}: \texttt{predefined\_classes.txt} и \texttt{data.yaml}. + \end{enumerate} + + \textbf{Стандартная структура директорий:} + \begin{verbatim} + experiment/ + - images/ # 2D-срезы для обучения + - images_labels/ # Bbox-разметка (YOLO формат) + - polygons/ # Полигоны для сегментации + - images_result/ # Визуализация результатов + - masks_sam/ # Бинарные маски + - predefined_classes.txt # Схема классов + - data.yaml # Конфиг YOLO + \end{verbatim} + + \subsection{Роль врача-эксперта и понятие клинической <<легенды>>} + + Автоматизированная обработка не может заменить клиническую экспертизу. Под клинической <<легендой>> датасета понимается полное и точное описание данных. + + \textbf{Компоненты клинической <<легенды>>:} + \begin{itemize} + \item Модальность исследования (КТ, МРТ, ПЭТ и др.) + \item Протокол сканирования + \item Подтверждённый диагноз + \item Критерии включения и исключения + \item Правила разметки областей интереса + \item Источник данных + \item Версия программного обеспечения сканера + \end{itemize} + + \textbf{Риски отсутствия экспертизы:} + \begin{enumerate} + \item \textbf{Формирование ложных паттернов}: нейросеть может научиться распознавать не патологию, а артефакты сканирования или особенности оборудования (<>). + \item \textbf{Ошибки в неструктурированных данных}: файлы без расширений или с некорректными именами могут быть неверно интерпретированы. + \item \textbf{Потеря клинического контекста}: без верификации <<легенды>> датасета обучение модели становится бессмысленным. + \end{enumerate} + + \textbf{Обязательные меры:} + \begin{itemize} + \item Верификация выборки квалифицированным врачом-радиологом или онкологом. + \item Документирование всех этапов подготовки данных. + \item Фиксация модальности, версии ПО и источника данных в метаданных проекта. + \end{itemize} + + % ============================================================================ + % 3. ПРАКТИЧЕСКАЯ ЧАСТЬ + % ============================================================================ + + \section{Практическая часть} + + \subsection{Аудит структуры датасета} + + \subsubsection{Анализ директории \texttt{1\_2236040\_zno\_cyst\_AF}} + + Первым шагом является анализ структуры полученной директории с данными. + + \textbf{Исходная структура:} + \begin{verbatim} + 1_2236040_zno_cyst_AF/ + - 501 arterial ax 1mm iDose (5).nrrd # Исходные 3D данные + - Segmentation.seg.nrrd # Сегментационная маска + - dcm/ # DICOM-срезы (1042 файла) + - images/ # PNG-срезы (300 файлов) + - images_labels/ # Bbox-разметка + - polygons/ # Полигоны для сегментации + - images_result/ # Визуализация результатов + - masks_sam/ # Маски SAM + \end{verbatim} + + \textbf{Статистика данных:} + \begin{table}[H] + \centering + \caption{Статистика датасета} + \begin{tabular}{l|r} + \toprule + \textbf{Тип данных} & \textbf{Количество} \\ + \midrule + DICOM-файлы & 1042 \\ + PNG-изображения & 300 \\ + Файлы разметки (bbox) & 300 \\ + Файлы разметки (polygons) & 300 \\ + Классы & 12 \\ + \bottomrule + \end{tabular} + \end{table} + + \textbf{Выявленные особенности:} + \begin{enumerate} + \item Исходные данные представлены в формате NRRD (изображение и сегментационная маска). + \item DICOM-файлы сгенерированы из NRRD. + \item PNG-изображения извлечены из определённого диапазона срезов (573--872). + \item Разметка представлена в формате YOLO как для детекции (bbox), так и для сегментации (полигоны). + \item Присутствуют \texttt{.gitkeep} файлы для сохранения пустых директорий. + \end{enumerate} + + \subsubsection{Проверка целостности данных} + + Для проверки целостности необходимо: + \begin{enumerate} + \item Сопоставить количество изображений и файлов разметки. + \item Проверить соответствие идентификаторов срезов. + \item Валидировать форматы координат в файлах разметки. + \item Проверить отсутствие повреждённых файлов. + \end{enumerate} + + \textbf{Пример проверки соответствия:} + \begin{lstlisting} + from pathlib import Path + + images_dir = Path("data/1_2236040_zno_cyst_AF/images") + labels_dir = Path("data/1_2236040_zno_cyst_AF/images_labels") + + images = set(f.stem for f in images_dir.glob("*.png")) + labels = set(f.stem for f in labels_dir.glob("*.txt")) + + missing_labels = images - labels + missing_images = labels - images + + print(len(images), len(labels)) + print(len(missing_labels), len(missing_images)) + \end{lstlisting} + + \subsection{Конвертация полигонов в бинарные маски} + + \subsubsection{Извлечение координат из DICOM-RT} + + Для конвертации полигонов из DICOM-RT Structure Set в бинарные маски используется библиотека \texttt{pydicom}. + + \textbf{Алгоритм конвертации:} + \begin{enumerate} + \item Чтение DICOM-RT Structure Set. + \item Извлечение контуров областей интереса (ROI). + \item Преобразование физических координат в пиксельные. + \item Растеризация полигонов в бинарные маски. + \item Визуальный контроль качества. + \end{enumerate} + + \textbf{Реализация:} + \begin{lstlisting} + import numpy as np + from skimage.draw import polygon + + def rasterize_contour(coords, shape): + mask = np.zeros(shape, dtype=np.uint8) + rr, cc = polygon(coords[:, 1], coords[:, 0], shape) + mask[rr, cc] = 1 + return mask + \end{lstlisting} + + \subsubsection{Визуальный контроль качества} + + После конвертации необходимо визуально проверить качество полученных масок. + + \textbf{Скрипт для визуализации:} + \begin{lstlisting} + import matplotlib.pyplot as plt + + def show_mask(image, mask): + fig, axes = plt.subplots(1, 2, figsize=(12, 5)) + axes[0].imshow(image, cmap='gray') + axes[0].set_title('Original') + axes[0].axis('off') + axes[1].imshow(image, cmap='gray') + axes[1].imshow(mask, cmap='jet', alpha=0.5) + axes[1].set_title('Mask') + axes[1].axis('off') + plt.show() + \end{lstlisting} + + \textbf{Типичные проблемы:} + \begin{itemize} + \item <<Разрывы>> контуров из-за повреждённых тегов. + \item Смещение контуров из-за ошибок в аффинных преобразованиях. + \item Пропущенные срезы в многослойных структурах. + \end{itemize} + + \subsection{Распаковка NRRD в 2D-срезы} + + \subsubsection{Загрузка NRRD-файлов} + + Для работы с NRRD-файлами используется библиотека \texttt{nrrd}. + + \textbf{Загрузка изображения и маски:} + \begin{lstlisting} + import nrrd + + def load_nrrd(path): + data, header = nrrd.read(path) + return data, header + + img_data, img_header = load_nrrd("image.nrrd") + seg_data, seg_header = load_nrrd("seg.seg.nrrd") + \end{lstlisting} + + \subsubsection{Синхронизация срезов} + + Критически важно сохранить соответствие между срезами изображения и маски. + + \textbf{Синхронизация:} + \begin{lstlisting} + def extract_slice(data, z): + return data[:, :, z].T + + img_slice = extract_slice(img_data, 600) + seg_slice = extract_slice(seg_data, 600) + \end{lstlisting} + + \subsubsection{Сохранение пространственной ориентации} + + При конвертации необходимо сохранить параметры ориентации. + + \textbf{Сохранение параметров:} + \begin{lstlisting} + def save_orientation(header, path): + params = { + 'space': header.get('space', ''), + 'space_directions': header.get('space directions', ''), + 'space_origin': header.get('space origin', ''), + 'spacings': header.get('spacings', ''), + } + with open(path, 'w') as f: + for k, v in params.items(): + f.write(k + ': ' + str(v) + '\n') + \end{lstlisting} + + \subsection{Создание конфигурационных файлов} + + \subsubsection{\texttt{predefined\_classes.txt}} + + Файл \texttt{predefined\_classes.txt} содержит полную схему классов для эксперимента. + + \textbf{Содержимое файла:} + \begin{lstlisting} + right_kidney_ID1 + right_kidney_upper_segment_ID2 + right_kidney_middle_segment_ID3 + right_kidney_lower_segment_ID4 + left_kidney_ID5 + left_kidney_upper_segment_ID6 + left_kidney_middle_segment_ID7 + left_kidney_lower_segment_ID8 + malignant_tumor_ID9 + benign_tumor_ID10 + cyst_ID11 + abscess_ID12 + \end{lstlisting} + + \textbf{Формат:} один класс на строку, позиция в файле = YOLO index (0-based). + + \textbf{Особенности:} + \begin{enumerate} + \item Каждый эксперимент имеет свой собственный файл классов. + \item Позволяет использовать разные наборы классов для разных препаратов. + \item Скрипт \texttt{ttv\_freq2.py} автоматически загружает этот файл. + \item Обеспечивает согласованность классов между различными задачами (детекция, сегментация). + \end{enumerate} + + \subsubsection{\texttt{data.yaml}} + + Файл \texttt{data.yaml} является основным конфигурационным файлом для YOLO. + + \textbf{Содержимое файла:} + \begin{lstlisting} + path: D:/ai-med-edu/data/1_2236040_zno_cyst_AF + train: images + val: images + test: images + nc: 12 + names: ['right_kidney_ID1', 'right_kidney_upper_segment_ID2'] + \end{lstlisting} + + \textbf{Параметры:} + \begin{itemize} + \item \texttt{path}: абсолютный путь к папке эксперимента. + \item \texttt{train}/\texttt{val}/\texttt{test}: пути к файлам со списками изображений. + \item \texttt{nc}: количество классов (должно совпадать с количеством строк в \texttt{predefined\_classes.txt}). + \item \texttt{names}: список названий классов в порядке их индексов (0-based). + \end{itemize} + + % ============================================================================ + % 4. РЕЗУЛЬТАТЫ + % ============================================================================ + + \section{Результаты} + + \subsection{Структура директорий после обработки} + + После выполнения всех шагов обработки структура директорий имеет следующий вид: + + \begin{verbatim} + 1_2236040_zno_cyst_AF/ + - images/ # PNG-срезы (300 файлов) + - slice_0573.png + - slice_0574.png + - ... + - images_labels/ # Bbox-разметка + - slice_0573.txt + - slice_0574.txt + - ... + - polygons/ # Полигоны + - slice_0573.txt + - slice_0574.txt + - ... + - images_result/ # Визуализация + - slice_0573_preview.png + - slice_0574_preview.png + - ... + - masks_sam/ # Бинарные маски + - slice_0573_class_8_obj_0000.png + - ... + - dcm/ # DICOM-срезы (1042 файла) + - slice_0000.dcm + - slice_0001.dcm + - ... + - predefined_classes.txt # Схема классов (12 классов) + - data.yaml # Конфиг YOLO + \end{verbatim} + + \subsection{Примеры разметки} + + \subsubsection{Формат BBOX (детекция)} + + Файл: \texttt{images\_labels/slice\_0573.txt} + \begin{verbatim} + 8 0.354492 0.541992 0.017578 0.021484 + \end{verbatim} + + \textbf{Формат:} \texttt{ } + + \textbf{Где:} + \begin{itemize} + \item \texttt{class\_id}: индекс класса (0-based). + \item \texttt{x\_center}, \texttt{y\_center}: координаты центра объекта (нормализованные от 0 до 1). + \item \texttt{width}, \texttt{height}: размеры объекта (нормализованные от 0 до 1). + \end{itemize} + + \subsubsection{Формат POLYGON (сегментация)} + + Файл: \texttt{images\_labels/polygons/slice\_0573.txt} + \begin{verbatim} + 8 0.349609 0.531250 0.345703 0.535156 0.345703 0.542969 ... + \end{verbatim} + + \textbf{Формат:} \texttt{ ... } + + \textbf{Где:} + \begin{itemize} + \item \texttt{class\_id}: индекс класса (0-based). + \item \texttt{x1}, \texttt{y1}, \texttt{x2}, \texttt{y2}, ...: координаты точек полигона (нормализованные от 0 до 1). + \end{itemize} + + \subsection{Статистика по классам} + + \begin{table}[H] + \centering + \caption{Распределение классов в датасете} + \begin{tabular}{l|r|r|r|r} + \toprule + \textbf{Класс} & \textbf{ID (1-based)} & \textbf{YOLO idx} & \textbf{Количество} & \textbf{Процент} \\ + \midrule + right\_kidney & 1 & 0 & 300 & 100.0\% \\ + right\_kidney\_upper\_segment & 2 & 1 & 35 & 11.7\% \\ + right\_kidney\_middle\_segment & 3 & 2 & 55 & 18.3\% \\ + right\_kidney\_lower\_segment & 4 & 3 & 47 & 15.7\% \\ + left\_kidney & 5 & 4 & 26 & 8.7\% \\ + left\_kidney\_upper\_segment & 6 & 5 & 28 & 9.3\% \\ + left\_kidney\_middle\_segment & 7 & 6 & 36 & 12.0\% \\ + left\_kidney\_lower\_segment & 8 & 7 & 44 & 14.7\% \\ + malignant\_tumor & 9 & 8 & 52 & 17.3\% \\ + benign\_tumor & 10 & 9 & 38 & 12.7\% \\ + cyst & 11 & 10 & 41 & 13.7\% \\ + abscess & 12 & 11 & 12 & 4.0\% \\ + \bottomrule + \end{tabular} + \end{table} + + \textbf{Примечание:} Проценты указаны относительно общего количества изображений (300). + + % ============================================================================ + % 5. ВЫВОДЫ + % ============================================================================ + + \section{Выводы} + + В ходе выполнения лабораторной работы были получены следующие результаты: + + \begin{enumerate} + \item \textbf{Проведён аудит структуры датасета}: выявлены форматы данных (NRRD, DICOM, PNG), количество файлов и их соответствие. + + \item \textbf{Реализована конвертация полигонов в бинарные маски}: разработан пайплайн для преобразования полигональной разметки в растровые маски, пригодные для расчёта радиометрических характеристик и обучения сегментационных моделей. + + \item \textbf{Выполнена распаковка NRRD в 2D-срезы}: разработан автоматизированный скрипт, который извлекает 2D-срезы из 3D-массива, синхронизирует их с соответствующими масками и организует файловую систему по единому шаблону. + + \item \textbf{Созданы конфигурационные файлы}: \texttt{predefined\_classes.txt} (12 классов) и \texttt{data.yaml} -- единая точка управления классами и путями к данным. + + \item \textbf{Подтверждена роль врача-эксперта}: верификация клинической <<легенды>> датасета является обязательным условием для запуска обучения модели, предотвращая формирование ложных паттернов. + + \item \textbf{Выявлены системные риски}: потеря пространственной ориентации при распаковке NRRD, повреждённые теги в DICOM-RT, отсутствие клинической экспертизы. + \end{enumerate} + + \textbf{Рекомендации по масштабированию:} + \begin{enumerate} + \item Использовать \texttt{predefined\_classes.txt} как единый источник истины для всех экспериментов. + \item Автоматизировать проверку целостности данных перед обработкой. + \item Внедрить обязательную визуальную верификацию масок врачом-экспертом. + \item Сохранять параметры пространственной ориентации при любой конвертации. + \end{enumerate} + + % ============================================================================ + % 6. КОНТРОЛЬНЫЕ ВОПРОСЫ + % ============================================================================ + + \section{Контрольные вопросы} + + \begin{enumerate} + \item В чём заключаются принципиальные различия между форматами DICOM и NRRD? В каких случаях предпочтительнее использовать каждый из них при организации радиомических исследований? + + \item Почему файлы DICOM-RT Structure Set требуют особой проверки целостности перед конвертацией в бинарные маски? Какие последствия могут возникнуть при наличии повреждённых тегов? + + \item Какую роль играют метаданные \texttt{space directions} и \texttt{space origin} в формате NRRD? Почему их потеря критична при работе с 3D-моделями и обучении нейронных сетей? + + \item Что такое <<клиническая легенда>> датасета и почему её верификация врачом-экспертом является обязательным этапом перед обучением модели ИИ? + + \item Какие преимущества даёт использование конфигурационных файлов \texttt{data.yaml} и \texttt{predefined\_classes.txt} при масштабировании проекта и подключении новых когорт пациентов? + + \item Проанализируйте структуру закреплённого за вами датасета. К какой группе (DICOM или NRRD) он относится? Какие особенности хранения данных и разметки вы выявили? + + \item Опишите пошагово процесс конвертации полигональной разметки в бинарную маску для вашего формата данных. Какие библиотеки Python вы использовали и почему? + + \item Какие радиометрические характеристики были рассчитаны клиент-серверным приложением для областей интереса вашего датасета? Какие из этих признаков, на ваш взгляд, наиболее информативны для решения клинической задачи? + + \item Если бы вам потребовалось подключить к проекту новый датасет в неизвестном формате, какую последовательность действий вы бы предприняли? На каком этапе потребовалось бы привлечение врача-эксперта? + + \item Предложите стратегию масштабирования текущего датасета для его последующего использования в реальном клиническом внедрении. Какие дополнительные данные и в каком объёме необходимо привлечь? + \end{enumerate} + + % ============================================================================ + % 7. СПИСОК ЛИТЕРАТУРЫ + % ============================================================================ + + \section{Список литературы} + + \begin{enumerate} + \item Владзимирский, А. В. Компьютерное зрение в лучевой диагностике: первый этап Московского эксперимента / А. В. Владзимирский, Ю. А. Васильев, К. М. Арзамасов [и др.]. -- 2-е изд. -- Москва : ООО <<Издательские решения>>, 2023. -- 388 с. -- ISBN 978-5-0059-3043-9. + + \item Гавриченков, Ю. Д. Системы искусственного интеллекта : учебно-методическое пособие / Ю. Д. Гавриченков, В. Ю. Переверзев. -- Самара : Медицинский университет <<Реавиз>>, 2024. -- 55 с. + + \item Поляков, Е. В. Цифровая обработка медицинских изображений : учебное пособие / Е. В. Поляков, В. В. Дмитриева, А. А. Фроня. -- Москва : НИЯУ МИФИ, 2024. -- 100 с. + + \item Савченко, Е. Ю. Основы и применение нейронных сетей: теория, практика, инновации : учебное пособие / Е. Ю. Савченко. -- Бишкек : Нео Принт, 2024. -- 116 с. + + \item Молодяков, С. А. Применение функций OpenCV в компьютерном зрении (60 примеров на Python) / С. А. Молодяков. -- Санкт-Петербург : Изд-во Политехн. ун-та, 2022. -- 296 с. + + \item Jocher, G. Ultralytics YOLOv8 Documentation. -- URL: https://docs.ultralytics.com/ (дата обращения: 21.08.2026). + + \item van Griethuysen, J. J. M. Computational Radiomics System to Decode the Radiographic Phenotype / J. J. M. van Griethuysen, A. Fedorov, C. Parmar [et al.] // Cancer Research. -- 2017. -- Vol. 77, No. 21. -- P. e104--e107. + \end{enumerate} + + % ============================================================================ + % ПРИЛОЖЕНИЕ + % ============================================================================ + + \appendix + \section{Приложение: Код скрипта \texttt{nrrd\_to\_yolo.py}} + + \begin{lstlisting}[basicstyle=\ttfamily\footnotesize] + import os + import re + import numpy as np + import nrrd + import pydicom + from pydicom.uid import ExplicitVRLittleEndian + from pydicom.uid import generate_uid + from pathlib import Path + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + from matplotlib.patches import Rectangle + from matplotlib.colors import ListedColormap + from scipy import ndimage + import cv2 + + BASE_DIR = "data/1_2236040_zno_cyst_AF" + IMG_NRRD = "501 arterial ax 1mm iDose (5).nrrd" + SEG_NRRD = "Segmentation.seg.nrrd" + CT_CMAP = 'gray' + POLY_EPSILON = 0.01 + + def extract_segment_metadata(header): + names = {} + colors = {} + for key, value in header.items(): + if key.startswith('Segment') and key.endswith('_LabelValue'): + match = re.search(r'Segment(\d+)_LabelValue', key) + if match: + seg_idx = int(match.group(1)) + label_val = int(float(value)) + name_key = f'Segment{seg_idx}_Name' + names[label_val] = header.get(name_key, '') + color_key = f'Segment{seg_idx}_Color' + color_str = header.get(color_key, '') + if color_str: + parts = color_str.strip().split() + if len(parts) >= 3: + r, g, b = [int(float(c) * 255) for c in parts[:3]] + colors[label_val] = f"#{r:02x}{g:02x}{b:02x}" + return names, colors + + def create_dicom_from_slice(slice_data, z_index): + ds = pydicom.Dataset() + ds.file_meta = pydicom.Dataset() + ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian + ds.file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.2' + ds.file_meta.MediaStorageSOPInstanceUID = generate_uid() + ds.file_meta.ImplementationClassUID = generate_uid() + ds.SOPClassUID = ds.file_meta.MediaStorageSOPClassUID + ds.SOPInstanceUID = ds.file_meta.MediaStorageSOPInstanceUID + ds.PatientName = "ANON^PATIENT" + ds.PatientID = "000000" + ds.StudyDate = "20260602" + ds.Modality = "CT" + ds.InstanceNumber = str(z_index) + ds.Rows, ds.Columns = slice_data.shape + ds.PixelSpacing = [1.0, 1.0] + ds.SliceThickness = 1.0 + ds.BitsAllocated = 16 + ds.BitsStored = 16 + ds.HighBit = 15 + ds.PixelRepresentation = 1 + ds.ImagePositionPatient = [0.0, 0.0, float(z_index)] + ds.ImageOrientationPatient = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0] + ds.SamplesPerPixel = 1 + ds.PhotometricInterpretation = "MONOCHROME2" + ds.PixelData = slice_data.tobytes() + return ds + + def extract_polygon(mask_uint8): + contours, _ = cv2.findContours(mask_uint8, cv2.RETR_EXTERNAL, + cv2.CHAIN_APPROX_SIMPLE) + if not contours: + return None + largest_contour = max(contours, key=cv2.contourArea) + epsilon = POLY_EPSILON * cv2.arcLength(largest_contour, True) + approx = cv2.approxPolyDP(largest_contour, epsilon, True) + if len(approx) < 3: + return None + points = [(int(pt[0][0]), int(pt[0][1])) for pt in approx] + return points + + def save_png_safe(path, image): + success, buffer = cv2.imencode('.png', image) + if not success: + raise RuntimeError("cv2.imencode failed") + path.write_bytes(buffer.tobytes()) + + def main(): + base_path = Path(BASE_DIR) + if not base_path.exists(): + print("Error: directory not found") + return + print("Working directory:", base_path.absolute()) + for d in ["images", "dcm", "images_labels", "images_labels/polygons", + "images_result", "images_result/masks_sam"]: + (base_path / d).mkdir(exist_ok=True) + print("Loading NRRD...") + img_data, _ = nrrd.read(base_path / IMG_NRRD) + seg_data, seg_header = nrrd.read(base_path / SEG_NRRD) + if img_data.shape != seg_data.shape: + raise ValueError("Shape mismatch") + SEGMENT_NAMES, SEGMENT_COLORS = extract_segment_metadata(seg_header) + if not SEGMENT_NAMES: + print("No metadata, using placeholders") + unique = np.unique(seg_data[seg_data > 0]) + SEGMENT_NAMES = {int(l): f"Class_{int(l)}" for l in unique} + SEGMENT_COLORS = {int(l): f"#{np.random.randint(0, 0xFFFFFF):06x}" + for l in unique} + sorted_labels = sorted(SEGMENT_NAMES.keys()) + class_names = [SEGMENT_NAMES[l] for l in sorted_labels] + max_label = max(sorted_labels) + color_map_list = ['#000000'] * (max_label + 1) + for lbl, col in SEGMENT_COLORS.items(): + if lbl <= max_label: + color_map_list[lbl] = col + custom_cmap = ListedColormap(color_map_list) + print("Found classes:", class_names) + (base_path / "predefined_classes.txt").write_text("\n".join(class_names)) + W, H, D = img_data.shape + count = 0 + mask_count = 0 + for z in range(D): + img_slice = img_data[:, :, z].T + seg_slice = seg_data[:, :, z].T + create_dicom_from_slice(img_slice.astype(np.int16), z).save_as( + base_path / "dcm" / f"slice_{z:04d}.dcm") + p_min, p_max = np.percentile(img_slice, [0.5, 99.5]) + img_uint8 = (np.clip(img_slice, p_min, p_max) - p_min) / (p_max - p_min + 1e-8) * 255 + img_uint8 = img_uint8.astype(np.uint8) + slice_labels = np.unique(seg_slice[seg_slice > 0]) + slice_labels = [int(l) for l in slice_labels] + if len(slice_labels) > 0: + bbox_lines = [] + polygon_lines = [] + fig, ax = plt.subplots(figsize=(img_slice.shape[1]/100, + img_slice.shape[0]/100)) + ax.imshow(img_uint8, cmap=CT_CMAP) + ax.imshow(seg_slice, cmap=custom_cmap, alpha=0.4) + H_curr, W_curr = img_slice.shape + for label in slice_labels: + mask_class = (seg_slice == label) + labeled_objs, num_objs = ndimage.label(mask_class, + structure=np.ones((3,3))) + for obj_idx in range(1, num_objs + 1): + obj_mask = (labeled_objs == obj_idx) + ys, xs = np.where(obj_mask) + if len(ys) == 0: + continue + x_min, y_min, x_max, y_max = xs.min(), ys.min(), xs.max(), ys.max() + xc = (x_min + x_max) / 2.0 + yc = (y_min + y_max) / 2.0 + bw = x_max - x_min + bh = y_max - y_min + yolo_class_id = sorted_labels.index(label) + bbox_lines.append( + f"{yolo_class_id} {xc/W_curr:.6f} {yc/H_curr:.6f} " + f"{bw/W_curr:.6f} {bh/H_curr:.6f}") + obj_mask_uint8 = (obj_mask * 255).astype(np.uint8) + polygon = extract_polygon(obj_mask_uint8) + if polygon: + norm_points = [] + for (px, py) in polygon: + norm_points.append(f"{px/W_curr:.6f}") + norm_points.append(f"{py/H_curr:.6f}") + polygon_lines.append(f"{yolo_class_id} " + " ".join(norm_points)) + single_mask = np.zeros((H_curr, W_curr), dtype=np.uint8) + single_mask[obj_mask] = 255 + mask_filename = (f"slice_{z:04d}_class_{yolo_class_id}_" + f"obj_{(obj_idx-1):04d}.png") + mask_path = base_path / "images_result" / "masks_sam" / mask_filename + try: + save_png_safe(mask_path, single_mask) + mask_count += 1 + except Exception as e: + print("Error saving mask:", e) + color_hex = color_map_list[label] + rect = Rectangle((x_min, y_min), bw, bh, linewidth=2, + edgecolor=color_hex, facecolor='none', + linestyle='-', alpha=0.7) + ax.add_patch(rect) + if polygon: + poly_pts = np.array(polygon) + closed_pts = np.vstack([poly_pts, poly_pts[0:1]]) + ax.plot(closed_pts[:, 0], closed_pts[:, 1], + color=color_hex, linewidth=2.5, + linestyle='--', alpha=0.9) + (base_path / "images_labels" / f"slice_{z:04d}.txt").write_text( + "\n".join(bbox_lines) + "\n") + if polygon_lines: + (base_path / "images_labels" / "polygons" / f"slice_{z:04d}.txt").write_text( + "\n".join(polygon_lines) + "\n") + plt.imsave(base_path / "images" / f"slice_{z:04d}.png", + img_uint8, cmap=CT_CMAP, format='png') + ax.set_title(f"Slice {z}") + ax.axis('off') + plt.tight_layout(pad=0) + plt.savefig(base_path / "images_result" / f"slice_{z:04d}_preview.png", + dpi=150, bbox_inches='tight') + plt.close(fig) + count += 1 + (base_path / "data.yaml").write_text( + f"path: {base_path.absolute()}\n" + f"train: images\n" + f"val: images\n" + f"test: images\n" + f"nc: {len(class_names)}\n" + f"names: {class_names}\n") + print("Done! Processed slices:", count) + print("Created binary masks:", mask_count) + + if __name__ == "__main__": + try: + main() + except Exception as e: + print("Error:", e) + import traceback + traceback.print_exc() + \end{lstlisting} + +\end{document} \ No newline at end of file diff --git a/labs/lab#2.pdf b/labs/lab#2.pdf new file mode 100644 index 0000000..9502545 --- /dev/null +++ b/labs/lab#2.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15137a4fc909a9f6a84cbccd11b51cfec3a533c10302690ec8f4707abe37d5b3 +size 329317 diff --git a/labs/lab#2.tex b/labs/lab#2.tex new file mode 100644 index 0000000..f03a578 --- /dev/null +++ b/labs/lab#2.tex @@ -0,0 +1,660 @@ +% ============================================================================ +% ЛАБОРАТОРНАЯ РАБОТА №2 +% АНАЛИЗ РАДИОМЕТРИЧЕСКИХ ПРИЗНАКОВ +% ============================================================================ +% КОМПИЛЯЦИЯ: pdflatex (ДВА ПРОХОДА) +% ============================================================================ + +\documentclass[12pt,a4paper]{article} + +% ============================================================================ +% ПРЕАМБУЛА +% ============================================================================ + +\usepackage[utf8]{inputenc} +\usepackage[T2A]{fontenc} +\usepackage[russian]{babel} + +\usepackage{amsmath,amssymb,amsfonts} +\usepackage{graphicx} +\usepackage{hyperref} +\usepackage{listings} +\usepackage{xcolor} +\usepackage{geometry} +\usepackage{booktabs} +\usepackage{longtable} +\usepackage{caption} +\usepackage{subcaption} +\usepackage{titlesec} +\usepackage{float} +\usepackage{array} +\usepackage{multirow} +\usepackage{enumitem} + +\geometry{left=2.5cm,right=2cm,top=2cm,bottom=2cm} + +\hypersetup{ + colorlinks=true, + linkcolor=blue, + urlcolor=blue, + pdfauthor={Студент}, + pdftitle={Лабораторная работа N2} +} + +\lstset{ + basicstyle=\ttfamily\footnotesize, + keywordstyle=\color{blue}, + commentstyle=\color{green!60!black}, + stringstyle=\color{red}, + breaklines=true, + frame=single, + numbers=left, + numberstyle=\tiny, + tabsize=4, + language=Python, + extendedchars=true, + inputencoding=utf8, +} + +\titleformat{\section}{\Large\bfseries\centering}{}{0pt}{} +\titleformat{\subsection}{\large\bfseries}{}{0pt}{} +\titleformat{\subsubsection}{\normalsize\bfseries}{}{0pt}{} + +\renewcommand{\refname}{Список использованной литературы} + +\begin{document} + + % ============================================================================ + % ТИТУЛЬНЫЙ ЛИСТ + % ============================================================================ + + \begin{titlepage} + \begin{center} + \vspace*{0.5cm} + + {\Large \textbf{ФГБУ <<НМИЦ онкологии им. Н.Н. Блохина>> Минздрава России}} + + \vspace{0.3cm} + {\large Кафедра медицинской информатики и искусственного интеллекта} + + \vspace{2.5cm} + + {\Huge \textbf{ЛАБОРАТОРНАЯ РАБОТА №2}} + + \vspace{0.5cm} + {\huge \textbf{Анализ радиометрических признаков}} + + \vspace{0.5cm} + {\Large и построение модели обнаружения областей интереса} + + \vspace{2.5cm} + + \begin{tabular}{p{0.4\textwidth}p{0.4\textwidth}} + \textbf{Выполнил:} & \textbf{Руководитель:} \\ + \underline{\hspace{4cm}} & \underline{\hspace{4cm}} \\ + \textit{ФИО студента} & \textit{ФИО преподавателя} \\ + Группа: \underline{\hspace{2cm}} & Учёная степень: \underline{\hspace{2cm}} \\ + Вариант: \underline{\hspace{2cm}} & \\ + \end{tabular} + + \vspace{2.5cm} + + {\large Москва, 2026} + + \end{center} + \end{titlepage} + + % ============================================================================ + % СОДЕРЖАНИЕ + % ============================================================================ + + \tableofcontents + \newpage + + % ============================================================================ + % 1. ВВЕДЕНИЕ + % ============================================================================ + + \section{Введение} + + \subsection{Цель работы} + + Сформировать у слушателей практические навыки количественного анализа медицинских изображений методами радиомики и построения прогностических моделей машинного обучения для дифференциальной диагностики патологических образований. + + \textbf{Задачи работы:} + \begin{enumerate} + \item Провести разведочный анализ данных (EDA) радиометрических признаков. + \item Выполнить предобработку данных (импутация пропусков, масштабирование). + \item Обучить модель бинарной классификации для дифференцировки патологических состояний. + \item Оценить качество модели и интерпретировать результаты. + \item Проанализировать важность радиометрических признаков для клинической задачи. + \end{enumerate} + + \subsection{Клинический контекст} + + В рамках лабораторной работы используются данные радиомического анализа пациентов с новообразованиями почек из датасета \texttt{1\_2236040\_zno\_cyst\_AF}. Задача состоит в построении модели, способной дифференцировать патологические образования (злокачественные опухоли, доброкачественные опухоли, кисты, абсцессы) на основе количественных признаков, извлечённых из медицинских изображений. + + \textbf{Объекты дифференцировки:} + \begin{itemize} + \item Злокачественные опухоли (агрессивные новообразования) + \item Доброкачественные опухоли и кисты (неагрессивные образования) + \item Абсцессы (воспалительные процессы) + \end{itemize} + + \textbf{Клиническая значимость:} Неинвазивная дифференцировка типов патологических образований почек на основе радиомических признаков позволяет: + \begin{itemize} + \item Снизить количество инвазивных диагностических процедур (биопсий). + \item Ускорить процесс принятия клинических решений. + \item Повысить точность дифференциальной диагностики. + \item Персонализировать тактику лечения. + \end{itemize} + + \subsection{Концепция радиомики} + + Радиомика — это высокопроизводительный метод извлечения количественных признаков из медицинских изображений (КТ, МРТ, ПЭТ). Её ключевая идея заключается в том, что визуальная информация содержит скрытые паттерны, неразличимые для человеческого глаза, но поддающиеся математическому описанию. Эти паттерны могут служить неинвазивными биомаркерами, отражающими гистологию, генетику и агрессивность заболевания. + + \textbf{Пайплайн радиомического анализа:} + \begin{enumerate} + \item Получение медицинских изображений (КТ, МРТ). + \item Сегментация областей интереса (ROI). + \item Извлечение количественных признаков. + \item Предобработка и отбор признаков. + \item Построение прогностических моделей. + \item Клиническая валидация и интерпретация. + \end{enumerate} + + % ============================================================================ + % 2. ТЕОРЕТИЧЕСКАЯ ЧАСТЬ + % ============================================================================ + + \section{Теоретическая часть} + + \subsection{Классификация радиометрических признаков} + + Современные библиотеки (например, PyRadiomics) извлекают более 100 признаков. Они делятся на несколько основных групп. + + \subsubsection{Геометрические признаки (Shape)} + + Описывают форму и размер области интереса (ROI). + + \textbf{Элонгация (вытянутость):} + \begin{equation} + E = \frac{L_{minor}}{L_{major}} + \end{equation} + где: + \begin{itemize} + \item $E$ -- элонгация (от 0 до 1) + \item $L_{minor}$ и $L_{major}$ -- малая и большая оси аппроксимирующего эллипса + \end{itemize} + Значение, близкое к 1, указывает на округлую (часто доброкачественную) форму. + + \textbf{Сферичность:} + \begin{equation} + S = \frac{36\pi V^2}{A^3} + \end{equation} + где: + \begin{itemize} + \item $V$ -- объём ROI + \item $A$ -- площадь поверхности + \end{itemize} + Низкая сферичность указывает на инфильтративный, нерегулярный рост. + + \subsubsection{Статистики первого порядка (First-order)} + + Описывают распределение интенсивности пикселей без учёта их расположения. + + \textbf{Среднее значение:} + \begin{equation} + \mu = \frac{1}{N} \sum_{i=1}^{N} X_i + \end{equation} + где: + \begin{itemize} + \item $\mu$ -- средняя интенсивность + \item $N$ -- число пикселей в маске + \item $X_i$ -- интенсивность $i$-го пикселя + \end{itemize} + + \textbf{Дисперсия:} + \begin{equation} + \sigma^2 = \frac{1}{N-1} \sum_{i=1}^{N} (X_i - \mu)^2 + \end{equation} + где $\sigma^2$ -- мера разброса значений (неоднородности ткани). + + \textbf{Энтропия (мера хаотичности):} + \begin{equation} + H = -\sum_{k} p_k \cdot \log_2(p_k) + \end{equation} + где: + \begin{itemize} + \item $H$ -- энтропия Шеннона + \item $p_k$ -- вероятность появления интенсивности $k$ + \end{itemize} + Высокая энтропия часто коррелирует с некрозом или фиброзом внутри опухоли. + + \subsubsection{Текстурные признаки} + + \textbf{Матрица пространственной смежности (GLCM)} оценивает, как часто пиксели с определёнными уровнями яркости встречаются рядом. + + \textbf{Контраст:} + \begin{equation} + Contrast = \sum_{i} \sum_{j} |i - j|^2 \cdot P(i, j) + \end{equation} + где $P(i, j)$ -- вероятность совместного появления пикселей с интенсивностями $i$ и $j$. Высокий контраст указывает на резкие перепады яркости. + + \textbf{Энергия (однородность):} + \begin{equation} + Energy = \sum_{i} \sum_{j} P(i, j)^2 + \end{equation} + Высокая энергия указывает на однородность текстуры. + + \textbf{Корреляция:} + \begin{equation} + Correlation = \frac{\sum_{i} \sum_{j} (i - \mu_i)(j - \mu_j) P(i, j)}{\sigma_i \sigma_j} + \end{equation} + Характеризует линейную зависимость между соседними пикселями. + + \subsection{Метрики оценки моделей в клиническом контексте} + + При бинарной классификации ($0$ -- норма/доброкачественное, $1$ -- патология/злокачественное) критически важен выбор метрики. + + \textbf{Полнота (Recall):} + \begin{equation} + Recall = \frac{TP}{TP + FN} + \end{equation} + Приоритетна для скрининга: минимизирует пропуск патологии (ложноотрицательные результаты). + + \textbf{Точность (Precision):} + \begin{equation} + Precision = \frac{TP}{TP + FP} + \end{equation} + Приоритетна для подтверждающей диагностики: минимизирует ложные тревоги, ведущие к ненужным биопсиям. + + \textbf{F1-мера (баланс):} + \begin{equation} + F1 = 2 \cdot \frac{Precision \cdot Recall}{Precision + Recall} + \end{equation} + + \textbf{ROC-AUC:} Площадь под ROC-кривой. Значение 0.5 соответствует случайному угадыванию, 1.0 -- идеальному классификатору. + + \subsection{Предобработка данных} + + \textbf{Стандартизация (Z-score):} + \begin{equation} + z = \frac{x - \mu}{\sigma} + \end{equation} + где: + \begin{itemize} + \item $z$ -- нормализованное значение + \item $x$ -- исходное значение + \item $\mu$ и $\sigma$ -- среднее и стандартное отклонение признака по обучающей выборке + \end{itemize} + Обязательна для линейных моделей и нейросетей. + + \textbf{Импутация пропусков:} + \begin{equation} + x_{imputed} = \text{median}(X) + \end{equation} + Медианная импутация устойчива к выбросам. + + % ============================================================================ + % 3. ПРАКТИЧЕСКАЯ ЧАСТЬ + % ============================================================================ + + \section{Практическая часть} + + \subsection{Загрузка и разведочный анализ данных (EDA)} + + \subsubsection{Загрузка данных} + + Радиометрические характеристики сохраняются в CSV-формате после обработки на платформе <<БиоМедАналитика>>. + + \begin{lstlisting} + import pandas as pd + import numpy as np + import matplotlib.pyplot as plt + import seaborn as sns + + df = pd.read_csv('result.csv') + print(df.shape) + print(df.columns.tolist()[:10]) + print(df.info()) + print(df.describe()) + \end{lstlisting} + + \subsubsection{Анализ распределения классов} + + \begin{lstlisting} + print("Distribution of classes:") + print(df['label'].value_counts()) + print(df['label'].value_counts(normalize=True)) + + sns.countplot(data=df, x='label') + plt.title('Distribution of classes') + plt.show() + \end{lstlisting} + + \subsubsection{Визуализация распределений признаков} + + \begin{lstlisting} + features = ['original_firstorder_Mean', 'original_firstorder_Entropy', + 'original_glcm_Contrast', 'original_glcm_Correlation'] + + fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + for i, feat in enumerate(features): + ax = axes[i//2, i%2] + sns.histplot(data=df, x=feat, hue='label', kde=True, ax=ax) + ax.set_title(feat) + plt.tight_layout() + plt.show() + \end{lstlisting} + + \subsection{Предобработка данных} + + \subsubsection{Выделение признаков и целевой переменной} + + \begin{lstlisting} + feature_cols = [col for col in df.columns if col not in ['label', 'path_to_cell', 'source_file']] + X = df[feature_cols] + y = df['label'] + + print("Features shape:", X.shape) + print("Target shape:", y.shape) + print("Classes:", y.unique()) + \end{lstlisting} + + \subsubsection{Разделение на обучающую и тестовую выборки} + + \begin{lstlisting} + from sklearn.model_selection import train_test_split + + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.3, random_state=42, stratify=y + ) + + print("Train size:", X_train.shape[0]) + print("Test size:", X_test.shape[0]) + print("Train class distribution:\n", y_train.value_counts(normalize=True)) + print("Test class distribution:\n", y_test.value_counts(normalize=True)) + \end{lstlisting} + + \subsubsection{Импутация пропусков} + + \begin{lstlisting} + from sklearn.impute import SimpleImputer + + imputer = SimpleImputer(strategy='median') + X_train_imputed = imputer.fit_transform(X_train) + X_test_imputed = imputer.transform(X_test) + + print("Missing values after imputation:", + np.isnan(X_train_imputed).sum(), np.isnan(X_test_imputed).sum()) + \end{lstlisting} + + \subsubsection{Стандартизация} + + \begin{lstlisting} + from sklearn.preprocessing import StandardScaler + + scaler = StandardScaler() + X_train_scaled = scaler.fit_transform(X_train_imputed) + X_test_scaled = scaler.transform(X_test_imputed) + + print("Mean after scaling:", X_train_scaled.mean(axis=0)[:5]) + print("Std after scaling:", X_train_scaled.std(axis=0)[:5]) + \end{lstlisting} + + \subsection{Обучение модели} + + \subsubsection{Случайный лес (Random Forest)} + + Случайный лес устойчив к выбросам, не требует строгой нормализации и позволяет оценить важность признаков. + + \begin{lstlisting} + from sklearn.ensemble import RandomForestClassifier + from sklearn.metrics import classification_report, confusion_matrix + from sklearn.metrics import roc_auc_score, RocCurveDisplay + + model = RandomForestClassifier( + n_estimators=100, + random_state=42, + class_weight='balanced' + ) + + model.fit(X_train_scaled, y_train) + \end{lstlisting} + + \subsubsection{Оценка качества} + + \begin{lstlisting} + y_pred = model.predict(X_test_scaled) + y_prob = model.predict_proba(X_test_scaled)[:, 1] + + print("Classification Report:") + print(classification_report(y_test, y_pred, target_names=['Benign', 'Malignant'])) + + cm = confusion_matrix(y_test, y_pred) + print("Confusion Matrix:") + print(cm) + + auc = roc_auc_score(y_test, y_prob) + print("ROC-AUC:", auc) + \end{lstlisting} + + \subsubsection{Визуализация результатов} + + \begin{lstlisting} + fig, axes = plt.subplots(1, 2, figsize=(12, 5)) + + sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=axes[0]) + axes[0].set_title('Confusion Matrix') + axes[0].set_xlabel('Predicted') + axes[0].set_ylabel('True') + + RocCurveDisplay.from_estimator(model, X_test_scaled, y_test, ax=axes[1]) + axes[1].set_title('ROC Curve') + + plt.tight_layout() + plt.show() + \end{lstlisting} + + \subsection{Анализ важности признаков} + + \begin{lstlisting} + importance = pd.DataFrame({ + 'feature': feature_cols, + 'importance': model.feature_importances_ + }).sort_values('importance', ascending=False) + + print("Top 10 features:") + print(importance.head(10)) + + plt.figure(figsize=(10, 8)) + sns.barplot(data=importance.head(15), x='importance', y='feature') + plt.title('Feature Importance') + plt.xlabel('Importance') + plt.tight_layout() + plt.show() + \end{lstlisting} + + % ============================================================================ + % 4. ВАРИАНТЫ ЗАДАНИЙ + % ============================================================================ + + \section{Варианты заданий} + + Выберите один из вариантов в соответствии с указанием преподавателя. Каждый вариант акцентирует внимание на специфике определённой анатомической зоны. + + \begin{table}[H] + \centering + \caption{Варианты заданий} + \begin{tabular}{p{1.5cm}|p{5cm}|p{6cm}} + \toprule + \textbf{Вариант} & \textbf{Анатомическая зона} & \textbf{Акцент} \\ + \midrule + 1 & Почки & Геометрические признаки и статистики первого порядка \\ + 2 & Молочная железа & Текстурные признаки (GLCM, GLRLM) \\ + 3 & Головной мозг & Спектральные признаки и NGTDM \\ + 4 & Брюшная полость & Комбинированное использование всех групп признаков \\ + 5 & Мультимодальный анализ & Сравнительный анализ и формулировка гипотез \\ + \bottomrule + \end{tabular} + \end{table} + + \textbf{Задание повышенной сложности (со звёздочкой):} + + Реализуйте отбор признаков методом рекурсивного исключения (RFE) или через \texttt{SelectFromModel}. Сравните метрики модели до и после уменьшения размерности. + + \begin{lstlisting} + from sklearn.feature_selection import RFE + from sklearn.linear_model import LogisticRegression + + selector = RFE(LogisticRegression(), n_features_to_select=20) + selector.fit(X_train_scaled, y_train) + selected_features = selector.get_support() + + print("Selected features:", np.sum(selected_features)) + X_train_selected = X_train_scaled[:, selected_features] + X_test_selected = X_test_scaled[:, selected_features] + + model_selected = RandomForestClassifier(random_state=42) + model_selected.fit(X_train_selected, y_train) + y_pred_selected = model_selected.predict(X_test_selected) + + print("Accuracy with RFE:", accuracy_score(y_test, y_pred_selected)) + \end{lstlisting} + + % ============================================================================ + % 5. РЕЗУЛЬТАТЫ + % ============================================================================ + + \section{Результаты} + + \subsection{Пример результатов} + + Ниже приведены типичные результаты, которые можно получить при выполнении работы. + + \begin{table}[H] + \centering + \caption{Метрики качества модели (пример)} + \begin{tabular}{l|r|r|r} + \toprule + \textbf{Метрика} & \textbf{Значение} & \textbf{95\% CI} & \textbf{Интерпретация} \\ + \midrule + Accuracy & 0.89 & 0.85--0.93 & Хорошее качество \\ + Precision (Malignant) & 0.87 & 0.82--0.92 & Низкий уровень ЛП \\ + Recall (Malignant) & 0.92 & 0.88--0.96 & Высокая чувствительность \\ + F1 (Malignant) & 0.89 & 0.85--0.93 & Хороший баланс \\ + ROC-AUC & 0.94 & 0.91--0.97 & Отличная дискриминация \\ + \bottomrule + \end{tabular} + \end{table} + + \subsection{Важность признаков (пример)} + + \begin{table}[H] + \centering + \caption{Топ-10 важнейших признаков} + \begin{tabular}{l|r} + \toprule + \textbf{Признак} & \textbf{Важность} \\ + \midrule + original\_firstorder\_Entropy & 0.142 \\ + original\_glcm\_Contrast & 0.118 \\ + original\_firstorder\_Variance & 0.095 \\ + original\_shape\_Sphericity & 0.087 \\ + original\_glcm\_Correlation & 0.076 \\ + original\_firstorder\_Mean & 0.068 \\ + original\_glrlm\_RunEntropy & 0.062 \\ + original\_glszm\_ZoneEntropy & 0.055 \\ + original\_gldm\_DependenceEntropy & 0.048 \\ + original\_shape\_Elongation & 0.041 \\ + \bottomrule + \end{tabular} + \end{table} + + \textbf{Интерпретация:} + \begin{enumerate} + \item \textbf{Энтропия} (original\_firstorder\_Entropy) -- отражает гетерогенность ткани. Высокая энтропия указывает на неоднородность, характерную для злокачественных образований. + \item \textbf{Контраст} (original\_glcm\_Contrast) -- характеризует резкие перепады интенсивности, связанные с неоваскуляризацией и некрозом. + \item \textbf{Дисперсия} (original\_firstorder\_Variance) -- мера разброса значений интенсивности, отражающая неоднородность ткани. + \item \textbf{Сферичность} (original\_shape\_Sphericity) -- низкая сферичность характерна для инфильтративного роста злокачественных опухолей. + \end{enumerate} + + % ============================================================================ + % 6. ВЫВОДЫ + % ============================================================================ + + \section{Выводы} + + В ходе выполнения лабораторной работы были получены следующие результаты: + + \begin{enumerate} + \item \textbf{Проведён разведочный анализ данных}: выявлены закономерности в распределении признаков, определены классы и их соотношение. + + \item \textbf{Выполнена предобработка данных}: реализована импутация пропусков и стандартизация признаков. + + \item \textbf{Обучена модель случайного леса}: модель демонстрирует высокое качество классификации (ROC-AUC $> 0.9$). + + \item \textbf{Проведён анализ важности признаков}: выявлены ключевые радиометрические характеристики (энтропия, контраст, дисперсия, сферичность) для дифференцировки патологических состояний. + + \item \textbf{Результаты интерпретированы с клинической точки зрения}: подтверждена диагностическая ценность текстурных признаков и статистик первого порядка. + \end{enumerate} + + \textbf{Клинические рекомендации:} + \begin{enumerate} + \item Для скрининга рекомендуется использовать метрику Recall (минимизация пропуска патологии). + \item Для подтверждающей диагностики рекомендуется использовать метрику Precision (минимизация ложных тревог). + \item Текстурные признаки (GLCM\_Contrast, GLCM\_Entropy) являются наиболее информативными для дифференциальной диагностики. + \end{enumerate} + + % ============================================================================ + % 7. КОНТРОЛЬНЫЕ ВОПРОСЫ + % ============================================================================ + + \section{Контрольные вопросы} + + \begin{enumerate} + \item В чём заключается принципиальное различие между статистиками первого порядка и текстурными признаками? Приведите пример медицинской задачи, в которой каждый из этих типов признаков будет обладать максимальной диагностической ценностью. + + \item Почему метрика Accuracy может давать искажённое представление о качестве модели при работе с медицинскими данными? Какие альтернативные метрики предпочтительнее использовать в задачах скрининга и по какой причине? + + \item Какую информацию несёт коэффициент корреляции Пирсона между двумя признаками? Почему наличие сильной корреляции (мультиколлинеарности) может создавать проблемы при обучении линейных моделей? + + \item Дайте содержательное объяснение метрики ROC-AUC. В каких ситуациях её использование более оправдано по сравнению с F1-мерой? + + \item Перечислите обязательные этапы предобработки радиометрических данных перед обучением модели. Обоснуйте необходимость каждого из них с точки зрения статистики и машинного обучения. + + \item Проанализируйте матрицу ошибок вашей модели: какое количество ложноположительных и ложноотрицательных прогнозов было допущено? Какой из этих типов ошибок представляет большую опасность в контексте вашей задачи и почему? + + \item Назовите три признака, оказавшие наибольшее влияние на прогноз вашей модели. Как вы интерпретируете их вклад с медицинской точки зрения? + + \item Если бы у вас появилась возможность дополнить набор данных одним новым типом информации (помимо радиометрических признаков), что бы вы выбрали: клинические метаданные, результаты лабораторных исследований, геномные данные? Обоснуйте свой выбор. + + \item Как изменится ваша стратегия моделирования при увеличении объёма обучающей выборки в десять раз? Какие корректировки потребуются в случае сокращения данных вдвое? + + \item Какие шаги вы предпримете, если модель демонстрирует высокую полноту при низкой точности прогноза положительных случаев (или наоборот)? Как такой дисбаланс повлияет на возможности клинического применения модели? + \end{enumerate} + + % ============================================================================ + % 8. СПИСОК ЛИТЕРАТУРЫ + % ============================================================================ + + \section{Список литературы} + + \begin{enumerate} + \item Владзимирский, А. В. Компьютерное зрение в лучевой диагностике: первый этап Московского эксперимента / А. В. Владзимирский, Ю. А. Васильев, К. М. Арзамасов [и др.]. -- 2-е изд. -- Москва : ООО <<Издательские решения>>, 2023. -- 388 с. -- ISBN 978-5-0059-3043-9. + + \item Гавриченков, Ю. Д. Системы искусственного интеллекта : учебно-методическое пособие / Ю. Д. Гавриченков, В. Ю. Переверзев. -- Самара : Медицинский университет <<Реавиз>>, 2024. -- 55 с. + + \item Поляков, Е. В. Цифровая обработка медицинских изображений : учебное пособие / Е. В. Поляков, В. В. Дмитриева, А. А. Фроня. -- Москва : НИЯУ МИФИ, 2024. -- 100 с. + + \item Савченко, Е. Ю. Основы и применение нейронных сетей: теория, практика, инновации : учебное пособие / Е. Ю. Савченко. -- Бишкек : Нео Принт, 2024. -- 116 с. + + \item van Griethuysen, J. J. M. Computational Radiomics System to Decode the Radiographic Phenotype / J. J. M. van Griethuysen, A. Fedorov, C. Parmar [et al.] // Cancer Research. -- 2017. -- Vol. 77, No. 21. -- P. e104--e107. + + \item Lambin, P. Radiomics: the bridge between medical imaging and personalized medicine / P. Lambin, E. Rios-Velazquez, R. Leijenaar [et al.] // Nature Reviews Clinical Oncology. -- 2017. -- Vol. 14, No. 12. -- P. 749--762. + + \item Breiman, L. Random Forests / L. Breiman // Machine Learning. -- 2001. -- Vol. 45, No. 1. -- P. 5--32. + \end{enumerate} + +\end{document} \ No newline at end of file diff --git a/labs/lab#3.pdf b/labs/lab#3.pdf new file mode 100644 index 0000000..dfa4902 --- /dev/null +++ b/labs/lab#3.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75422798c2299243b99cc0f13735711e974f3df7109122e1f46e40b58bcf4a10 +size 425943 diff --git a/labs/lab#3.tex b/labs/lab#3.tex new file mode 100644 index 0000000..95f7eb7 --- /dev/null +++ b/labs/lab#3.tex @@ -0,0 +1,1064 @@ +% ============================================================================ +% LABORATORY WORK No.3 +% RADIOMIC ANALYSIS OF BRAIN TUMORS +% BASED ON MULTIPARAMETRIC MRI +% ============================================================================ + +\documentclass[12pt,a4paper]{article} + +% ============================================================================ +% PREAMBLE +% ============================================================================ + +\usepackage[utf8]{inputenc} +\usepackage[T2A]{fontenc} +\usepackage[russian]{babel} + +\usepackage{amsmath,amssymb,amsfonts} +\usepackage{graphicx} +\usepackage{hyperref} +\usepackage{listings} +\usepackage{xcolor} +\usepackage{geometry} +\usepackage{booktabs} +\usepackage{longtable} +\usepackage{caption} +\usepackage{subcaption} +\usepackage{titlesec} +\usepackage{float} +\usepackage{array} +\usepackage{multirow} +\usepackage{enumitem} + +\geometry{left=2.5cm,right=2cm,top=2cm,bottom=2cm} + +\hypersetup{ + colorlinks=true, + linkcolor=blue, + urlcolor=blue, + pdfauthor={Student}, + pdftitle={Lab 3} +} + +% --- LSTLISTING SETTINGS (ENGLISH COMMENTS ONLY) --- +\lstset{ + basicstyle=\ttfamily\footnotesize, + keywordstyle=\color{blue}, + commentstyle=\color{green!60!black}, + stringstyle=\color{red}, + breaklines=true, + frame=single, + numbers=left, + numberstyle=\tiny, + tabsize=4, + language=Python, + extendedchars=true, + inputencoding=utf8, +} + +\titleformat{\section}{\Large\bfseries\centering}{}{0pt}{} +\titleformat{\subsection}{\large\bfseries}{}{0pt}{} +\titleformat{\subsubsection}{\normalsize\bfseries}{}{0pt}{} + +\renewcommand{\refname}{References} + +\begin{document} + + % ============================================================================ + % TITLE PAGE + % ============================================================================ + + \begin{titlepage} + \begin{center} + \vspace*{0.5cm} + + {\Large \textbf{FGBU <>}} + + \vspace{0.3cm} + {\large Department of Medical Informatics and AI} + + \vspace{2.5cm} + + {\Huge \textbf{LABORATORY WORK No.3}} + + \vspace{0.5cm} + {\huge \textbf{Radiomic Analysis of Brain Tumors}} + + \vspace{0.5cm} + {\Large based on Multiparametric MRI} + + \vspace{2.5cm} + + \begin{tabular}{p{0.4\textwidth}p{0.4\textwidth}} + \textbf{Student:} & \textbf{Supervisor:} \\ + \underline{\hspace{4cm}} & \underline{\hspace{4cm}} \\ + \textit{Full name} & \textit{Full name} \\ + Group: \underline{\hspace{2cm}} & Degree: \underline{\hspace{2cm}} \\ + Variant: \underline{\hspace{2cm}} & \\ + \end{tabular} + + \vspace{2.5cm} + + {\large Moscow, 2026} + + \end{center} + \end{titlepage} + + % ============================================================================ + % TABLE OF CONTENTS + % ============================================================================ + + \tableofcontents + \newpage + + % ============================================================================ + % 1. INTRODUCTION + % ============================================================================ + + \section{Введение} + + \subsection{Цель работы} + + Сформировать у слушателей компетенции в области мультипараметрического радиомического анализа опухолей головного мозга с использованием данных магнитно-резонансной томографии (МРТ) в рамках стандарта BRATS, а также освоить методы сравнительного анализа радиомических профилей пациентов для оценки гетерогенности и агрессивности новообразований. + + \textbf{Задачи работы:} + \begin{enumerate} + \item Сравнить распределение радиометрических признаков между четырьмя модальностями МРТ (T1, T1c, T2, FLAIR). + \item Провести корреляционный анализ между модальностями. + \item Построить радарные диаграммы радиомических профилей пациентов. + \item Выполнить PCA-анализ для снижения размерности данных. + \item Оценить важность признаков для дифференцировки фенотипов опухоли. + \item Сформулировать клинические выводы на основе количественных радиомических данных. + \end{enumerate} + + \subsection{Клинический контекст} + + Современная диагностика опухолей головного мозга основана на использовании мультипараметрической МРТ (mpMRI), которая включает четыре ключевые модальности, каждая из которых предоставляет уникальную информацию о биологических характеристиках новообразования. + + \textbf{Стандарт BRATS (Brain Tumor Segmentation):} + \begin{itemize} + \item Международный стандарт для задач сегментации и анализа опухолей мозга. + \item Включает мультипараметрические МРТ-исследования с экспертной разметкой. + \item Содержит четыре ключевых субрегиона опухоли. + \end{itemize} + + \textbf{Клиническая значимость:} Радиомический анализ позволяет не только количественно охарактеризовать биологические свойства опухоли, но и провести неинвазивную дифференциальную диагностику между различными фенотипами новообразований головного мозга, что имеет важное значение для планирования тактики лечения и прогнозирования клинических исходов. + + \subsection{Мультипараметрическая МРТ в нейровизуализации} + + \subsubsection{T1-взвешенные изображения (T1)} + + Отображают анатомическую структуру мозга. Опухолевая ткань обычно гипоинтенсивна, что позволяет визуализировать общие контуры образования. + + \subsubsection{T1 с контрастным усилением (T1c)} + + Ключевая модальность для оценки гематоэнцефалического барьера. Позволяет выявить зоны активного роста опухоли, некроза и неоангиогенеза. + + \subsubsection{T2-взвешенные изображения (T2)} + + Чувствительны к содержанию воды в тканях. Опухолевая ткань и перитуморальный отёк выглядят гиперинтенсивными. + + \subsubsection{FLAIR} + + Модификация T2, в которой сигнал от свободной жидкости подавляется. Позволяет чётко отделить перитуморальный отёк от желудочков мозга. + + % ============================================================================ + % 2. THEORETICAL BACKGROUND + % ============================================================================ + + \section{Теоретическая часть} + + \subsection{Стандарт BRATS и субрегионы опухоли} + + \textbf{Субрегионы опухоли:} + \begin{itemize} + \item \textbf{Некротическое ядро (NCR)}: зоны некроза и кровоизлияний. Маркер высокой агрессивности. + \item \textbf{Отёчная область (ED)}: перитуморальный вазогенный отёк. + \item \textbf{Усиливающаяся опухоль (ET)}: активно растущая опухолевая ткань с нарушением ГЭБ. + \item \textbf{Общая опухоль (WT)}: объединение всех трёх областей. + \end{itemize} + + \subsection{Классификация радиометрических признаков} + + \subsubsection{Геометрические признаки} + + \textbf{Сферичность (Shape\_Sphericity):} + \begin{equation} + S = \frac{36\pi V^2}{A^3} + \end{equation} + Низкая сферичность указывает на инфильтративный рост. + + \subsubsection{Статистики первого порядка} + + \textbf{Средняя интенсивность:} + \begin{equation} + \mu = \frac{1}{N} \sum_{i=1}^{N} X_i + \end{equation} + + \textbf{Энтропия:} + \begin{equation} + H = -\sum_{k} p_k \cdot \log_2(p_k) + \end{equation} + Высокая энтропия коррелирует с некрозом и гетерогенностью. + + \subsubsection{Текстурные признаки GLCM} + + \textbf{Контраст:} + \begin{equation} + Contrast = \sum_{i} \sum_{j} |i - j|^2 \cdot P(i, j) + \end{equation} + + \textbf{Корреляция:} + \begin{equation} + Correlation = \frac{\sum_{i} \sum_{j} (i - \mu_i)(j - \mu_j) P(i, j)}{\sigma_i \sigma_j} + \end{equation} + + \subsection{Клиническая интерпретация} + + \begin{table}[H] + \centering + \caption{Клиническая интерпретация радиомических признаков} + \begin{tabular}{p{5cm}|p{8cm}} + \toprule + \textbf{Признак} & \textbf{Интерпретация} \\ + \midrule + Высокая энтропия в T1c & Активное контрастное усиление, высокая степень злокачественности \\ + Высокий контраст & Неоднородность ткани, некротические зоны \\ + Высокая дисперсия в T2/FLAIR & Выраженный перитуморальный отёк \\ + Низкая сферичность & Инфильтративный рост, характерен для глиобластомы \\ + \bottomrule + \end{tabular} + \end{table} + + % ============================================================================ + % 3. PRACTICAL PART + % ============================================================================ + + \section{Практическая часть} + + \subsection{Data Loading} + + \begin{lstlisting} + import pandas as pd + import numpy as np + import matplotlib.pyplot as plt + import seaborn as sns + from sklearn.preprocessing import StandardScaler + from sklearn.decomposition import PCA + from sklearn.ensemble import RandomForestClassifier + import warnings + warnings.filterwarnings('ignore') + + # Load data + df = pd.read_csv('combined_multichannel_features.csv') + + # View data + print("Patients:", df['patient_id'].unique()) + print("Shape:", df.shape) + \end{lstlisting} + + \subsection{Modality Comparison} + + \begin{lstlisting} + def plot_modality_comparison(): + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + features = ['firstorder_Mean', 'firstorder_Entropy', + 'glcm_Contrast', 'glcm_Correlation'] + + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + axes = axes.flatten() + + for idx, feature in enumerate(features): + ax = axes[idx] + data = [] + + for mod in modalities: + col = mod + '_' + feature + if col in df.columns: + for patient in df['patient_id'].unique(): + val = df[df['patient_id'] == patient][col].values[0] + data.append({'modality': mod, 'value': val, + 'patient': patient}) + + plot_df = pd.DataFrame(data) + sns.boxplot(data=plot_df, x='modality', y='value', + hue='patient', ax=ax) + ax.set_title(feature) + ax.legend(title='Patient') + + plt.tight_layout() + plt.savefig('modality_comparison.png', dpi=300) + plt.show() + + plot_modality_comparison() + \end{lstlisting} + + \subsection{Correlation Analysis} + + \begin{lstlisting} + def plot_correlation_matrix(): + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + features = ['firstorder_Mean', 'firstorder_Entropy', + 'glcm_Contrast', 'glcm_Correlation'] + + corr_dict = {} + for patient in df['patient_id'].unique(): + for feat in features: + for mod in modalities: + col = mod + '_' + feat + if col in df.columns: + val = df[df['patient_id'] == patient][col].values[0] + if isinstance(val, (int, float)): + key = patient + '_' + mod + '_' + feat + corr_dict[key] = val + + corr_data = [] + for patient in df['patient_id'].unique(): + row = [] + for mod in modalities: + for feat in features: + key = patient + '_' + mod + '_' + feat + row.append(corr_dict.get(key, np.nan)) + corr_data.append(row) + + cols = [] + for m in modalities: + for f in features: + cols.append(m + '_' + f) + + corr_df = pd.DataFrame(corr_data, index=df['patient_id'].unique(), + columns=cols) + corr_matrix = corr_df.T.corr() + + plt.figure(figsize=(10, 8)) + sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', + square=True) + plt.title('Correlation Matrix') + plt.tight_layout() + plt.savefig('correlation_matrix.png', dpi=300) + plt.show() + + plot_correlation_matrix() + \end{lstlisting} + + \subsection{Radar Chart} + + \begin{lstlisting} + def plot_radar_chart(): + radar_features = [ + ('shape_Sphericity', 'Sphericity'), + ('firstorder_Mean', 'Mean'), + ('firstorder_Entropy', 'Entropy'), + ('glcm_Contrast', 'Contrast'), + ('glcm_Correlation', 'Correlation') + ] + + modalities = ['T1', 'T1c'] + fig, axes = plt.subplots(1, 2, figsize=(14, 6), + subplot_kw=dict(polar=True)) + + for idx, mod in enumerate(modalities): + ax = axes[idx] + labels = [] + p1_vals = [] + p2_vals = [] + + for feat, label in radar_features: + col = mod + '_' + feat + if col in df.columns: + labels.append(label) + p1 = df[df['patient_id'] == 'BRATS_001'][col].values[0] + p2 = df[df['patient_id'] == 'BRATS_002'][col].values[0] + if isinstance(p1, (int, float)): + p1_vals.append(p1) + p2_vals.append(p2) + + if p1_vals: + all_vals = p1_vals + p2_vals + if min(all_vals) != max(all_vals): + min_val = min(all_vals) + max_val = max(all_vals) + else: + min_val = 0 + max_val = 1 + + p1_norm = [(v - min_val) / (max_val - min_val) for v in p1_vals] + p2_norm = [(v - min_val) / (max_val - min_val) for v in p2_vals] + + angles = np.linspace(0, 2*np.pi, len(labels), endpoint=False).tolist() + angles += angles[:1] + p1_norm += p1_norm[:1] + p2_norm += p2_norm[:1] + + ax.plot(angles, p1_norm, 'o-', linewidth=2, + label='BRATS_001', color='#FF6B6B') + ax.fill(angles, p1_norm, alpha=0.25, color='#FF6B6B') + ax.plot(angles, p2_norm, 'o-', linewidth=2, + label='BRATS_002', color='#4ECDC4') + ax.fill(angles, p2_norm, alpha=0.25, color='#4ECDC4') + ax.set_xticks(angles[:-1]) + ax.set_xticklabels(labels, size=10) + ax.set_ylim(0, 1) + ax.set_title('Modality: ' + mod) + ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.0)) + + plt.suptitle('Patient Profiles Comparison') + plt.tight_layout() + plt.savefig('radar_chart.png', dpi=300) + plt.show() + + plot_radar_chart() + \end{lstlisting} + + \subsection{PCA Analysis} + + \begin{lstlisting} + def plot_pca_analysis(): + patterns = ['firstorder', 'glcm', 'glrlm', 'glszm'] + selected_cols = [] + + for pattern in patterns: + cols = [col for col in df.columns if pattern in col + and 'wavelet' not in col] + selected_cols.extend(cols[:10]) + + X = df[selected_cols].select_dtypes(include=[np.number]).fillna(0) + scaler = StandardScaler() + X_scaled = scaler.fit_transform(X) + + pca = PCA(n_components=2) + X_pca = pca.fit_transform(X_scaled) + + fig, axes = plt.subplots(1, 2, figsize=(14, 6)) + + ax1 = axes[0] + explained = pca.explained_variance_ratio_ + cum_var = np.cumsum(explained) + ax1.bar(range(1, len(explained)+1), explained, alpha=0.7, label='Ind') + ax1.plot(range(1, len(explained)+1), cum_var, 'ro-', label='Cum') + ax1.set_xlabel('Principal Component') + ax1.set_ylabel('Explained Variance') + ax1.set_title('Scree Plot') + ax1.legend() + ax1.grid(True, alpha=0.3) + + ax2 = axes[1] + patients = df['patient_id'].values + colors = {'BRATS_001': '#FF6B6B', 'BRATS_002': '#4ECDC4'} + for patient in df['patient_id'].unique(): + mask = (patients == patient) + ax2.scatter(X_pca[mask, 0], X_pca[mask, 1] if X_pca.shape[1] > 1 else 0, + c=colors[patient], s=200, label=patient, + edgecolors='black', linewidth=2, alpha=0.8) + + pc1_text = 'PC1 (' + str(round(explained[0]*100, 1)) + '%)' + pc2_text = 'PC2 (' + str(round(explained[1]*100, 1)) + '%)' + ax2.set_xlabel(pc1_text) + ax2.set_ylabel(pc2_text) + ax2.set_title('PCA Projection') + ax2.legend() + ax2.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig('pca_analysis.png', dpi=300) + plt.show() + + plot_pca_analysis() + \end{lstlisting} + + \subsection{Feature Importance} + + \begin{lstlisting} + def plot_feature_importance(): + patterns = ['firstorder_Mean', 'firstorder_Entropy', + 'glcm_Contrast', 'glcm_Correlation', + 'glrlm_RunEntropy', 'glszm_ZoneEntropy'] + + selected_cols = [] + for pattern in patterns: + cols = [col for col in df.columns if pattern in col + and 'wavelet' not in col] + selected_cols.extend(cols[:4]) + + X = df[selected_cols].select_dtypes(include=[np.number]).fillna(0) + y = (df['patient_id'] == 'BRATS_001').astype(int) + + rf = RandomForestClassifier(n_estimators=100, random_state=42) + rf.fit(X, y) + + importance = pd.DataFrame({ + 'feature': selected_cols[:len(rf.feature_importances_)], + 'importance': rf.feature_importances_ + }).sort_values('importance', ascending=False) + + plt.figure(figsize=(12, 8)) + top_n = min(15, len(importance)) + + colors = [] + for f in importance.head(top_n)['feature']: + if 'T1' in f: + colors.append('#FF6B6B') + elif 'T1c' in f: + colors.append('#FF9F43') + elif 'T2' in f: + colors.append('#4ECDC4') + else: + colors.append('#45B7D1') + + sns.barplot(data=importance.head(top_n), x='importance', y='feature', + palette=colors) + plt.title('Feature Importance') + plt.xlabel('Importance') + plt.tight_layout() + plt.savefig('feature_importance.png', dpi=300) + plt.show() + + print(importance.head(10)) + + plot_feature_importance() + \end{lstlisting} + + \subsection{Comprehensive Heatmap} + + \begin{lstlisting} + def plot_comprehensive_heatmap(): + key_features = [ + 'shape_Sphericity', + 'firstorder_Mean', + 'firstorder_Entropy', + 'firstorder_Variance', + 'glcm_Contrast', + 'glcm_Correlation', + 'glrlm_RunEntropy', + 'glszm_ZoneEntropy' + ] + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + + heatmap_data = [] + row_labels = [] + + for patient in df['patient_id'].unique(): + for mod in modalities: + row_name = patient + '_' + mod + row_labels.append(row_name) + row_values = [] + for feat in key_features: + col = mod + '_' + feat + if col in df.columns: + val = df[df['patient_id'] == patient][col].values[0] + if isinstance(val, (int, float)): + row_values.append(val) + else: + row_values.append(0) + else: + row_values.append(0) + heatmap_data.append(row_values) + + heatmap_df = pd.DataFrame(heatmap_data, index=row_labels, + columns=[f[:15] for f in key_features]) + + mean_vals = heatmap_df.mean(axis=1).values.reshape(-1, 1) + std_vals = heatmap_df.std(axis=1).values.reshape(-1, 1) + heatmap_norm = (heatmap_df - mean_vals) / std_vals + heatmap_norm = heatmap_norm.fillna(0) + + plt.figure(figsize=(14, 6)) + sns.heatmap(heatmap_norm, annot=True, fmt='.2f', cmap='coolwarm', + cbar_kws={'label': 'Z-score'}, + linewidths=0.5, linecolor='white') + plt.title('Comprehensive Radiomic Analysis') + plt.xlabel('Features') + plt.ylabel('Patient / Modality') + plt.tight_layout() + plt.savefig('heatmap.png', dpi=300) + plt.show() + + plot_comprehensive_heatmap() + \end{lstlisting} + + % ============================================================================ + % 4. KEY FINDINGS + % ============================================================================ + + \section{Ключевые выводы} + + \begin{enumerate} + \item T1c показывает наибольшую гетерогенность опухоли. + \item T2 и FLAIR отражают перитуморальный отёк. + \item GLCM\_Contrast и GLCM\_Entropy наиболее информативны. + \item Shape\_Sphericity отражает агрессивность роста. + \item BRATS\_001 имеет более гетерогенную опухоль. + \item BRATS\_002 имеет более компактную опухоль. + \end{enumerate} + + \subsection{Сводная клиническая интерпретация} + + \textbf{BRATS\_001:} + \begin{itemize} + \item Более гетерогенная структура опухоли. + \item Высокая энтропия и контраст на T2/FLAIR. + \item Типичен для глиобластомы. + \end{itemize} + + \textbf{BRATS\_002:} + \begin{itemize} + \item Более высокие значения корреляции. + \item Относительно низкая энтропия. + \item Характерен для менее агрессивных новообразований. + \end{itemize} + + % ============================================================================ + % 5. CONCLUSIONS + % ============================================================================ + + \section{Выводы} + + \begin{enumerate} + \item Успешно выполнено сравнение модальностей МРТ. + \item Построены корреляционные матрицы. + \item Созданы радарные диаграммы. + \item Выполнен PCA-анализ. + \item Определены важнейшие признаки. + \item Сформулированы клинические выводы. + \end{enumerate} + + % ============================================================================ + % 6. CONTROL QUESTIONS + % ============================================================================ + + \section{Контрольные вопросы} + + \subsection{Теоретический блок} + + \begin{enumerate} + \item Объясните физическую природу модальностей МРТ. + \item Что такое стандарт BRATS? + \item Почему текстурные признаки более информативны? + \item Что означает низкая сферичность? + \item Объясните принцип PCA. + \end{enumerate} + + \subsection{Практический блок} + + \begin{enumerate} + \item Проанализируйте распределение энтропии для BRATS\_001. + \item Сравните контраст на T1c для обоих пациентов. + \item Постройте корреляционную матрицу. + \item Определите важнейшие признаки. + \item Предложите дополнительные модальности. + \end{enumerate} + + \subsection{Рефлексивный блок} + + \begin{enumerate} + \item С какими трудностями вы столкнулись? + \item Как навыки радиомики могут быть применены? + \item Какие этические аспекты необходимо учитывать? + \end{enumerate} + + % ============================================================================ + % 7. REFERENCES + % ============================================================================ + + \section{Список литературы} + + \begin{enumerate} + \item Bakas, S. Advancing The Cancer Genome Atlas glioma MRI collections. -- 2017. + \item Lambin, P. Radiomics: the bridge between medical imaging and personalized medicine. -- 2017. + \item Menze, B. H. The Multimodal Brain Tumor Image Segmentation Benchmark. -- 2015. + \item van Griethuysen, J. J. M. Computational Radiomics System. -- 2017. + \item Владзимирский, А. В. Компьютерное зрение в лучевой диагностике. -- 2023. + \end{enumerate} + + % ============================================================================ + % APPENDIX A. FULL ANALYSIS SCRIPT + % ============================================================================ + + \appendix + \section{Приложение А. Полный код скрипта анализа} + + \begin{lstlisting}[basicstyle=\ttfamily\footnotesize] + import pandas as pd + import numpy as np + import matplotlib.pyplot as plt + import seaborn as sns + from sklearn.preprocessing import StandardScaler + from sklearn.decomposition import PCA + from sklearn.ensemble import RandomForestClassifier + from sklearn.model_selection import cross_val_score + import warnings + warnings.filterwarnings('ignore') + + # ============================================================================ + # 1. LOAD DATA + # ============================================================================ + + df = pd.read_csv('combined_multichannel_features.csv') + print("Patients:", df['patient_id'].unique()) + print("Shape:", df.shape) + + # ============================================================================ + # 2. MODALITY COMPARISON + # ============================================================================ + + def plot_modality_comparison(): + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + features = ['firstorder_Mean', 'firstorder_Entropy', + 'glcm_Contrast', 'glcm_Correlation'] + + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + axes = axes.flatten() + + for idx, feature in enumerate(features): + ax = axes[idx] + data = [] + + for mod in modalities: + col = mod + '_' + feature + if col in df.columns: + for patient in df['patient_id'].unique(): + val = df[df['patient_id'] == patient][col].values[0] + data.append({'modality': mod, 'value': val, + 'patient': patient}) + + plot_df = pd.DataFrame(data) + sns.boxplot(data=plot_df, x='modality', y='value', + hue='patient', ax=ax) + ax.set_title(feature) + ax.legend(title='Patient') + + plt.tight_layout() + plt.savefig('modality_comparison.png', dpi=300) + plt.show() + + plot_modality_comparison() + + # ============================================================================ + # 3. CORRELATION MATRIX + # ============================================================================ + + def plot_correlation_matrix(): + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + features = ['firstorder_Mean', 'firstorder_Entropy', + 'glcm_Contrast', 'glcm_Correlation'] + + corr_dict = {} + for patient in df['patient_id'].unique(): + for feat in features: + for mod in modalities: + col = mod + '_' + feat + if col in df.columns: + val = df[df['patient_id'] == patient][col].values[0] + if isinstance(val, (int, float)): + key = patient + '_' + mod + '_' + feat + corr_dict[key] = val + + corr_data = [] + for patient in df['patient_id'].unique(): + row = [] + for mod in modalities: + for feat in features: + key = patient + '_' + mod + '_' + feat + row.append(corr_dict.get(key, np.nan)) + corr_data.append(row) + + cols = [] + for m in modalities: + for f in features: + cols.append(m + '_' + f) + + corr_df = pd.DataFrame(corr_data, index=df['patient_id'].unique(), + columns=cols) + corr_matrix = corr_df.T.corr() + + plt.figure(figsize=(10, 8)) + sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', + square=True) + plt.title('Correlation Matrix') + plt.tight_layout() + plt.savefig('correlation_matrix.png', dpi=300) + plt.show() + + plot_correlation_matrix() + + # ============================================================================ + # 4. RADAR CHART + # ============================================================================ + + def plot_radar_chart(): + radar_features = [ + ('shape_Sphericity', 'Sphericity'), + ('firstorder_Mean', 'Mean'), + ('firstorder_Entropy', 'Entropy'), + ('glcm_Contrast', 'Contrast'), + ('glcm_Correlation', 'Correlation') + ] + + modalities = ['T1', 'T1c'] + fig, axes = plt.subplots(1, 2, figsize=(14, 6), + subplot_kw=dict(polar=True)) + + for idx, mod in enumerate(modalities): + ax = axes[idx] + labels = [] + p1_vals = [] + p2_vals = [] + + for feat, label in radar_features: + col = mod + '_' + feat + if col in df.columns: + labels.append(label) + p1 = df[df['patient_id'] == 'BRATS_001'][col].values[0] + p2 = df[df['patient_id'] == 'BRATS_002'][col].values[0] + if isinstance(p1, (int, float)): + p1_vals.append(p1) + p2_vals.append(p2) + + if p1_vals: + all_vals = p1_vals + p2_vals + if min(all_vals) != max(all_vals): + min_val = min(all_vals) + max_val = max(all_vals) + else: + min_val = 0 + max_val = 1 + + p1_norm = [(v - min_val) / (max_val - min_val) for v in p1_vals] + p2_norm = [(v - min_val) / (max_val - min_val) for v in p2_vals] + + angles = np.linspace(0, 2*np.pi, len(labels), endpoint=False).tolist() + angles += angles[:1] + p1_norm += p1_norm[:1] + p2_norm += p2_norm[:1] + + ax.plot(angles, p1_norm, 'o-', linewidth=2, + label='BRATS_001', color='#FF6B6B') + ax.fill(angles, p1_norm, alpha=0.25, color='#FF6B6B') + ax.plot(angles, p2_norm, 'o-', linewidth=2, + label='BRATS_002', color='#4ECDC4') + ax.fill(angles, p2_norm, alpha=0.25, color='#4ECDC4') + ax.set_xticks(angles[:-1]) + ax.set_xticklabels(labels, size=10) + ax.set_ylim(0, 1) + ax.set_title('Modality: ' + mod) + ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.0)) + + plt.suptitle('Patient Profiles Comparison') + plt.tight_layout() + plt.savefig('radar_chart.png', dpi=300) + plt.show() + + plot_radar_chart() + + # ============================================================================ + # 5. PCA ANALYSIS + # ============================================================================ + + def plot_pca_analysis(): + patterns = ['firstorder', 'glcm', 'glrlm', 'glszm'] + selected_cols = [] + + for pattern in patterns: + cols = [col for col in df.columns if pattern in col + and 'wavelet' not in col] + selected_cols.extend(cols[:10]) + + X = df[selected_cols].select_dtypes(include=[np.number]).fillna(0) + scaler = StandardScaler() + X_scaled = scaler.fit_transform(X) + + pca = PCA(n_components=2) + X_pca = pca.fit_transform(X_scaled) + + fig, axes = plt.subplots(1, 2, figsize=(14, 6)) + + ax1 = axes[0] + explained = pca.explained_variance_ratio_ + cum_var = np.cumsum(explained) + ax1.bar(range(1, len(explained)+1), explained, alpha=0.7, label='Ind') + ax1.plot(range(1, len(explained)+1), cum_var, 'ro-', label='Cum') + ax1.set_xlabel('Principal Component') + ax1.set_ylabel('Explained Variance') + ax1.set_title('Scree Plot') + ax1.legend() + ax1.grid(True, alpha=0.3) + + ax2 = axes[1] + patients = df['patient_id'].values + colors = {'BRATS_001': '#FF6B6B', 'BRATS_002': '#4ECDC4'} + for patient in df['patient_id'].unique(): + mask = (patients == patient) + ax2.scatter(X_pca[mask, 0], X_pca[mask, 1] if X_pca.shape[1] > 1 else 0, + c=colors[patient], s=200, label=patient, + edgecolors='black', linewidth=2, alpha=0.8) + + pc1_text = 'PC1 (' + str(round(explained[0]*100, 1)) + '%)' + pc2_text = 'PC2 (' + str(round(explained[1]*100, 1)) + '%)' + ax2.set_xlabel(pc1_text) + ax2.set_ylabel(pc2_text) + ax2.set_title('PCA Projection') + ax2.legend() + ax2.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig('pca_analysis.png', dpi=300) + plt.show() + + plot_pca_analysis() + + # ============================================================================ + # 6. FEATURE IMPORTANCE + # ============================================================================ + + def plot_feature_importance(): + patterns = ['firstorder_Mean', 'firstorder_Entropy', + 'glcm_Contrast', 'glcm_Correlation', + 'glrlm_RunEntropy', 'glszm_ZoneEntropy'] + + selected_cols = [] + for pattern in patterns: + cols = [col for col in df.columns if pattern in col + and 'wavelet' not in col] + selected_cols.extend(cols[:4]) + + X = df[selected_cols].select_dtypes(include=[np.number]).fillna(0) + y = (df['patient_id'] == 'BRATS_001').astype(int) + + rf = RandomForestClassifier(n_estimators=100, random_state=42) + rf.fit(X, y) + + importance = pd.DataFrame({ + 'feature': selected_cols[:len(rf.feature_importances_)], + 'importance': rf.feature_importances_ + }).sort_values('importance', ascending=False) + + plt.figure(figsize=(12, 8)) + top_n = min(15, len(importance)) + + colors = [] + for f in importance.head(top_n)['feature']: + if 'T1' in f: + colors.append('#FF6B6B') + elif 'T1c' in f: + colors.append('#FF9F43') + elif 'T2' in f: + colors.append('#4ECDC4') + else: + colors.append('#45B7D1') + + sns.barplot(data=importance.head(top_n), x='importance', y='feature', + palette=colors) + plt.title('Feature Importance') + plt.xlabel('Importance') + plt.tight_layout() + plt.savefig('feature_importance.png', dpi=300) + plt.show() + + print(importance.head(10)) + + plot_feature_importance() + + # ============================================================================ + # 7. COMPREHENSIVE HEATMAP + # ============================================================================ + + def plot_comprehensive_heatmap(): + key_features = [ + 'shape_Sphericity', + 'firstorder_Mean', + 'firstorder_Entropy', + 'firstorder_Variance', + 'glcm_Contrast', + 'glcm_Correlation', + 'glrlm_RunEntropy', + 'glszm_ZoneEntropy' + ] + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + + heatmap_data = [] + row_labels = [] + + for patient in df['patient_id'].unique(): + for mod in modalities: + row_name = patient + '_' + mod + row_labels.append(row_name) + row_values = [] + for feat in key_features: + col = mod + '_' + feat + if col in df.columns: + val = df[df['patient_id'] == patient][col].values[0] + if isinstance(val, (int, float)): + row_values.append(val) + else: + row_values.append(0) + else: + row_values.append(0) + heatmap_data.append(row_values) + + heatmap_df = pd.DataFrame(heatmap_data, index=row_labels, + columns=[f[:15] for f in key_features]) + + mean_vals = heatmap_df.mean(axis=1).values.reshape(-1, 1) + std_vals = heatmap_df.std(axis=1).values.reshape(-1, 1) + heatmap_norm = (heatmap_df - mean_vals) / std_vals + heatmap_norm = heatmap_norm.fillna(0) + + plt.figure(figsize=(14, 6)) + sns.heatmap(heatmap_norm, annot=True, fmt='.2f', cmap='coolwarm', + cbar_kws={'label': 'Z-score'}, + linewidths=0.5, linecolor='white') + plt.title('Comprehensive Radiomic Analysis') + plt.xlabel('Features') + plt.ylabel('Patient / Modality') + plt.tight_layout() + plt.savefig('heatmap.png', dpi=300) + plt.show() + + plot_comprehensive_heatmap() + + print("\nAnalysis completed successfully!") + \end{lstlisting} + + % ============================================================================ + % APPENDIX B. RESULTS SUMMARY + % ============================================================================ + + \appendix + \section{Приложение Б. Сводная таблица результатов} + + \begin{table}[H] + \centering + \caption{Сравнение ключевых признаков} + \begin{tabular}{l|l|r|r|r|r|r} + \toprule + \textbf{Признак} & \textbf{Пациент} & \textbf{T1} & \textbf{T1c} & \textbf{T2} & \textbf{FLAIR} & \textbf{Среднее} \\ + \midrule + Entropy & BRATS\_001 & 3.72 & 4.15 & 3.98 & 4.34 & 4.05 \\ + Entropy & BRATS\_002 & 3.45 & 3.88 & 3.67 & 4.12 & 3.78 \\ + Contrast & BRATS\_001 & 245.6 & 312.4 & 278.9 & 298.7 & 283.9 \\ + Contrast & BRATS\_002 & 198.3 & 256.7 & 223.4 & 245.6 & 231.0 \\ + Correlation & BRATS\_001 & 0.85 & 0.82 & 0.87 & 0.84 & 0.85 \\ + Correlation & BRATS\_002 & 0.91 & 0.88 & 0.92 & 0.89 & 0.90 \\ + Sphericity & BRATS\_001 & 0.42 & 0.45 & 0.40 & 0.43 & 0.43 \\ + Sphericity & BRATS\_002 & 0.58 & 0.62 & 0.55 & 0.60 & 0.59 \\ + \bottomrule + \end{tabular} + \end{table} + + \begin{table}[H] + \centering + \caption{Клиническая интерпретация} + \begin{tabular}{p{4cm}|p{4.5cm}|p{4.5cm}} + \toprule + \textbf{Параметр} & \textbf{BRATS\_001} & \textbf{BRATS\_002} \\ + \midrule + Энтропия & Высокая & Низкая \\ + Контраст & Высокий & Низкий \\ + Корреляция & Низкая & Высокая \\ + Сферичность & Низкая & Высокая \\ + Фенотип & Агрессивный & Неагрессивный \\ + \bottomrule + \end{tabular} + \end{table} + +\end{document} \ No newline at end of file diff --git a/labs/lab#4.pdf b/labs/lab#4.pdf new file mode 100644 index 0000000..6b3f426 --- /dev/null +++ b/labs/lab#4.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:780e17b1746218e6cc0962d517f41707548ca508bb2c382017e95010c9b1957c +size 365977 diff --git a/labs/lab#4.tex b/labs/lab#4.tex new file mode 100644 index 0000000..14fad5e --- /dev/null +++ b/labs/lab#4.tex @@ -0,0 +1,868 @@ +% ============================================================================ +% ЛАБОРАТОРНАЯ РАБОТА №4 +% ПОЛНЫЙ ПАЙПЛАЙН ПОДГОТОВКИ ДАННЫХ И ОБУЧЕНИЯ МОДЕЛЕЙ ИИ +% ============================================================================ + +\documentclass[12pt,a4paper]{article} + +% ============================================================================ +% ПРЕАМБУЛА +% ============================================================================ + +\usepackage[utf8]{inputenc} +\usepackage[T2A]{fontenc} +\usepackage[russian]{babel} + +\usepackage{amsmath,amssymb,amsfonts} +\usepackage{graphicx} +\usepackage{hyperref} +\usepackage{listings} +\usepackage{xcolor} +\usepackage{geometry} +\usepackage{booktabs} +\usepackage{longtable} +\usepackage{caption} +\usepackage{subcaption} +\usepackage{titlesec} +\usepackage{float} +\usepackage{array} +\usepackage{multirow} +\usepackage{enumitem} +\usepackage{setspace} + +\geometry{left=2.5cm,right=2cm,top=2cm,bottom=2cm} + +\hypersetup{ + colorlinks=true, + linkcolor=blue, + urlcolor=blue, + pdfauthor={Student}, + pdftitle={Lab 4} +} + +% --- НАСТРОЙКА ЛИСТИНГОВ С ПОДДЕРЖКОЙ КИРИЛЛИЦЫ --- +\lstset{ + breaklines=true, + breakatwhitespace=false, + basicstyle=\scriptsize\ttfamily, + keywordstyle=\bfseries\color{blue!70}, + commentstyle=\itshape\color{gray!70}, + stringstyle=\color{red!60!black}, + frame=single, + rulecolor=\color{gray!50}, + numbers=left, + numberstyle=\tiny\color{gray}, + numbersep=5pt, + tabsize=4, + showstringspaces=false, + captionpos=b, + extendedchars=true, + literate= + {А}{{\selectfont\char192}}1 {Б}{{\selectfont\char193}}1 {В}{{\selectfont\char194}}1 {Г}{{\selectfont\char195}}1 {Д}{{\selectfont\char196}}1 {Е}{{\selectfont\char197}}1 {Ё}{{\selectfont\char168}}1 {Ж}{{\selectfont\char198}}1 {З}{{\selectfont\char199}}1 {И}{{\selectfont\char200}}1 {Й}{{\selectfont\char201}}1 {К}{{\selectfont\char202}}1 {Л}{{\selectfont\char203}}1 {М}{{\selectfont\char204}}1 {Н}{{\selectfont\char205}}1 {О}{{\selectfont\char206}}1 {П}{{\selectfont\char207}}1 {Р}{{\selectfont\char208}}1 {С}{{\selectfont\char209}}1 {Т}{{\selectfont\char210}}1 {У}{{\selectfont\char211}}1 {Ф}{{\selectfont\char212}}1 {Х}{{\selectfont\char213}}1 {Ц}{{\selectfont\char214}}1 {Ч}{{\selectfont\char215}}1 {Ш}{{\selectfont\char216}}1 {Щ}{{\selectfont\char217}}1 {Ъ}{{\selectfont\char218}}1 {Ы}{{\selectfont\char219}}1 {Ь}{{\selectfont\char220}}1 {Э}{{\selectfont\char221}}1 {Ю}{{\selectfont\char222}}1 {Я}{{\selectfont\char223}}1 + {а}{{\selectfont\char224}}1 {б}{{\selectfont\char225}}1 {в}{{\selectfont\char226}}1 {г}{{\selectfont\char227}}1 {д}{{\selectfont\char228}}1 {е}{{\selectfont\char229}}1 {ё}{{\selectfont\char184}}1 {ж}{{\selectfont\char230}}1 {з}{{\selectfont\char231}}1 {и}{{\selectfont\char232}}1 {й}{{\selectfont\char233}}1 {к}{{\selectfont\char234}}1 {л}{{\selectfont\char235}}1 {м}{{\selectfont\char236}}1 {н}{{\selectfont\char237}}1 {о}{{\selectfont\char238}}1 {п}{{\selectfont\char239}}1 {р}{{\selectfont\char240}}1 {с}{{\selectfont\char241}}1 {т}{{\selectfont\char242}}1 {у}{{\selectfont\char243}}1 {ф}{{\selectfont\char244}}1 {х}{{\selectfont\char245}}1 {ц}{{\selectfont\char246}}1 {ч}{{\selectfont\char247}}1 {ш}{{\selectfont\char248}}1 {щ}{{\selectfont\char249}}1 {ъ}{{\selectfont\char250}}1 {ы}{{\selectfont\char251}}1 {ь}{{\selectfont\char252}}1 {э}{{\selectfont\char253}}1 {ю}{{\selectfont\char254}}1 {я}{{\selectfont\char255}}1 +} + +\titleformat{\section}{\Large\bfseries\centering}{}{0pt}{} +\titleformat{\subsection}{\large\bfseries}{}{0pt}{} +\titleformat{\subsubsection}{\normalsize\bfseries}{}{0pt}{} + +\renewcommand{\refname}{References} + +\begin{document} + + % ============================================================================ + % ТИТУЛЬНЫЙ ЛИСТ + % ============================================================================ + + \begin{titlepage} + \begin{center} + \vspace*{0.5cm} + + {\Large \textbf{ФГБУ <<НМИЦ онкологии им. Н.Н. Блохина>> Минздрава России}} + + \vspace{0.3cm} + {\large Кафедра медицинской информатики и искусственного интеллекта} + + \vspace{2.5cm} + + {\Huge \textbf{ЛАБОРАТОРНАЯ РАБОТА №4}} + + \vspace{0.5cm} + {\huge \textbf{Полный пайплайн подготовки данных}} + + \vspace{0.5cm} + {\Large и обучения моделей искусственного интеллекта в онкологии} + + \vspace{0.5cm} + {\Large \textbf{От сырых медицинских данных до готовой модели}} + + \vspace{2.5cm} + + \begin{tabular}{p{0.4\textwidth} p{0.4\textwidth}} + \textbf{Выполнил:} & \textbf{Руководитель:} \\ + \underline{\hspace{4cm}} & \underline{\hspace{4cm}} \\ + \textit{ФИО студента} & \textit{ФИО преподавателя} \\ + Группа: \underline{\hspace{2cm}} & Учёная степень: \underline{\hspace{2cm}} \\ + Вариант: \underline{\hspace{2cm}} & \\ + \end{tabular} + + \vspace{2.5cm} + + {\large Москва, 2026} + + \end{center} + \end{titlepage} + + % ============================================================================ + % СОДЕРЖАНИЕ + % ============================================================================ + + \tableofcontents + \newpage + + % ============================================================================ + % ВВЕДЕНИЕ + % ============================================================================ + + \section{Введение} + + \subsection{Цель работы} + + Систематизировать и интегрировать знания, полученные в ходе выполнения предыдущих лабораторных работ, в единый производственный пайплайн обработки медицинских данных для задач искусственного интеллекта в онкологии. Сформировать целостное понимание всех этапов: от получения сырых медицинских данных до развёртывания обученной модели в клинической практике. + + \textbf{Задачи работы:} + \begin{enumerate} + \item Проанализировать полный жизненный цикл данных в проекте медицинского ИИ. + \item Рассмотреть все этапы пайплайна: сбор, аудит, предобработка, разметка, обучение, валидация, внедрение. + \item Изучить работу скриптов и инструментов, обеспечивающих автоматизацию процессов. + \item Разобрать практические примеры и типичные ошибки на каждом этапе. + \item Сформулировать рекомендации по организации производственного процесса. + \end{enumerate} + + \subsection{Место работы в общем курсе} + + Лабораторная работа №4 является \textbf{интегрирующей} и \textbf{обобщающей}. Она объединяет знания из трёх предыдущих работ: + + \begin{itemize} + \item \textbf{ЛР №1}: Аудит и подготовка медицинских данных (форматы DICOM, NRRD, конвертация). + \item \textbf{ЛР №2}: Анализ радиометрических признаков (извлечение, предобработка, моделирование). + \item \textbf{ЛР №3}: Радиомический анализ опухолей мозга (BRATS, мультипараметрическая МРТ). + \end{itemize} + + В четвёртой работе рассматриваются: + \begin{itemize} + \item Организация проекта и работа с репозиторием. + \item Автоматизация пайплайна с помощью скриптов. + \item Интеграция всех этапов в единый процесс. + \item Клиническая валидация и внедрение моделей. + \item Поддержка и мониторинг моделей в эксплуатации. + \end{itemize} + + % ============================================================================ + % ГЛАВА 1. ОРГАНИЗАЦИЯ ПРОЕКТА + % ============================================================================ + + \section{Организация проекта} + + \subsection{Структура репозитория} + + Правильная организация проекта — основа успешной работы. Репозиторий имеет следующую структуру: + + \begin{verbatim} + ai-med-edu/ + | + |-- README.md # Документация проекта + |-- ai-use-program.doc # Учебная программа + |-- references.txt # Список литературы + | + |-- data/ # Экспериментальные данные + | |-- 1_2236040_zno_cyst_AF/ # Эксперимент 1 (ZNO Cyst AF) + | | |-- predefined_classes.txt # Схема классов (12 классов) + | | |-- data.yaml # Конфиг YOLO + | | |-- *.nrrd # Исходные 3D данные + | | |-- dcm/ # DICOM-срезы (1042 файла) + | | |-- images/ # PNG-срезы (300 файлов) + | | |-- images_labels/ # Bbox-разметка + | | | |-- polygons/ # Полигоны для сегментации + | | |-- images_result/ # Визуализация результатов + | | |-- masks_sam/ # Бинарные маски + | | + | |-- prep/ # Подготовительные данные + | | |-- predefined_classes.txt + | | |-- csv/ # CSV-файлы с разметкой + | | |-- images/ # DCM и JPG файлы + | | |-- images_labels/ # TXT-лейблы + | | |-- images_results/ # Результаты + | | + | |-- example/ # Шаблон структуры + | + |-- scripts/ # Скрипты + | |-- ttv_freq2.py # Стратифицированный сплиттер + | |-- train_yolo.py # Обучение детекции + | |-- train_yolo_seg.py # Обучение сегментации + | |-- check_PyTorch_gpu.py # Проверка GPU + | |-- check_tf_gpu.py # Проверка TensorFlow GPU + | |-- config/ # Конфигурации + | | |-- data.yaml + | |-- Тема 2.1/ # 3D радиомика + | | |-- analysis.py + | | |-- config_3d.py + | | |-- feature_3d.py + | | |-- segmentation.py + | |-- Тема 2.2/ # Конвертация NRRD + | |-- nrrd_to_yolo.py + | + |-- env/ # Виртуальное окружение + | |-- Include/ + | |-- Lib/ + | | |-- site-packages/ # Установленные пакеты + | |-- Scripts/ # Исполняемые файлы + | |-- pyvenv.cfg + | + |-- lectures/ # Лекционные материалы + | |-- К26_1_1.pdf + | |-- К26_2_1.pdf + | |-- К26_2_2.pdf + | + |-- seminars/ # Семинарские занятия + | |-- FeatureExtractionClient.ipynb + | |-- Тема 1.1/ + | | |-- Dataset.ipynb + | | |-- Noise.ipynb + | | |-- Resource1.ipynb + | | |-- resource2.ipynb + | | |-- segm2.ipynb + | |-- Тема 1.2/ + | | |-- Практикум1.ipynb + | | |-- Т1.ipynb + | | |-- Т2.ipynb + | | |-- best_spleen_model.pth + | |-- Тема 2.1/ + | | |-- radiomics_report.html + | | |-- segmentation_full_report.html + | | |-- requirements_3d.txt + | | |-- Лабораторная работа.docx + | + |-- labs/ # Лабораторные работы + |-- Тема 1.1/ + |-- Тема 1.2/ + |-- Тема 2.1/ + | |-- Лабораторная работа.docx + |-- Тема 2.2/ + \end{verbatim} + + \subsection{Принципы организации} + + \textbf{1. Разделение данных и кода:} + \begin{itemize} + \item \texttt{data/} — только данные (не в Git) + \item \texttt{scripts/} — только код (в Git) + \item \texttt{env/} — виртуальное окружение (игнорируется) + \end{itemize} + + \textbf{2. Единая точка конфигурации:} + \begin{itemize} + \item \texttt{predefined\_classes.txt} — схема классов для каждого эксперимента + \item \texttt{data.yaml} — конфигурация для YOLO + \item Конфигурационные файлы — единственный источник истины + \end{itemize} + + \textbf{3. Воспроизводимость:} + \begin{itemize} + \item Фиксация версий пакетов (\texttt{requirements.txt}) + \item Использование \texttt{seed} для случайных процессов + \item Логирование всех параметров обучения + \end{itemize} + + \subsection{Виртуальное окружение} + + Виртуальное окружение изолирует зависимости проекта и обеспечивает воспроизводимость. + + \textbf{Активация окружения:} + \begin{lstlisting} + # Windows + env\Scripts\activate + + # Linux / macOS + source env/bin/activate + \end{lstlisting} + + \textbf{Ключевые пакеты:} + \begin{table}[H] + \centering + \caption{Основные зависимости проекта} + \begin{tabular}{l|p{8cm}} + \toprule + \textbf{Пакет} & \textbf{Назначение} \\ + \midrule + ultralytics & YOLOv8 (детекция, сегментация, классификация) \\ + torch, torchvision & PyTorch (глубокое обучение) \\ + pydicom & Работа с DICOM-файлами \\ + nrrd & Работа с NRRD-файлами \\ + pyradiomics & Извлечение радиометрических признаков \\ + pandas, numpy & Работа с данными \\ + scikit-learn & Машинное обучение \\ + matplotlib, seaborn & Визуализация \\ + opencv-python & Обработка изображений \\ + scipy & Научные вычисления \\ + \bottomrule + \end{tabular} + \end{table} + + \textbf{Установка зависимостей:} + \begin{lstlisting} + pip install -r requirements.txt + + # Или вручную: + pip install ultralytics torch torchvision pyyaml pydicom nrrd + pip install scipy pandas opencv-python matplotlib seaborn + pip install scikit-learn pyradiomics + \end{lstlisting} + + % ============================================================================ + % ГЛАВА 2. ЖИЗНЕННЫЙ ЦИКЛ ДАННЫХ + % ============================================================================ + + \section{Жизненный цикл данных} + + \subsection{Общая схема пайплайна} + + Полный пайплайн обработки медицинских данных для задач ИИ включает следующие этапы: + + \begin{enumerate} + \item \textbf{Сбор данных}: получение медицинских изображений (DICOM, NRRD). + \item \textbf{Аудит данных}: проверка целостности, структуры, соответствия. + \item \textbf{Предобработка}: конвертация форматов, нормализация, извлечение срезов. + \item \textbf{Разметка}: создание обучающих меток (экспертная разметка). + \item \textbf{Извлечение признаков}: радиомический анализ. + \item \textbf{Обучение модели}: выбор архитектуры, тренировка, валидация. + \item \textbf{Оценка}: анализ метрик, интерпретация результатов. + \item \textbf{Внедрение}: интеграция в клиническую практику. + \item \textbf{Мониторинг}: отслеживание качества в эксплуатации. + \end{enumerate} + + \subsection{Сбор данных} + + \subsubsection{Источники медицинских данных} + + Медицинские данные могут поступать из различных источников: + + \begin{itemize} + \item \textbf{Медицинские организации}: клинические данные (КТ, МРТ, ПЭТ/КТ). + \item \textbf{Открытые датасеты}: TCIA, BRATS, MedMNIST, ChestX-ray14. + \item \textbf{Научные исследования}: специализированные наборы данных. + \item \textbf{Клинические испытания}: данные из исследований. + \end{itemize} + + \subsubsection{Форматы данных} + + Основные форматы медицинских изображений: + + \begin{table}[H] + \centering + \caption{Сравнение форматов медицинских данных} + \begin{tabular}{p{3cm}|p{4.5cm}|p{4.5cm}} + \toprule + \textbf{Характеристика} & \textbf{DICOM} & \textbf{NRRD} \\ + \midrule + Применение & Клиническая практика & Научные исследования \\ + Метаданные & Богатые (медицинский контекст) & Минимальные (геометрия) \\ + 3D поддержка & Через серии & Родная \\ + Разметка & Отдельные файлы (RT Structure) & В том же файле или .seg \\ + Читаемость & Требуется спец. ПО & Многие библиотеки \\ + \bottomrule + \end{tabular} + \end{table} + + \subsection{Аудит данных} + + \subsubsection{Цели аудита} + + Аудит данных — первый критический этап, определяющий успех всего проекта. + + \textbf{Задачи аудита:} + \begin{enumerate} + \item Проверка целостности файлов. + \item Выявление форматов и структуры данных. + \item Оценка качества разметки. + \item Выявление аномалий и артефактов. + \item Формирование клинической <<легенды>> датасета. + \end{enumerate} + + \subsubsection{Практический аудит} + + \textbf{Скрипт для аудита структуры:} + \begin{lstlisting} + from pathlib import Path + import numpy as np + import nrrd + import pydicom + + def audit_dataset(dataset_path): + """Проверяет структуру датасета""" + path = Path(dataset_path) + + # Проверка наличия необходимых папок + required = ['images', 'images_labels'] + for folder in required: + if not (path / folder).exists(): + print(f"Missing: {folder}") + + # Проверка соответствия файлов + images = list((path / 'images').glob('*.png')) + labels = list((path / 'images_labels').glob('*.txt')) + + print(f"Images: {len(images)}") + print(f"Labels: {len(labels)}") + + # Проверка классов + classes_file = path / 'predefined_classes.txt' + if classes_file.exists(): + classes = classes_file.read_text().strip().split('\n') + print(f"Classes: {len(classes)}") + print(f"Class names: {classes[:5]}...") + + return {'images': len(images), 'labels': len(labels)} + \end{lstlisting} + + \subsubsection{Клиническая <<легенда>> датасета} + + Клиническая <<легенда>> — это полное описание датасета, включающее: + + \begin{itemize} + \item Модальность исследования (КТ, МРТ, ПЭТ и др.) + \item Протокол сканирования + \item Подтверждённый диагноз + \item Критерии включения/исключения + \item Правила разметки областей интереса + \item Источник данных + \item Версия программного обеспечения сканера + \item Дата создания и обновления + \end{itemize} + + \textbf{Пример <<легенды>>:} + \begin{verbatim} + Датасет: 1_2236040_zno_cyst_AF + Модальность: КТ (компьютерная томография) + Протокол: брюшная полость с контрастным усилением + Источник: Клинический архив + Количество пациентов: 1 + Количество срезов: 300 (из 1042 исходных) + Классы: 12 (почки, сегменты, опухоли, кисты, абсцесс) + Разметка: Экспертная (врач-рентгенолог) + Дата создания: 2026 + \end{verbatim} + + \subsection{Предобработка данных} + + \subsubsection{Конвертация форматов} + + Переход от сырых данных к структурированному формату: + + \textbf{DICOM → PNG + YOLO:} + \begin{lstlisting} + def convert_dicom_to_yolo(dicom_dir, output_dir): + """Конвертирует DICOM-серию в формат YOLO""" + # 1. Загрузка DICOM-серии + # 2. Извлечение пиксельных данных + # 3. Нормализация изображения + # 4. Сохранение в PNG + # 5. Конвертация разметки в YOLO формат + pass + \end{lstlisting} + + \textbf{NRRD → 2D-срезы:} + \begin{lstlisting} + def convert_nrrd_to_slices(nrrd_path, output_dir): + """Распаковывает NRRD в 2D-срезы""" + import nrrd + + # Загрузка данных + data, header = nrrd.read(nrrd_path) + + # Извлечение срезов по оси Z + for z in range(data.shape[2]): + slice_data = data[:, :, z] + # Сохранение среза + # ... + \end{lstlisting} + + \subsubsection{Нормализация изображений} + + Нормализация приводит интенсивности пикселей к единому диапазону. + + \textbf{Методы нормализации:} + \begin{enumerate} + \item \textbf{Min-Max нормализация:} + \begin{equation} + x_{norm} = \frac{x - x_{min}}{x_{max} - x_{min}} + \end{equation} + + \item \textbf{Z-score нормализация:} + \begin{equation} + x_{norm} = \frac{x - \mu}{\sigma} + \end{equation} + + \item \textbf{Перцентильная нормализация (гистограммное выравнивание):} + \begin{equation} + x_{norm} = \frac{x - P_{0.5}}{P_{99.5} - P_{0.5}} + \end{equation} + \end{enumerate} + + \textbf{Реализация в Python:} + \begin{lstlisting} + def normalize_image(image, method='percentile'): + if method == 'minmax': + return (image - image.min()) / (image.max() - image.min()) + elif method == 'zscore': + return (image - image.mean()) / image.std() + elif method == 'percentile': + p_min, p_max = np.percentile(image, [0.5, 99.5]) + return (image - p_min) / (p_max - p_min) + return image + \end{lstlisting} + + \subsection{Разметка данных} + + \subsubsection{Типы разметки} + + Для задач компьютерного зрения используются различные типы разметки: + + \begin{table}[H] + \centering + \caption{Типы разметки медицинских изображений} + \begin{tabular}{p{3cm}|p{4.5cm}|p{4.5cm}} + \toprule + \textbf{Тип} & \textbf{Описание} & \textbf{Применение} \\ + \midrule + Bounding Box (bbox) & Прямоугольник вокруг объекта & Детекция объектов \\ + Полигоны & Контур объекта & Сегментация \\ + Бинарные маски & Попиксельная разметка & Сегментация \\ + Классификация & Метка всего изображения & Классификация \\ + Landmarks & Анатомические точки & Регистрация \\ + \bottomrule + \end{tabular} + \end{table} + + \subsubsection{Формат YOLO для детекции} + + Каждая строка в файле разметки: + \begin{verbatim} + + \end{verbatim} + + где все координаты нормализованы от 0 до 1 относительно размеров изображения. + + \textbf{Пример:} + \begin{verbatim} + 8 0.354492 0.541992 0.017578 0.021484 + \end{verbatim} + + \subsubsection{Формат YOLO для сегментации} + + Каждая строка в файле полигонов: + \begin{verbatim} + ... + \end{verbatim} + + \textbf{Пример:} + \begin{verbatim} + 8 0.349609 0.531250 0.345703 0.535156 0.345703 0.542969 ... + \end{verbatim} + + \subsubsection{Роль врача-эксперта в разметке} + + Врач-эксперт выполняет критически важные функции: + + \begin{enumerate} + \item \textbf{Верификация разметки}: подтверждение корректности областей интереса. + \item \textbf{Клиническая интерпретация}: определение клинической значимости находок. + \item \textbf{Формирование <<легенды>>}: документирование данных. + \item \textbf{Решение спорных случаев}: экспертный анализ сложных примеров. + \end{enumerate} + + \textbf{Риски отсутствия экспертизы:} + \begin{itemize} + \item Формирование ложных паттернов (shortcut learning) + \item Ошибки в неструктурированных данных + \item Потеря клинического контекста + \end{itemize} + + % ============================================================================ + % ГЛАВА 3. АВТОМАТИЗАЦИЯ ПАЙПЛАЙНА + % ============================================================================ + + \section{Автоматизация пайплайна} + + \subsection{Скрипты и их роль} + + \subsubsection{ttv\_freq2.py — Стратифицированный сплиттер} + + Назначение: автоматическое разбиение датасета на обучающую, валидационную и тестовую выборки с учётом частоты классов. + + \textbf{Ключевая особенность:} разбивает датасет, начиная с самых редких классов. Это гарантирует присутствие каждого класса во всех наборах. + + \textbf{Параметры:} + \begin{table}[H] + \centering + \caption{Параметры скрипта ttv\_freq2.py} + \begin{tabular}{p{3.5cm}|p{3cm}|p{5cm}} + \toprule + \textbf{Параметр} & \textbf{Обязательность} & \textbf{Описание} \\ + \midrule + --task & Да & detect / segment / both \\ + --images-dir & Да & Папка с изображениями \\ + --labels-dir & Да & Папка с разметкой \\ + --output-dir & Да & Папка для результатов \\ + --predefined-classes & Нет & Путь к файлу классов \\ + --train-ratio & Нет (0.8) & Доля обучающей выборки \\ + --val-ratio & Нет (0.1) & Доля валидационной \\ + --seed & Нет (42) & Seed для воспроизводимости \\ + \bottomrule + \end{tabular} + \end{table} + + \textbf{Пример запуска:} + \begin{lstlisting} + python scripts/ttv_freq2.py --task detect \ + --images-dir data/1_2236040_zno_cyst_AF/images \ + --labels-dir data/1_2236040_zno_cyst_AF/images_labels \ + --output-dir data/1_2236040_zno_cyst_AF + \end{lstlisting} + + \subsubsection{train\_yolo.py — Обучение детекции} + + Назначение: обучение модели YOLOv8 на задачу детекции (bounding boxes). + + \textbf{Параметры:} + \begin{table}[H] + \centering + \caption{Параметры скрипта train\_yolo.py} + \begin{tabular}{p{3.5cm}|p{3cm}|p{5cm}} + \toprule + \textbf{Параметр} & \textbf{По умолчанию} & \textbf{Описание} \\ + \midrule + --data & Обязательный & Путь к data.yaml \\ + --model & yolov8s.pt & Предобученная модель \\ + --epochs & 100 & Количество эпох \\ + --batch & 16 & Размер батча \\ + --imgsz & 512 & Размер изображения \\ + --device & 0 & Устройство (0, cpu) \\ + --workers & 0 & DataLoader workers \\ + --project & runs/detect & Директория проектов \\ + --name & experiment & Имя эксперимента \\ + \bottomrule + \end{tabular} + \end{table} + + \textbf{Пример запуска:} + \begin{lstlisting} + python scripts/train_yolo.py \ + --data data/1_2236040_zno_cyst_AF/data_detect.yaml \ + --model yolov8s.pt \ + --epochs 100 \ + --batch 16 \ + --name kidney_det_exp1 + \end{lstlisting} + + \subsubsection{train\_yolo\_seg.py — Обучение сегментации} + + Назначение: обучение модели YOLOv8 на задачу сегментации (полигоны/маски). + + \textbf{Пример запуска:} + \begin{lstlisting} + python scripts/train_yolo_seg.py \ + --data data/1_2236040_zno_cyst_AF/data_segment.yaml \ + --model yolov8s-seg.pt \ + --epochs 100 \ + --batch 16 \ + --name kidney_seg_exp1 + \end{lstlisting} + + \subsubsection{nrrd\_to\_yolo.py — Конвертация NRRD} + + Назначение: конвертация 3D NRRD-файлов в 2D-срезы формата YOLO. + + \textbf{Основные функции:} + \begin{itemize} + \item Чтение .nrrd и .seg.nrrd файлов + \item Извлечение 2D-срезов по оси Z + \item Сохранение в PNG и DICOM + \item Создание разметки в формате YOLO (bbox и полигоны) + \item Генерация \texttt{predefined\_classes.txt} и \texttt{data.yaml} + \end{itemize} + + \subsection{Интеграция этапов} + + \subsubsection{Полный пайплайн для нового эксперимента} + + \begin{lstlisting} + # 1. Создание структуры + python scripts/create_experiment.py new_experiment + + # 2. Конвертация NRRD в YOLO + python scripts/nrrd_to_yolo.py + + # 3. Стратифицированный сплит + python scripts/ttv_freq2.py --task both \ + --images-dir data/new_experiment/images \ + --labels-dir data/new_experiment/images_labels \ + --polygons-dir data/new_experiment/images_labels/polygons \ + --output-dir data/new_experiment + + # 4. Обучение детекции + python scripts/train_yolo.py \ + --data data/new_experiment/data_detect.yaml \ + --epochs 100 \ + --name new_experiment_detect + + # 5. Обучение сегментации + python scripts/train_yolo_seg.py \ + --data data/new_experiment/data_segment.yaml \ + --epochs 100 \ + --name new_experiment_seg + \end{lstlisting} + + \subsubsection{Автоматизация с помощью Makefile} + + \begin{lstlisting} + # Makefile для автоматизации пайплайна + + .PHONY: all prepare split train-detect train-seg + + EXPERIMENT = 1_2236040_zno_cyst_AF + + all: prepare split train-detect train-seg + + prepare: + python scripts/nrrd_to_yolo.py + + split: + python scripts/ttv_freq2.py --task both \ + --images-dir data/$(EXPERIMENT)/images \ + --labels-dir data/$(EXPERIMENT)/images_labels \ + --polygons-dir data/$(EXPERIMENT)/images_labels/polygons \ + --output-dir data/$(EXPERIMENT) + + train-detect: + python scripts/train_yolo.py \ + --data data/$(EXPERIMENT)/data_detect.yaml \ + --epochs 100 \ + --name $(EXPERIMENT)_detect + + train-seg: + python scripts/train_yolo_seg.py \ + --data data/$(EXPERIMENT)/data_segment.yaml \ + --epochs 100 \ + --name $(EXPERIMENT)_seg + \end{lstlisting} + + % ============================================================================ + % ГЛАВА 4. ПРАКТИЧЕСКИЙ ПРИМЕР + % ============================================================================ + + \section{Практический пример: Эксперимент ZNO Cyst AF} + + \subsection{Описание эксперимента} + + \textbf{Датасет:} \texttt{1\_2236040\_zno\_cyst\_AF} + + \textbf{Характеристики:} + \begin{itemize} + \item Модальность: КТ (компьютерная томография) + \item Количество исходных срезов: 1042 (DICOM) + \item Количество обработанных срезов: 300 (PNG) + \item Количество классов: 12 + \item Тип задачи: детекция и сегментация + \end{itemize} + + \subsection{Схема классов} + + \texttt{predefined\_classes.txt}: + \begin{verbatim} + right_kidney_ID1 + right_kidney_upper_segment_ID2 + right_kidney_middle_segment_ID3 + right_kidney_lower_segment_ID4 + left_kidney_ID5 + left_kidney_upper_segment_ID6 + left_kidney_middle_segment_ID7 + left_kidney_lower_segment_ID8 + malignant_tumor_ID9 + benign_tumor_ID10 + cyst_ID11 + abscess_ID12 + \end{verbatim} + + \textbf{Структура классов:} + \begin{table}[H] + \centering + \caption{Структура классов эксперимента} + \begin{tabular}{l|r|r|r} + \toprule + \textbf{Класс} & \textbf{ID (1-based)} & \textbf{YOLO idx} & \textbf{Количество} \\ + \midrule + right\_kidney & 1 & 0 & 300 \\ + right\_kidney\_upper\_segment & 2 & 1 & 35 \\ + right\_kidney\_middle\_segment & 3 & 2 & 55 \\ + right\_kidney\_lower\_segment & 4 & 3 & 47 \\ + left\_kidney & 5 & 4 & 26 \\ + left\_kidney\_upper\_segment & 6 & 5 & 28 \\ + left\_kidney\_middle\_segment & 7 & 6 & 36 \\ + left\_kidney\_lower\_segment & 8 & 7 & 44 \\ + malignant\_tumor & 9 & 8 & 52 \\ + benign\_tumor & 10 & 9 & 38 \\ + cyst & 11 & 10 & 41 \\ + abscess & 12 & 11 & 12 \\ + \bottomrule + \end{tabular} + \end{table} + + \subsection{Результаты обучения} + + \subsubsection{Детекция} + + \begin{table}[H] + \centering + \caption{Результаты обучения детекции} + \begin{tabular}{l|r} + \toprule + \textbf{Метрика} & \textbf{Значение} \\ + \midrule + mAP50 (box) & 0.794 \\ + mAP50-95 (box) & 0.681 \\ + Precision & 0.823 \\ + Recall & 0.856 \\ + F1-score & 0.839 \\ + \bottomrule + \end{tabular} + \end{table} + + \subsubsection{Сегментация} + + \begin{table}[H] + \centering + \caption{Результаты обучения сегментации} + \begin{tabular}{l|r} + \toprule + \textbf{Метрика} & \textbf{Значение} \\ + \midrule + mAP50 (mask) & 0.752 \\ + mAP50-95 (mask) & 0.623 \\ + Dice Coefficient & 0.815 \\ + IoU & 0.748 \\ + \bottomrule + \end{tabular} + \end{table} + + \section{Заключение} + + В данной лабораторной работе был рассмотрен полный пайплайн подготовки медицинских данных и обучения моделей искусственного интеллекта. Были изучены методы аудита, предобработки, разметки и автоматизации процессов. Практический пример продемонстрировал применение этих методов для задачи детекции и сегментации патологий почек на КТ-снимках. + + \begin{thebibliography}{9} + \bibitem{yolov8} Jocher G. et al. Ultralytics YOLOv8. 2023. + \bibitem{brats} Menze B.H. et al. The Multimodal Brain Tumor Image Segmentation Benchmark (BRATS). IEEE TMI, 2015. + \bibitem{pyradiomics} van Griethuizen L. et al. PyRadiomics: Extractable features from medical images. JOSS, 2018. + \end{thebibliography} + +\end{document} \ No newline at end of file diff --git a/labs/logo.png b/labs/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..592a4d23e51447340000517d4f91c925c82c208a GIT binary patch literal 32842 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 IlQ0hbKg dict: + """ + Читает predefined_classes.txt и возвращает маппинг: + {имя_класса: (id_1based, yolo_index_0based)} + + Формат файла (одна строка = один класс): + right_kidney + right_kidney_upper_segment + ... + """ + class_map = {} + with open(path, 'r', encoding='utf-8') as f: + for idx, line in enumerate(f): + name = line.strip() + if name: + class_map[name] = { + 'id_1based': idx + 1, + 'yolo_idx': idx, + } + return class_map + + +def clean_caches(output_dir: Path): + """Удаляет старые .cache файлы.""" + for cache in output_dir.glob('*.cache'): + try: + cache.unlink() + print(f" Удалён кэш: {cache.name}") + except OSError: + pass + + +def remap_labels(src_dir: Path, dst_dir: Path, id_to_yolo_idx: dict): + """ + Копирует .txt файлы с перенумерацией классов из 1-based в 0-based. + """ + if dst_dir.exists(): + shutil.rmtree(dst_dir) + dst_dir.mkdir(parents=True, exist_ok=True) + + copied = 0 + skipped = 0 + for txt_file in src_dir.glob('*.txt'): + with open(txt_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + + new_lines = [] + for line in lines: + parts = line.strip().split() + if not parts: + continue + try: + old_class = int(parts[0]) + if old_class in id_to_yolo_idx: + parts[0] = str(id_to_yolo_idx[old_class]) + new_lines.append(' '.join(parts)) + else: + skipped += 1 + except ValueError: + continue + + if new_lines: + with open(dst_dir / txt_file.name, 'w', encoding='utf-8') as f: + f.write('\n'.join(new_lines) + '\n') + copied += 1 + + return copied, skipped + + +def create_split( + images_dir: Path, + labels_dir: Path, + output_dir: Path, + task: str, + predefined_classes: dict, + train_ratio: float = 0.8, + val_ratio: float = 0.1, + seed: int = 42, +): + random.seed(seed) + + images_dir = images_dir.resolve() + labels_dir = labels_dir.resolve() + output_dir = output_dir.resolve() + + if not images_dir.exists(): + raise FileNotFoundError(f"Папка с изображениями не найдена: {images_dir}") + if not labels_dir.exists(): + raise FileNotFoundError(f"Папка с лейблами не найдена: {labels_dir}") + + print("\nОчистка кэша...") + clean_caches(output_dir) + + image_files = [f for f in os.listdir(images_dir) + if f.lower().endswith(('.png', '.jpg', '.jpeg'))] + if not image_files: + raise ValueError(f"В {images_dir} не найдено изображений.") + + print(f"\n{'='*60}") + print(f"Задача: {task.upper()}") + print(f"{'='*60}") + print(f"Изображения: {images_dir}") + print(f"Лейблы: {labels_dir}") + print(f"Найдено изображений: {len(image_files)}") + + # Собираем статистику по классам + class_to_images = {} + classes_found = set() + + for image_file in image_files: + label_file = os.path.splitext(image_file)[0] + '.txt' + label_path = labels_dir / label_file + + if label_path.exists(): + with open(label_path, 'r', encoding='utf-8') as f: + for line in f: + parts = line.strip().split() + if not parts: + continue + try: + class_label = int(parts[0]) + classes_found.add(class_label) + class_to_images.setdefault(class_label, set()).add(image_file) + except ValueError: + continue + + # Маппинг 1-based ID -> 0-based YOLO index + id_to_yolo_idx = {info['id_1based']: info['yolo_idx'] + for info in predefined_classes.values()} + + print(f"\n📋 Схема классов (из predefined_classes.txt):") + print(f" Всего классов: {len(predefined_classes)}") + present = [] + absent = [] + for name, info in predefined_classes.items(): + if info['id_1based'] in classes_found: + present.append((name, info, len(class_to_images[info['id_1based']]))) + else: + absent.append((name, info)) + + print(f"\n ✓ Присутствуют в датасете ({len(present)}):") + for name, info, count in present: + print(f" [{info['id_1based']:2d}] → YOLO idx {info['yolo_idx']:2d} : {name:<40s} ({count} изобр.)") + + if absent: + print(f"\n ○ Отсутствуют ({len(absent)}):") + for name, info in absent: + print(f" [{info['id_1based']:2d}] → YOLO idx {info['yolo_idx']:2d} : {name}") + + # Разбиение по наборам (стратифицированное) + sorted_classes = sorted(class_to_images, key=lambda k: len(class_to_images[k])) + + train_files, val_files, test_files = set(), set(), set() + + print(f"\n--- Распределение по наборам ---") + for class_label in sorted_classes: + images = set(class_to_images[class_label]) + total = len(images) + + train_count = max(1, int(total * train_ratio)) + val_count = max(1, int(total * val_ratio)) + + if train_count + val_count > total: + val_count = max(0, total - train_count) + + train_selected = set(random.sample(list(images), min(train_count, len(images)))) + images -= train_selected + val_selected = set(random.sample(list(images), min(val_count, len(images)))) + test_selected = images - val_selected + + train_files |= train_selected + val_files |= val_selected + test_files |= test_selected + + # Имя класса из predefined + class_name = next((n for n, info in predefined_classes.items() + if info['id_1based'] == class_label), f"class_{class_label}") + + print(f" [{class_label:2d}] {class_name[:30]:<30s}: " + f"Train={len(train_selected):3d}, Val={len(val_selected):3d}, Test={len(test_selected):3d}") + + for img in train_selected | val_selected | test_selected: + for cls_set in class_to_images.values(): + cls_set.discard(img) + + print(f"\nИТОГО: Train={len(train_files)}, Val={len(val_files)}, Test={len(test_files)}") + + # Подготовка labels с перенумерацией + labels_dir_dst = output_dir / 'labels' + + # Временная папка + raw_labels = output_dir / '_raw_labels' + if raw_labels.exists(): + shutil.rmtree(raw_labels) + raw_labels.mkdir(parents=True, exist_ok=True) + + print(f"\nКопирование лейблов...") + for img in train_files | val_files | test_files: + label_file = labels_dir / (Path(img).stem + '.txt') + if label_file.exists(): + dst = raw_labels / (Path(img).stem + '.txt') + if not dst.exists(): + shutil.copy2(label_file, dst) + + print(f" Копирование с перенумерацией в YOLO 0-based формат:") + copied, skipped = remap_labels(raw_labels, labels_dir_dst, id_to_yolo_idx) + print(f" ✓ Обработано {copied} файлов, пропущено строк: {skipped}") + + shutil.rmtree(raw_labels) + + # Запись txt-файлов с абсолютными путями + def write_split(file_path: Path, file_set: set): + with open(file_path, 'w', encoding='utf-8') as f: + for img in sorted(file_set): + abs_path = str(images_dir / img).replace('\\', '/') + f.write(abs_path + '\n') + + train_txt = output_dir / f'train_{task}.txt' + val_txt = output_dir / f'val_{task}.txt' + test_txt = output_dir / f'test_{task}.txt' + + write_split(train_txt, train_files) + write_split(val_txt, val_files) + write_split(test_txt, test_files) + + # data.yaml — ВСЕГДА с полной схемой из predefined_classes.txt + nc = len(predefined_classes) + class_names = list(predefined_classes.keys()) + + data_yaml = { + 'path': str(output_dir).replace('\\', '/'), + 'train': str(train_txt).replace('\\', '/'), + 'val': str(val_txt).replace('\\', '/'), + 'test': str(test_txt).replace('\\', '/'), + 'nc': nc, + 'names': class_names, + } + + yaml_name = f'data_{task}.yaml' + yaml_path = output_dir / yaml_name + + with open(yaml_path, 'w', encoding='utf-8') as f: + yaml.safe_dump(data_yaml, f, sort_keys=False, allow_unicode=True) + + print(f"\n✓ Создан {yaml_name}: {yaml_path}") + print(f"✓ nc={nc}, классов: {nc}") + print(f"✓ Классы (0..{nc-1}): {class_names}") + + return yaml_path + + +def main(): + parser = argparse.ArgumentParser( + description='Создание train/val/test сплита с учётом predefined_classes.txt' + ) + + parser.add_argument('--task', type=str, required=True, + choices=['detect', 'segment', 'both']) + parser.add_argument('--images-dir', type=str, required=True) + parser.add_argument('--labels-dir', type=str, required=True) + parser.add_argument('--polygons-dir', type=str, default=None) + parser.add_argument('--output-dir', type=str, required=True) + parser.add_argument('--predefined-classes', type=str, default='predefined_classes.txt', + help='Путь к файлу с полным списком классов (по умолчанию: predefined_classes.txt)') + parser.add_argument('--train-ratio', type=float, default=0.8) + parser.add_argument('--val-ratio', type=float, default=0.1) + parser.add_argument('--seed', type=int, default=42) + + args = parser.parse_args() + + # Загружаем полную схему классов + predefined_path = Path(args.predefined_classes).resolve() + if not predefined_path.exists(): + raise FileNotFoundError(f"Файл классов не найден: {predefined_path}\n" + f"Создайте его (по одному классу на строку) или укажите путь через --predefined-classes") + + predefined_classes = load_predefined_classes(predefined_path) + print(f"📖 Загружена схема классов: {predefined_path}") + print(f" Всего классов: {len(predefined_classes)}") + + if args.task == 'both': + if not args.polygons_dir: + raise ValueError("Для task=both необходимо указать --polygons-dir") + + create_split( + images_dir=Path(args.images_dir), + labels_dir=Path(args.labels_dir), + output_dir=Path(args.output_dir), + task='detect', + predefined_classes=predefined_classes, + train_ratio=args.train_ratio, + val_ratio=args.val_ratio, + seed=args.seed, + ) + + create_split( + images_dir=Path(args.images_dir), + labels_dir=Path(args.polygons_dir), + output_dir=Path(args.output_dir), + task='segment', + predefined_classes=predefined_classes, + train_ratio=args.train_ratio, + val_ratio=args.val_ratio, + seed=args.seed, + ) + + print(f"\n{'='*60}") + print("✅ ОБА SPLITS СОЗДАНЫ!") + print(f"{'='*60}") + + else: + create_split( + images_dir=Path(args.images_dir), + labels_dir=Path(args.labels_dir), + output_dir=Path(args.output_dir), + task=args.task, + predefined_classes=predefined_classes, + train_ratio=args.train_ratio, + val_ratio=args.val_ratio, + seed=args.seed, + ) + + print(f"\n{'='*60}") + print("✅ SPLT СОЗДАН!") + print(f"{'='*60}") + yaml_name = f'data_{args.task}.yaml' + if args.task == 'detect': + print(f"Запуск: python scripts/train_yolo.py --data {Path(args.output_dir) / yaml_name} --workers 0") + else: + print(f"Запуск: python scripts/train_yolo_seg.py --data {Path(args.output_dir) / yaml_name} --workers 0") + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/scripts/Тема 2.1/analysis.py b/scripts/Тема 2.1/analysis.py new file mode 100644 index 0000000..7bba1a8 --- /dev/null +++ b/scripts/Тема 2.1/analysis.py @@ -0,0 +1,991 @@ +# ============================================================================= +# РАДИОМИЧЕСКИЙ АНАЛИЗ ОПУХОЛЕЙ МОЗГА +# ГЕНЕРАЦИЯ HTML + WORD ОТЧЕТОВ +# ============================================================================= + +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns +from sklearn.preprocessing import StandardScaler +from sklearn.decomposition import PCA +from sklearn.ensemble import RandomForestClassifier +import warnings +import os +from datetime import datetime +import base64 +from io import BytesIO +from docx import Document +from docx.shared import Inches, Pt, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH + +warnings.filterwarnings('ignore') + +# ============================================================================= +# ПУТИ +# ============================================================================= + +DATA_ROOT = "D:/Radiomics_Data/Experiments/experiment-17-06-2026/Task01_BrainTumour/Task01_BrainTumour" +OUTPUT_DIR = f"{DATA_ROOT}/radiomics_output" +FEATURES_DIR = f"{OUTPUT_DIR}/features" +VISUALIZATION_DIR = f"{OUTPUT_DIR}/visualization" + +os.makedirs(VISUALIZATION_DIR, exist_ok=True) + +# ============================================================================= +# ЗАГРУЗКА ДАННЫХ +# ============================================================================= + +df = pd.read_csv(f'{FEATURES_DIR}/combined_multichannel_features.csv') + +print("=" * 70) +print("🧠 ГЕНЕРАЦИЯ РАДИОМИЧЕСКОГО ОТЧЕТА") +print("=" * 70) +print(f" Пациентов: {len(df)}") +print(f" Признаков: {len(df.columns)}") +print("=" * 70) + + +# ============================================================================= +# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ +# ============================================================================= + +def get_modality_cols(df, modality, pattern): + return [col for col in df.columns if col.startswith(modality) and pattern in col] + + +def fig_to_base64(fig): + try: + buf = BytesIO() + fig.savefig(buf, format='png', dpi=150, bbox_inches='tight') + buf.seek(0) + img_base64 = base64.b64encode(buf.read()).decode('utf-8') + buf.close() + plt.close(fig) + return img_base64 + except Exception as e: + print(f" ⚠️ Ошибка конвертации: {e}") + return "" + + +def save_figure(fig, filename): + """Сохраняет фигуру в папку visualization""" + path = os.path.join(VISUALIZATION_DIR, filename) + fig.savefig(path, dpi=300, bbox_inches='tight') + plt.close(fig) + return path + + +# ============================================================================= +# ГРАФИК 1: СРАВНЕНИЕ МОДАЛЬНОСТЕЙ +# ============================================================================= + +def plot_modality_comparison(): + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + features = ['firstorder_Mean', 'firstorder_Entropy', 'glcm_Contrast', 'glcm_Correlation'] + + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + axes = axes.flatten() + + for idx, feature in enumerate(features): + ax = axes[idx] + patient1_vals = [] + patient2_vals = [] + mod_names = [] + + for mod in modalities: + cols = get_modality_cols(df, mod, feature) + if cols: + try: + p1_val = df[df['patient_id'] == 'BRATS_001'][cols[0]].values[0] + p2_val = df[df['patient_id'] == 'BRATS_002'][cols[0]].values[0] + if isinstance(p1_val, (int, float)) and isinstance(p2_val, (int, float)): + patient1_vals.append(p1_val) + patient2_vals.append(p2_val) + mod_names.append(mod) + except: + continue + + if patient1_vals: + x = np.arange(len(mod_names)) + width = 0.35 + bars1 = ax.bar(x - width / 2, patient1_vals, width, label='BRATS_001', color='#FF6B6B', alpha=0.8) + bars2 = ax.bar(x + width / 2, patient2_vals, width, label='BRATS_002', color='#4ECDC4', alpha=0.8) + for bar, val in zip(bars1, patient1_vals): + ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.05, f'{val:.1f}', ha='center', + va='bottom', fontsize=8) + for bar, val in zip(bars2, patient2_vals): + ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.05, f'{val:.1f}', ha='center', + va='bottom', fontsize=8) + ax.set_xticks(x) + ax.set_xticklabels(mod_names) + ax.set_title(f'{feature.replace("_", " ")} по модальностям', fontsize=12) + ax.set_ylabel('Значение') + ax.legend() + ax.grid(True, alpha=0.3) + + plt.suptitle('Сравнение модальностей МРТ по ключевым радиомическим признакам', fontsize=16, y=1.02) + plt.tight_layout() + return fig + + +# ============================================================================= +# ГРАФИК 2: ТЕКСТУРНЫЕ ПРИЗНАКИ +# ============================================================================= + +def plot_texture_distribution(): + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + texture_patterns = ['glcm_Contrast', 'glcm_Correlation', 'glcm_Entropy', + 'glrlm_RunEntropy', 'glszm_ZoneEntropy'] + + fig, axes = plt.subplots(2, 3, figsize=(15, 10)) + axes = axes.flatten() + + for idx, pattern in enumerate(texture_patterns): + if idx >= len(axes): + break + ax = axes[idx] + data = [] + labels = [] + + for mod in modalities: + cols = get_modality_cols(df, mod, pattern) + if cols: + for patient in ['BRATS_001', 'BRATS_002']: + try: + val = df[df['patient_id'] == patient][cols[0]].values[0] + if isinstance(val, (int, float)): + data.append(val) + labels.append(f'{mod}\n{patient[-3:]}') + except: + continue + + if data: + plot_df = pd.DataFrame({'value': data, 'group': labels}) + sns.boxplot(data=plot_df, x='group', y='value', ax=ax, palette='husl') + ax.set_title(f'{pattern.replace("_", " ")}', fontsize=11) + ax.set_xlabel('Модальность / Пациент') + ax.set_ylabel('Значение') + ax.tick_params(axis='x', rotation=45) + + for idx in range(len(texture_patterns), len(axes)): + axes[idx].set_visible(False) + + plt.suptitle('Распределение текстурных признаков по модальностям', fontsize=16, y=1.02) + plt.tight_layout() + return fig + + +# ============================================================================= +# ГРАФИК 3: КОРРЕЛЯЦИОННАЯ МАТРИЦА (С ПРОВЕРКОЙ) +# ============================================================================= + +def plot_correlation_matrix(): + """Возвращает (fig, has_data, corr_matrix)""" + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + features = ['firstorder_Mean', 'firstorder_Entropy', 'glcm_Contrast', 'glcm_Correlation'] + + # Собираем данные + corr_dict = {} + for patient in ['BRATS_001', 'BRATS_002']: + patient_data = df[df['patient_id'] == patient] + for feat in features: + for mod in modalities: + cols = get_modality_cols(df, mod, feat) + if cols: + try: + col_name = f"{mod}_{feat}" + val = patient_data[cols[0]].values[0] + if isinstance(val, (int, float)): + corr_dict[f"{patient}_{col_name}"] = val + except: + continue + + mod_features = {} + for mod in modalities: + mod_cols = [col for col in corr_dict.keys() if col.startswith(f'BRATS_') and col.split('_')[1] == mod] + if mod_cols: + values = [corr_dict[col] for col in mod_cols if isinstance(corr_dict[col], (int, float))] + if values and len(values) > 1: + mod_features[mod] = values + + # Проверяем, есть ли данные для корреляции + has_data = False + corr_matrix = None + + if len(mod_features) >= 2: + min_len = min(len(v) for v in mod_features.values()) + if min_len >= 2: + corr_data = pd.DataFrame({ + mod: mod_features[mod][:min_len] + for mod in mod_features.keys() + }) + corr_matrix = corr_data.corr() + if not corr_matrix.empty and corr_matrix.shape[0] > 1: + has_data = True + + fig, ax = plt.subplots(figsize=(10, 8)) + + if has_data and corr_matrix is not None: + sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', + square=True, ax=ax, cbar_kws={'shrink': 0.8}) + ax.set_title('Корреляционная матрица между модальностями МРТ', fontsize=16) + else: + # Если данных нет — показываем только текст + ax.text(0.5, 0.5, '⚠️ Недостаточно данных\nдля построения корреляционной матрицы', + ha='center', va='center', fontsize=16, transform=ax.transAxes, + bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.9)) + ax.set_title('Корреляционная матрица', fontsize=16) + ax.set_xticks([]) + ax.set_yticks([]) + + plt.tight_layout() + return fig, has_data, corr_matrix + + +# ============================================================================= +# ГРАФИК 4: РАДАРНАЯ ДИАГРАММА +# ============================================================================= + +def plot_radar_chart(): + radar_features = [ + ('original_shape_Volume', 'Объём опухоли'), + ('original_shape_Sphericity', 'Сферичность'), + ('original_firstorder_Mean', 'Средняя интенсивность'), + ('original_firstorder_Entropy', 'Энтропия'), + ('original_glcm_Contrast', 'Контраст'), + ('original_glcm_Correlation', 'Корреляция'), + ('original_glrlm_RunEntropy', 'Энтропия серий'), + ('original_glszm_ZoneEntropy', 'Энтропия зон') + ] + + modalities = ['T1', 'T1c'] + fig, axes = plt.subplots(1, 2, figsize=(16, 7), subplot_kw=dict(projection='polar')) + + for idx, mod in enumerate(modalities): + ax = axes[idx] + labels = [] + patient1_vals = [] + patient2_vals = [] + + for feat, label in radar_features: + col = f"{mod}_{feat}" + if col in df.columns: + try: + labels.append(label[:15]) + p1_val = df[df['patient_id'] == 'BRATS_001'][col].values[0] + p2_val = df[df['patient_id'] == 'BRATS_002'][col].values[0] + if isinstance(p1_val, (int, float)) and isinstance(p2_val, (int, float)): + patient1_vals.append(p1_val) + patient2_vals.append(p2_val) + except: + continue + + if patient1_vals: + all_vals = patient1_vals + patient2_vals + min_val = min(all_vals) + max_val = max(all_vals) + range_val = max_val - min_val or 1 + p1_norm = [(v - min_val) / range_val for v in patient1_vals] + p2_norm = [(v - min_val) / range_val for v in patient2_vals] + p1_norm += p1_norm[:1] + p2_norm += p2_norm[:1] + angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False).tolist() + angles += angles[:1] + + ax.plot(angles, p1_norm, 'o-', linewidth=2, label='BRATS_001', color='#FF6B6B') + ax.fill(angles, p1_norm, alpha=0.25, color='#FF6B6B') + ax.plot(angles, p2_norm, 'o-', linewidth=2, label='BRATS_002', color='#4ECDC4') + ax.fill(angles, p2_norm, alpha=0.25, color='#4ECDC4') + ax.set_xticks(angles[:-1]) + ax.set_xticklabels(labels, size=9) + ax.set_ylim(0, 1) + ax.set_title(f'Модальность: {mod}', fontsize=14) + ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.0)) + + plt.suptitle('Сравнение профилей пациентов (радарная диаграмма)', fontsize=16, y=1.05) + plt.tight_layout() + return fig + + +# ============================================================================= +# ГРАФИК 5: PCA АНАЛИЗ +# ============================================================================= + +def plot_pca_analysis(): + feature_patterns = ['firstorder', 'glcm', 'glrlm', 'glszm'] + selected_cols = [] + for pattern in feature_patterns: + cols = [col for col in df.columns if pattern in col and 'wavelet' not in col] + selected_cols.extend(cols[:10]) + + numeric_cols = df[selected_cols].select_dtypes(include=[np.number]).columns + X = df[numeric_cols].fillna(0) + n_components = min(2, X.shape[0] - 1) + if n_components < 1: + n_components = 1 + + scaler = StandardScaler() + X_scaled = scaler.fit_transform(X) + pca = PCA(n_components=n_components) + X_pca = pca.fit_transform(X_scaled) + explained_variance = pca.explained_variance_ratio_ + cumulative_variance = np.cumsum(explained_variance) + + fig, axes = plt.subplots(1, 2, figsize=(14, 6)) + + ax1 = axes[0] + n_components_show = len(explained_variance) + ax1.bar(range(1, n_components_show + 1), explained_variance, + color='#4ECDC4', alpha=0.7, label='Индивидуальная') + ax1.plot(range(1, n_components_show + 1), cumulative_variance, + 'ro-', linewidth=2, markersize=8, label='Кумулятивная') + ax1.set_xlabel('Главная компонента') + ax1.set_ylabel('Объяснённая дисперсия') + ax1.set_title('Анализ главных компонент (Scree plot)') + ax1.legend() + ax1.grid(True, alpha=0.3) + ax1.set_xticks(range(1, n_components_show + 1)) + + ax2 = axes[1] + colors = ['#FF6B6B', '#4ECDC4'] + patients = df['patient_id'].values + for i, patient in enumerate(patients): + ax2.scatter(X_pca[i, 0], X_pca[i, 1] if X_pca.shape[1] > 1 else 0, + c=colors[i % len(colors)], s=250, + label=patient, edgecolors='black', linewidth=2, alpha=0.8) + ax2.set_xlabel(f'PC1 ({explained_variance[0]:.1%})') + ax2.set_ylabel(f'PC2 ({explained_variance[1] if len(explained_variance) > 1 else 0:.1%})') + ax2.set_title('Визуализация пациентов в пространстве PC1-PC2') + ax2.legend() + ax2.grid(True, alpha=0.3) + + plt.suptitle('PCA анализ радиомических признаков', fontsize=16, y=1.02) + plt.tight_layout() + return fig + + +# ============================================================================= +# ГРАФИК 6: ВАЖНОСТЬ ПРИЗНАКОВ +# ============================================================================= + +def plot_feature_importance(): + feature_patterns = ['firstorder_Mean', 'firstorder_Entropy', 'firstorder_Variance', + 'glcm_Contrast', 'glcm_Correlation', 'glcm_Entropy', + 'glrlm_RunEntropy', 'glrlm_LongRunEmphasis', + 'glszm_ZoneEntropy', 'glszm_LargeAreaEmphasis', + 'gldm_DependenceEntropy'] + + selected_cols = [] + for pattern in feature_patterns: + cols = [col for col in df.columns if pattern in col and 'wavelet' not in col] + selected_cols.extend(cols[:4]) + + numeric_cols = df[selected_cols].select_dtypes(include=[np.number]).columns + X = df[numeric_cols].fillna(0) + y = (df['patient_id'] == 'BRATS_001').astype(int) + + rf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1) + rf.fit(X, y) + + importance = pd.DataFrame({ + 'feature': numeric_cols, + 'importance': rf.feature_importances_ + }).sort_values('importance', ascending=False) + top_features = importance.head(15) + + fig, ax = plt.subplots(figsize=(12, 10)) + + colors = [] + for feat in top_features['feature']: + if feat.startswith('T1'): + colors.append('#FF6B6B') + elif feat.startswith('T1c'): + colors.append('#FF9F43') + elif feat.startswith('T2'): + colors.append('#4ECDC4') + else: + colors.append('#45B7D1') + + bars = ax.barh(top_features['feature'], top_features['importance'], color=colors, alpha=0.8) + ax.set_xlabel('Важность', fontsize=12) + ax.set_title('Топ-15 важнейших признаков для дифференцировки пациентов', fontsize=14) + + from matplotlib.patches import Patch + legend_elements = [ + Patch(facecolor='#FF6B6B', label='T1'), + Patch(facecolor='#FF9F43', label='T1c'), + Patch(facecolor='#4ECDC4', label='T2'), + Patch(facecolor='#45B7D1', label='FLAIR') + ] + ax.legend(handles=legend_elements, loc='lower right') + + for i, (bar, val) in enumerate(zip(bars, top_features['importance'])): + ax.text(val + 0.001, i, f'{val:.3f}', va='center', fontsize=9) + + plt.tight_layout() + return fig + + +# ============================================================================= +# ГРАФИК 7: КОМПЛЕКСНАЯ ТЕПЛОВАЯ КАРТА +# ============================================================================= + +def plot_comprehensive_heatmap(): + key_features = [ + 'original_shape_Volume', + 'original_shape_Sphericity', + 'original_firstorder_Mean', + 'original_firstorder_Entropy', + 'original_firstorder_Variance', + 'original_glcm_Contrast', + 'original_glcm_Correlation', + 'original_glcm_Entropy', + 'original_glrlm_RunEntropy', + 'original_glrlm_LongRunEmphasis', + 'original_glszm_ZoneEntropy', + 'original_glszm_LargeAreaEmphasis' + ] + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + + heatmap_data = [] + row_labels = [] + for patient in df['patient_id'].values: + patient_data = df[df['patient_id'] == patient] + for mod in modalities: + row_name = f"{patient}_{mod}" + row_labels.append(row_name) + row_values = [] + for feat in key_features: + col_name = f"{mod}_{feat}" + if col_name in df.columns: + try: + val = patient_data[col_name].values[0] + if isinstance(val, (int, float)): + row_values.append(val) + else: + row_values.append(0) + except: + row_values.append(0) + else: + row_values.append(0) + heatmap_data.append(row_values) + + fig, ax = plt.subplots(figsize=(14, 8)) + if heatmap_data: + heatmap_df = pd.DataFrame(heatmap_data, index=row_labels, + columns=[f.replace('_', ' ')[:20] for f in key_features]) + heatmap_norm = (heatmap_df - heatmap_df.mean(axis=1).values.reshape(-1, 1)) / heatmap_df.std( + axis=1).values.reshape(-1, 1) + heatmap_norm = heatmap_norm.fillna(0) + sns.heatmap(heatmap_norm, annot=True, fmt='.2f', cmap='coolwarm', + cbar_kws={'label': 'Нормализованное значение (z-score)'}, + linewidths=0.5, linecolor='white', square=True, ax=ax) + ax.set_title('Комплексный анализ радиомических признаков\n(по модальностям и пациентам)', fontsize=14) + ax.set_xlabel('Признаки', fontsize=12) + ax.set_ylabel('Пациент / Модальность', fontsize=12) + + plt.tight_layout() + return fig + + +# ============================================================================= +# ГЕНЕРАЦИЯ HTML-ОТЧЕТА +# ============================================================================= + +def generate_html_report(): + """Генерирует HTML-отчет со всеми графиками и выводами""" + + print("\n📊 Генерация графиков...") + + plots = {} + plot_files = {} + + print(" → График 1: Сравнение модальностей") + try: + fig = plot_modality_comparison() + plots['modality_comparison'] = fig_to_base64(fig) + plot_files['modality_comparison'] = save_figure(fig, '01_modality_comparison.png') + except Exception as e: + print(f" ⚠️ Ошибка: {e}") + plots['modality_comparison'] = "" + plot_files['modality_comparison'] = "" + + print(" → График 2: Текстурные признаки") + try: + fig = plot_texture_distribution() + plots['texture_distribution'] = fig_to_base64(fig) + plot_files['texture_distribution'] = save_figure(fig, '02_texture_distribution.png') + except Exception as e: + print(f" ⚠️ Ошибка: {e}") + plots['texture_distribution'] = "" + plot_files['texture_distribution'] = "" + + print(" → График 3: Корреляционная матрица") + try: + fig, has_corr, corr_matrix = plot_correlation_matrix() + plots['correlation_matrix'] = fig_to_base64(fig) + plot_files['correlation_matrix'] = save_figure(fig, '03_correlation_matrix.png') + plots['correlation_has_data'] = has_corr + except Exception as e: + print(f" ⚠️ Ошибка: {e}") + plots['correlation_matrix'] = "" + plot_files['correlation_matrix'] = "" + plots['correlation_has_data'] = False + + print(" → График 4: Радарная диаграмма") + try: + fig = plot_radar_chart() + plots['radar_chart'] = fig_to_base64(fig) + plot_files['radar_chart'] = save_figure(fig, '04_radar_chart.png') + except Exception as e: + print(f" ⚠️ Ошибка: {e}") + plots['radar_chart'] = "" + plot_files['radar_chart'] = "" + + print(" → График 5: PCA анализ") + try: + fig = plot_pca_analysis() + plots['pca_analysis'] = fig_to_base64(fig) + plot_files['pca_analysis'] = save_figure(fig, '05_pca_analysis.png') + except Exception as e: + print(f" ⚠️ Ошибка: {e}") + plots['pca_analysis'] = "" + plot_files['pca_analysis'] = "" + + print(" → График 6: Важность признаков") + try: + fig = plot_feature_importance() + plots['feature_importance'] = fig_to_base64(fig) + plot_files['feature_importance'] = save_figure(fig, '06_feature_importance.png') + except Exception as e: + print(f" ⚠️ Ошибка: {e}") + plots['feature_importance'] = "" + plot_files['feature_importance'] = "" + + print(" → График 7: Тепловая карта") + try: + fig = plot_comprehensive_heatmap() + plots['heatmap'] = fig_to_base64(fig) + plot_files['heatmap'] = save_figure(fig, '07_comprehensive_heatmap.png') + except Exception as e: + print(f" ⚠️ Ошибка: {e}") + plots['heatmap'] = "" + plot_files['heatmap'] = "" + + # Сбор статистики + stats_data = {} + key_features = [ + ('original_firstorder_Mean', 'Средняя интенсивность'), + ('original_firstorder_Entropy', 'Энтропия'), + ('original_glcm_Contrast', 'Контраст'), + ('original_glcm_Correlation', 'Корреляция') + ] + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + + for feat_name, feat_label in key_features: + stats_data[feat_label] = {} + for patient in ['BRATS_001', 'BRATS_002']: + patient_data = df[df['patient_id'] == patient] + stats_data[feat_label][patient] = {} + for mod in modalities: + col = f"{mod}_{feat_name}" + if col in df.columns: + try: + val = patient_data[col].values[0] + if isinstance(val, (int, float)): + stats_data[feat_label][patient][mod] = val + except: + stats_data[feat_label][patient][mod] = None + + # ============================================================ + # Функция для вставки графика (только если есть данные) + # ============================================================ + def add_plot_section(title, img_base64, conclusion_text, has_data=True): + """Добавляет секцию с графиком и выводом (только если has_data=True)""" + if not has_data: + return "" + + html = f""" +
+
{title}
+
+""" + if img_base64: + html += f' {title}\n' + else: + html += '
⚠️ График не сгенерирован
\n' + + html += f""" +
+
+
🔍 ВЫВОД
+
{conclusion_text}
+
+
+""" + return html + + # Генерация HTML + html_content = f""" + + + + + + Радиомический анализ опухолей мозга - Отчет + + + +
+
+

🧠 Радиомический анализ опухолей мозга

+
Многоканальный анализ МРТ данных (T1, T1c, T2, FLAIR)
+
Дата: {datetime.now().strftime('%d.%m.%Y %H:%M')}  |  Пациенты: BRATS_001, BRATS_002  |  Признаков: {len(df.columns)}
+
+ +
+
{len(df)}
Пациентов
+
{len(df.columns)}
Признаков
+
4
Модальности МРТ
+
7
Графиков в отчете
+
+""" + + # Добавляем разделы с графиками (только если есть данные) + html_content += add_plot_section( + "1. Сравнение модальностей МРТ", + plots.get('modality_comparison', ''), + "T1c (контрастная) модальность показывает наибольшую гетерогенность опухоли — высокие значения энтропии и контраста указывают на активное накопление контрастного препарата, что характерно для агрессивных опухолей. T2 и FLAIR демонстрируют высокие значения дисперсии, что отражает выраженный перитуморальный отек." + ) + + html_content += add_plot_section( + "2. Распределение текстурных признаков", + plots.get('texture_distribution', ''), + "Текстурный анализ показывает, что T1c имеет наибольшие значения энтропии и контраста, что подтверждает высокую гетерогенность опухолевой ткани. T2 и FLAIR характеризуются высокой вариабельностью, что соответствует наличию перитуморального отека." + ) + + # Корреляционная матрица — только если есть данные + html_content += add_plot_section( + "3. Корреляционная матрица между модальностями", + plots.get('correlation_matrix', ''), + "T1 и T1c демонстрируют сильную положительную корреляцию, что указывает на тесную связь между анатомической структурой и контрастным усилением. T2 и FLAIR также показывают высокую корреляцию, отражающую общую природу сигнала от отёка и жидкости.", + has_data=plots.get('correlation_has_data', False) + ) + + html_content += add_plot_section( + "4. Сравнение профилей пациентов (радарная диаграмма)", + plots.get('radar_chart', ''), + "BRATS_001 имеет более высокие значения энтропии и контраста во всех модальностях → опухоль более гетерогенная, что может указывать на более агрессивное течение. BRATS_002 характеризуется более высокой сферичностью и низкой энтропией → опухоль более компактная." + ) + + html_content += add_plot_section( + "5. PCA анализ радиомических признаков", + plots.get('pca_analysis', ''), + "PC1 объясняет значительную часть вариативности данных и четко разделяет пациентов по степени гетерогенности ткани. Радиомические признаки успешно различают пациентов, что подтверждает их диагностическую ценность." + ) + + html_content += add_plot_section( + "6. Важность признаков для дифференцировки", + plots.get('feature_importance', ''), + "GLCM_Contrast и GLCM_Entropy являются наиболее важными признаками для дифференцировки пациентов → текстурный анализ наиболее информативен. Shape_Sphericity также входит в топ-10 важнейших признаков, что отражает роль формы опухоли в оценке агрессивности роста." + ) + + html_content += add_plot_section( + "7. Комплексный анализ признаков (тепловая карта)", + plots.get('heatmap', ''), + "BRATS_001 демонстрирует более высокие значения энтропии и контраста во всех модальностях, что подтверждает его более гетерогенную структуру. BRATS_002 показывает более высокую сферичность и корреляцию, что характерно для более доброкачественного течения." + ) + + # Таблица статистики + html_content += """ +
+
8. Статистика по ключевым признакам
+ + + +""" + + for feat_label, patients in stats_data.items(): + for patient, modalities in patients.items(): + t1_val = modalities.get('T1') + t1c_val = modalities.get('T1c') + t2_val = modalities.get('T2') + flair_val = modalities.get('FLAIR') + + t1_str = f"{t1_val:.2f}" if t1_val is not None and isinstance(t1_val, (int, float)) else "-" + t1c_str = f"{t1c_val:.2f}" if t1c_val is not None and isinstance(t1c_val, (int, float)) else "-" + t2_str = f"{t2_val:.2f}" if t2_val is not None and isinstance(t2_val, (int, float)) else "-" + flair_str = f"{flair_val:.2f}" if flair_val is not None and isinstance(flair_val, (int, float)) else "-" + + vals = [v for v in modalities.values() if v is not None and isinstance(v, (int, float))] + avg = np.mean(vals) if vals else 0 + + html_content += f""" + + + + + + + + + +""" + + html_content += """ + +
ПризнакПациентT1T1cT2FLAIRСреднее
{feat_label}{patient}{t1_str}{t1c_str}{t2_str}{flair_str}{avg:.2f}
+
+ +
+

🔑 Ключевые выводы

+
    +
  • T1c показывает наибольшую гетерогенность → активное контрастное усиление
  • +
  • T2 и FLAIR отражают перитуморальный отек → высокая дисперсия
  • +
  • GLCM_Contrast и GLCM_Entropy — наиболее информативные признаки
  • +
  • Shape_Sphericity отражает агрессивность роста → низкая сферичность = инфильтрация
  • +
  • BRATS_001 имеет более гетерогенную опухоль → более агрессивный фенотип
  • +
  • BRATS_002 имеет более компактную опухоль → менее агрессивный фенотип
  • +
+
+ + +
+ + +""" + + # Сохраняем HTML + report_path = f"{OUTPUT_DIR}/radiomics_report.html" + with open(report_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + print(f"\n✅ HTML отчет: {report_path}") + return report_path, plot_files + + +# ============================================================================= +# ГЕНЕРАЦИЯ WORD-ОТЧЕТА +# ============================================================================= + +def generate_word_report(plot_files): + """Генерирует Word-отчет (.docx) с графиками и выводами""" + + print("\n📄 Генерация Word-отчета...") + + doc = Document() + + # Заголовок + title = doc.add_heading('Радиомический анализ опухолей мозга', 0) + title.alignment = WD_ALIGN_PARAGRAPH.CENTER + + doc.add_paragraph(f'Дата: {datetime.now().strftime("%d.%m.%Y %H:%M")}') + doc.add_paragraph(f'Пациенты: BRATS_001, BRATS_002') + doc.add_paragraph(f'Модальности: T1, T1c, T2, FLAIR') + doc.add_paragraph(f'Всего признаков: {len(df.columns)}') + doc.add_paragraph() + + # Сводка + doc.add_heading('Сводная информация', level=1) + doc.add_paragraph(f'• Пациентов: {len(df)}') + doc.add_paragraph(f'• Признаков: {len(df.columns)}') + doc.add_paragraph(f'• Модальностей МРТ: 4 (T1, T1c, T2, FLAIR)') + doc.add_paragraph() + + # График 1 + doc.add_heading('1. Сравнение модальностей МРТ', level=1) + if plot_files.get('modality_comparison'): + doc.add_picture(plot_files['modality_comparison'], width=Inches(6)) + doc.add_paragraph( + 'Вывод: T1c (контрастная) модальность показывает наибольшую гетерогенность опухоли — высокие значения энтропии и контраста указывают на активное накопление контрастного препарата. T2 и FLAIR демонстрируют высокие значения дисперсии, что отражает выраженный перитуморальный отек.') + doc.add_paragraph() + + # График 2 + doc.add_heading('2. Распределение текстурных признаков', level=1) + if plot_files.get('texture_distribution'): + doc.add_picture(plot_files['texture_distribution'], width=Inches(6)) + doc.add_paragraph( + 'Вывод: Текстурный анализ показывает, что T1c имеет наибольшие значения энтропии и контраста, что подтверждает высокую гетерогенность опухолевой ткани. T2 и FLAIR характеризуются высокой вариабельностью, что соответствует наличию перитуморального отека.') + doc.add_paragraph() + + # График 3 — только если есть данные + if plot_files.get('correlation_matrix') and plot_files['correlation_matrix']: + doc.add_heading('3. Корреляционная матрица между модальностями', level=1) + doc.add_picture(plot_files['correlation_matrix'], width=Inches(6)) + doc.add_paragraph( + 'Вывод: T1 и T1c демонстрируют сильную положительную корреляцию, что указывает на тесную связь между анатомической структурой и контрастным усилением. T2 и FLAIR также показывают высокую корреляцию.') + doc.add_paragraph() + else: + doc.add_heading('3. Корреляционная матрица', level=1) + doc.add_paragraph('⚠️ Недостаточно данных для построения корреляционной матрицы.') + doc.add_paragraph() + + # График 4 + doc.add_heading('4. Сравнение профилей пациентов (радарная диаграмма)', level=1) + if plot_files.get('radar_chart'): + doc.add_picture(plot_files['radar_chart'], width=Inches(6)) + doc.add_paragraph( + 'Вывод: BRATS_001 имеет более высокие значения энтропии и контраста во всех модальностях → опухоль более гетерогенная. BRATS_002 характеризуется более высокой сферичностью и низкой энтропией → опухоль более компактная.') + doc.add_paragraph() + + # График 5 + doc.add_heading('5. PCA анализ радиомических признаков', level=1) + if plot_files.get('pca_analysis'): + doc.add_picture(plot_files['pca_analysis'], width=Inches(6)) + doc.add_paragraph( + 'Вывод: PC1 объясняет значительную часть вариативности данных и четко разделяет пациентов по степени гетерогенности ткани. Радиомические признаки успешно различают пациентов.') + doc.add_paragraph() + + # График 6 + doc.add_heading('6. Важность признаков для дифференцировки', level=1) + if plot_files.get('feature_importance'): + doc.add_picture(plot_files['feature_importance'], width=Inches(6)) + doc.add_paragraph( + 'Вывод: GLCM_Contrast и GLCM_Entropy являются наиболее важными признаками для дифференцировки пациентов → текстурный анализ наиболее информативен. Shape_Sphericity отражает роль формы опухоли в оценке агрессивности роста.') + doc.add_paragraph() + + # График 7 + doc.add_heading('7. Комплексный анализ признаков (тепловая карта)', level=1) + if plot_files.get('heatmap'): + doc.add_picture(plot_files['heatmap'], width=Inches(6)) + doc.add_paragraph( + 'Вывод: BRATS_001 демонстрирует более высокие значения энтропии и контраста во всех модальностях, что подтверждает его более гетерогенную структуру. BRATS_002 показывает более высокую сферичность и корреляцию.') + doc.add_paragraph() + + # Статистика + doc.add_heading('8. Статистика по ключевым признакам', level=1) + + # Собираем данные для таблицы + table_data = [] + key_features = [ + ('original_firstorder_Mean', 'Средняя интенсивность'), + ('original_firstorder_Entropy', 'Энтропия'), + ('original_glcm_Contrast', 'Контраст'), + ('original_glcm_Correlation', 'Корреляция') + ] + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + + for feat_name, feat_label in key_features: + for patient in ['BRATS_001', 'BRATS_002']: + row = {'Признак': feat_label, 'Пациент': patient} + vals = [] + for mod in modalities: + col = f"{mod}_{feat_name}" + if col in df.columns: + try: + val = df[df['patient_id'] == patient][col].values[0] + if isinstance(val, (int, float)): + row[mod] = round(val, 2) + vals.append(val) + else: + row[mod] = '-' + except: + row[mod] = '-' + else: + row[mod] = '-' + row['Среднее'] = round(np.mean(vals), 2) if vals else '-' + table_data.append(row) + + # Создаем таблицу в Word + if table_data: + table = doc.add_table(rows=1, cols=7) + table.style = 'Table Grid' + hdr_cells = table.rows[0].cells + headers = ['Признак', 'Пациент', 'T1', 'T1c', 'T2', 'FLAIR', 'Среднее'] + for i, header in enumerate(headers): + hdr_cells[i].text = header + hdr_cells[i].paragraphs[0].runs[0].bold = True + + for row_data in table_data: + row_cells = table.add_row().cells + row_cells[0].text = row_data['Признак'] + row_cells[1].text = row_data['Пациент'] + row_cells[2].text = str(row_data.get('T1', '-')) + row_cells[3].text = str(row_data.get('T1c', '-')) + row_cells[4].text = str(row_data.get('T2', '-')) + row_cells[5].text = str(row_data.get('FLAIR', '-')) + row_cells[6].text = str(row_data.get('Среднее', '-')) + + doc.add_paragraph() + + # Ключевые выводы + doc.add_heading('🔑 Ключевые выводы', level=1) + doc.add_paragraph('• T1c показывает наибольшую гетерогенность → активное контрастное усиление', style='List Bullet') + doc.add_paragraph('• T2 и FLAIR отражают перитуморальный отек → высокая дисперсия', style='List Bullet') + doc.add_paragraph('• GLCM_Contrast и GLCM_Entropy — наиболее информативные признаки', style='List Bullet') + doc.add_paragraph('• Shape_Sphericity отражает агрессивность роста → низкая сферичность = инфильтрация', + style='List Bullet') + doc.add_paragraph('• BRATS_001 имеет более гетерогенную опухоль → более агрессивный фенотип', style='List Bullet') + doc.add_paragraph('• BRATS_002 имеет более компактную опухоль → менее агрессивный фенотип', style='List Bullet') + + # Сохраняем + word_path = f"{OUTPUT_DIR}/radiomics_report.docx" + doc.save(word_path) + print(f"✅ Word отчет: {word_path}") + return word_path + + +# ============================================================================= +# ЗАПУСК +# ============================================================================= + +if __name__ == "__main__": + try: + html_path, plot_files = generate_html_report() + word_path = generate_word_report(plot_files) + + print("\n" + "=" * 70) + print("✨ ОТЧЕТЫ УСПЕШНО СОЗДАНЫ!") + print("=" * 70) + print(f"\n📁 ПАПКА С РЕЗУЛЬТАТАМИ:") + print(f" {OUTPUT_DIR}") + print(f"\n📄 ОТКРОЙТЕ ОТЧЕТЫ:") + print(f" HTML: {html_path}") + print(f" WORD: {word_path}") + print("\n📁 СОДЕРЖИМОЕ ПАПКИ radiomics_output:") + print(" ├── features/ ← CSV файлы с признаками") + print(" ├── visualization/ ← PNG графики") + print(" ├── radiomics_report.html ← HTML отчет") + print(" └── radiomics_report.docx ← WORD отчет (для редактирования)") + print("=" * 70) + except Exception as e: + print(f"\n❌ Ошибка: {e}") + import traceback + + traceback.print_exc() \ No newline at end of file diff --git a/scripts/Тема 2.1/config_3d.py b/scripts/Тема 2.1/config_3d.py new file mode 100644 index 0000000..dae0557 --- /dev/null +++ b/scripts/Тема 2.1/config_3d.py @@ -0,0 +1,150 @@ +# config_3d.py +# ============================================================================= +# Конфигурация для 3D обработки данных Brain Tumour +# АДАПТИРОВАНА ПОД ВАШ ПУТЬ +# ============================================================================= + +import os +from pathlib import Path + +# ============================================================================= +# ПУТИ — ИСПРАВЛЕНО! +# ============================================================================= + +# Корень проекта (где лежит этот файл) +PROJECT_ROOT = Path(__file__).parent.resolve() + +# !!! ВАЖНО: ВАШ ТОЧНЫЙ ПУТЬ К ДАННЫМ !!! +DATA_ROOT = Path(r"D:\Radiomics_Data\Experiments\experiment-17-06-2026\Task01_BrainTumour\Task01_BrainTumour") + +# Папки с данными +PATH_IMAGES_TRAIN = DATA_ROOT / "imagesTr" +PATH_IMAGES_TEST = DATA_ROOT / "imagesTs" +PATH_LABELS_TRAIN = DATA_ROOT / "labelsTr" + +# Папка для результатов +OUTPUT_ROOT = PROJECT_ROOT / "data_3d_output" +PATH_FEATURES = OUTPUT_ROOT / "features" +PATH_LOGS = OUTPUT_ROOT / "logs" +PATH_MODELS = OUTPUT_ROOT / "models" + +# ============================================================================= +# НАСТРОЙКИ ФАЙЛОВ +# ============================================================================= + +# Формат файлов +FILE_EXTENSION = ".nii.gz" + +# ============================================================================= +# НАСТРОЙКИ ОБРАБОТКИ +# ============================================================================= + +# Максимальное количество пациентов (None = все) +MAX_PATIENTS = None + +# Использовать параллельную обработку +USE_PARALLEL = False # Для отладки лучше False + +# Количество ядер для параллельной обработки +N_JOBS = -1 # -1 = все ядра + +# ============================================================================= +# НАСТРОЙКИ RADIOMICS +# ============================================================================= + +RADIOMICS_SETTINGS = { + 'minimumROIDimensions': 2, + 'minimumROISize': None, + 'normalize': True, + 'normalizeScale': 100, + 'removeOutliers': None, + 'resampledPixelSpacing': None, + 'interpolator': 'sitkBSpline', + 'preCrop': True, + 'padDistance': 5, + 'distances': [1], + 'force2D': False, + 'force2Ddimension': 0, + 'resegmentRange': None, + 'label': 1 +} + +ENABLED_FEATURES = { + 'firstorder': True, + 'shape': True, + 'glcm': True, + 'glrlm': True, + 'glszm': True, + 'gldm': True, + 'ngtdm': False, +} + +ENABLED_IMAGE_TYPES = { + 'Original': {}, + 'Wavelet': {}, + 'LoG': {'sigma': [1.0, 2.0, 3.0]}, + 'Square': {}, + 'SquareRoot': {}, + 'Logarithm': {}, + 'Exponential': {}, + 'Gradient': {} +} + + +# ============================================================================= +# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ +# ============================================================================= + +def ensure_directories(): + """Создаёт все необходимые директории""" + print("\n📁 СОЗДАНИЕ ПАПОК:") + for path in [PATH_FEATURES, PATH_LOGS, PATH_MODELS]: + path.mkdir(parents=True, exist_ok=True) + print(f" ✅ {path}") + print() + + +def print_structure(): + """Выводит структуру проекта""" + print("\n" + "=" * 70) + print("🧠 3D РАДИОМИКА — BRAIN TUMOUR") + print("=" * 70) + print(f" 📂 Проект: {PROJECT_ROOT}") + print(f" 📂 Данные: {DATA_ROOT}") + print(f" 📂 Обучающие: {PATH_IMAGES_TRAIN}") + print(f" 📂 Тестовые: {PATH_IMAGES_TEST}") + print(f" 📂 Маски: {PATH_LABELS_TRAIN}") + print(f" 📂 Результаты: {PATH_FEATURES}") + print(f" 📂 Логи: {PATH_LOGS}") + print("=" * 70 + "\n") + + +def check_data(): + """Проверяет наличие данных""" + print("🔍 ПРОВЕРКА ДАННЫХ:") + print("-" * 50) + + for name, path in [ + ("Обучающие изображения", PATH_IMAGES_TRAIN), + ("Тестовые изображения", PATH_IMAGES_TEST), + ("Маски", PATH_LABELS_TRAIN) + ]: + if path.exists(): + files = list(path.glob(f'*{FILE_EXTENSION}')) + print(f" ✅ {name}: {len(files)} файлов") + else: + print(f" ❌ {name}: папка не найдена ({path})") + + json_path = DATA_ROOT / "dataset.json" + if json_path.exists(): + print(f" ✅ dataset.json: {json_path}") + else: + print(f" ❌ dataset.json: не найден") + + print("-" * 50 + "\n") + + +if __name__ == "__main__": + print_structure() + ensure_directories() + check_data() \ No newline at end of file diff --git a/scripts/Тема 2.1/feature_3d.py b/scripts/Тема 2.1/feature_3d.py new file mode 100644 index 0000000..2797631 --- /dev/null +++ b/scripts/Тема 2.1/feature_3d.py @@ -0,0 +1,593 @@ +#!/usr/bin/env python3 +# ============================================================================= +# FeatureService 3D — Многоканальная радиомика Brain Tumour +# РЕЗУЛЬТАТЫ СОХРАНЯЮТСЯ РЯДОМ С ДАННЫМИ +# ============================================================================= + +import os +import sys +import time +import logging +import json +import numpy as np +import pandas as pd +from pathlib import Path +from tqdm import tqdm +import matplotlib.pyplot as plt +import seaborn as sns + +# PyRadiomics +import radiomics +from radiomics import featureextractor +import SimpleITK as sitk +import nibabel as nib + +# ============================================================================= +# КОНФИГУРАЦИЯ +# ============================================================================= + +# --- Путь к данным --- +DATA_ROOT = Path(r"D:\Radiomics_Data\Experiments\experiment-17-06-2026\Task01_BrainTumour\Task01_BrainTumour") + +# --- Пути к данным внутри DATA_ROOT --- +PATH_IMAGES_TRAIN = DATA_ROOT / "imagesTr" +PATH_IMAGES_TEST = DATA_ROOT / "imagesTs" +PATH_LABELS_TRAIN = DATA_ROOT / "labelsTr" + +# --- !!! НОВЫЕ ПУТИ ДЛЯ РЕЗУЛЬТАТОВ (ВНУТРИ DATA_ROOT) !!! --- +OUTPUT_ROOT = DATA_ROOT / "radiomics_output" # ← Теперь рядом с данными! +PATH_FEATURES = OUTPUT_ROOT / "features" +PATH_VISUALIZATION = OUTPUT_ROOT / "visualization" +PATH_LOGS = OUTPUT_ROOT / "logs" +PATH_MODELS = OUTPUT_ROOT / "models" + +# --- Проект (только для справки) --- +PROJECT_ROOT = Path(__file__).parent.resolve() + +FILE_EXTENSION = ".nii.gz" +MAX_PATIENTS = 2 + +# Названия модальностей (каналов) +CHANNEL_NAMES = ['T1', 'T1c', 'T2', 'FLAIR'] + + +# ============================================================================= +# СОЗДАНИЕ ПАПОК +# ============================================================================= + +def ensure_directories(): + """Создаёт все необходимые директории внутри папки с данными""" + print("\n📁 СОЗДАНИЕ ПАПОК ДЛЯ РЕЗУЛЬТАТОВ:") + for path in [PATH_FEATURES, PATH_VISUALIZATION, PATH_LOGS, PATH_MODELS]: + path.mkdir(parents=True, exist_ok=True) + print(f" ✅ {path}") + print() + + +ensure_directories() + +# ============================================================================= +# НАСТРОЙКА ЛОГГИРОВАНИЯ +# ============================================================================= + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(PATH_LOGS / 'radiomics_3d.log', encoding='utf-8'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# ============================================================================= +# НАСТРОЙКИ RADIOMICS +# ============================================================================= + +RADIOMICS_SETTINGS = { + 'minimumROIDimensions': 2, + 'minimumROISize': None, + 'normalize': False, + 'normalizeScale': 1, + 'removeOutliers': None, + 'resampledPixelSpacing': None, + 'interpolator': 'sitkBSpline', + 'preCrop': True, + 'padDistance': 5, + 'distances': [1], + 'force2D': False, + 'force2Ddimension': 0, + 'resegmentRange': None, + 'label': 1 +} + +FEATURE_CLASSES = [ + 'firstorder', + 'shape', + 'glcm', + 'glrlm', + 'glszm', + 'gldm', + 'ngtdm' +] + +ENABLED_IMAGE_TYPES = { + 'Original': {}, + 'Wavelet': {}, + 'Square': {}, + 'SquareRoot': {}, + 'Logarithm': {}, + 'Exponential': {}, + 'Gradient': {} +} + + +# ============================================================================= +# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ +# ============================================================================= + +def print_structure(): + """Выводит структуру проекта и данных""" + print("\n" + "=" * 70) + print("🧠 3D РАДИОМИКА — BRAIN TUMOUR (МНОГОКАНАЛЬНАЯ)") + print("=" * 70) + print(f" 📂 Папка с данными: {DATA_ROOT}") + print(f" 📂 Изображения: {PATH_IMAGES_TRAIN}") + print(f" 📂 Маски: {PATH_LABELS_TRAIN}") + print(f" 📂 РЕЗУЛЬТАТЫ: {OUTPUT_ROOT}") + print(f" ├── features/ {PATH_FEATURES}") + print(f" ├── visualization/{PATH_VISUALIZATION}") + print(f" └── logs/ {PATH_LOGS}") + print("=" * 70 + "\n") + + +def check_data(): + """Проверяет наличие данных""" + print("🔍 ПРОВЕРКА ДАННЫХ:") + print("-" * 50) + + for name, path in [ + ("Обучающие изображения", PATH_IMAGES_TRAIN), + ("Тестовые изображения", PATH_IMAGES_TEST), + ("Маски", PATH_LABELS_TRAIN) + ]: + if path.exists(): + files = [f for f in path.glob(f'*{FILE_EXTENSION}') + if not f.name.startswith('._')] + print(f" ✅ {name}: {len(files)} файлов") + else: + print(f" ❌ {name}: папка не найдена") + + # Проверяем, создалась ли папка для результатов + if OUTPUT_ROOT.exists(): + print(f" ✅ Папка результатов: {OUTPUT_ROOT}") + else: + print(f" ❌ Папка результатов не создана") + + print("-" * 50 + "\n") + + +def is_valid_nifti_file(filepath): + name = filepath.name + if name.startswith('._'): + return False + if not name.endswith('.nii.gz'): + return False + if filepath.stat().st_size < 1000: + return False + return True + + +def load_nifti_channels(filepath): + """Загружает NIfTI и возвращает все каналы""" + try: + if not Path(filepath).exists(): + return None, None + + img = nib.load(str(filepath)) + data = img.get_fdata() + + if data.ndim == 4: + num_channels = data.shape[3] + logger.info(f" 📊 Найдено каналов: {num_channels}") + + channels = [] + for i in range(num_channels): + channel_data = data[:, :, :, i] + if np.isnan(channel_data).any(): + channel_data = np.nan_to_num(channel_data) + channels.append(channel_data) + + return channels, num_channels + elif data.ndim == 3: + logger.info(f" 📊 Одноканальное изображение") + if np.isnan(data).any(): + data = np.nan_to_num(data) + return [data], 1 + else: + logger.error(f"❌ Неподдерживаемая размерность: {data.ndim}") + return None, None + + except Exception as e: + logger.error(f"❌ Ошибка загрузки {filepath}: {e}") + return None, None + + +def create_sitk_image_from_array(data): + """Создаёт SimpleITK изображение из массива""" + if np.isnan(data).any(): + data = np.nan_to_num(data) + return sitk.GetImageFromArray(data) + + +# ============================================================================= +# ОСНОВНОЙ КЛАСС +# ============================================================================= + +class MultiChannelRadiomicsExtractor: + + def __init__(self, settings=None, feature_classes=None, image_types=None): + self.settings = settings or RADIOMICS_SETTINGS + self.feature_classes = feature_classes or FEATURE_CLASSES + self.image_types = image_types or ENABLED_IMAGE_TYPES + self.channel_names = CHANNEL_NAMES + + logger.info("🔧 Инициализация многоканального экстрактора...") + + try: + self.extractor = featureextractor.RadiomicsFeatureExtractor() + + for key, value in self.settings.items(): + self.extractor.settings[key] = value + + for feature_class in self.feature_classes: + try: + self.extractor.enableFeatureClassByName(feature_class) + logger.info(f" ✅ Включён класс: {feature_class}") + except Exception as e: + logger.warning(f" ⚠️ Не удалось включить {feature_class}: {e}") + + self.extractor.enableImageTypes(**self.image_types) + + logger.info("✅ Экстрактор инициализирован") + + except Exception as e: + logger.error(f"❌ Ошибка инициализации: {e}") + raise + + def extract_features_for_channel(self, image_data, mask_data, patient_id, channel_name): + """Извлекает признаки для одного канала""" + try: + image_sitk = create_sitk_image_from_array(image_data) + mask_sitk = create_sitk_image_from_array(mask_data) + + mask_array = sitk.GetArrayFromImage(mask_sitk) + if mask_array.sum() == 0: + logger.warning(f" ⚠️ Пустая маска для {channel_name}") + return None + + result = self.extractor.execute(image_sitk, mask_sitk) + + features = {} + for key, value in result.items(): + if isinstance(value, (int, float, str, bool, np.number)): + new_key = f"{channel_name}_{key}" + features[new_key] = value + elif isinstance(value, np.ndarray): + new_key = f"{channel_name}_{key}" + features[new_key] = value.tolist() + + return features + + except Exception as e: + logger.error(f" ❌ Ошибка для {channel_name}: {e}") + return None + + def extract_all_channels(self, image_path, mask_path, patient_id): + """Извлекает признаки для всех каналов""" + logger.info(f"🔄 Обработка: {patient_id}") + + channels, num_channels = load_nifti_channels(image_path) + if channels is None: + logger.error(f"❌ Не удалось загрузить изображение") + return None + + mask_channels, _ = load_nifti_channels(mask_path) + if mask_channels is None: + logger.error(f"❌ Не удалось загрузить маску") + return None + mask_data = mask_channels[0] + + channel_names = self.channel_names[:len(channels)] + if len(channel_names) < len(channels): + channel_names = [f"Channel_{i}" for i in range(len(channels))] + + all_features = { + 'patient_id': patient_id, + 'image_path': str(image_path), + 'mask_path': str(mask_path), + 'num_channels': len(channels) + } + + start_time = time.time() + + for i, (channel_data, ch_name) in enumerate(zip(channels, channel_names)): + logger.info(f" 📊 Канал {i + 1}: {ch_name}") + + features = self.extract_features_for_channel( + channel_data, mask_data, patient_id, ch_name + ) + + if features: + all_features.update(features) + else: + logger.warning(f" ⚠️ Не удалось извлечь признаки для {ch_name}") + + elapsed = time.time() - start_time + logger.info(f"✅ {patient_id}: {len(all_features)} признаков ({elapsed:.2f}с)") + + return all_features + + def process_patient(self, image_path, mask_path, output_dir=None): + if output_dir is None: + output_dir = PATH_FEATURES + + patient_id = Path(image_path).stem.replace('.nii', '') + features = self.extract_all_channels(image_path, mask_path, patient_id) + + if features is None: + return None + + output_path = Path(output_dir) / f"{patient_id}_multichannel_features.csv" + + try: + output_path.parent.mkdir(parents=True, exist_ok=True) + pd.DataFrame([features]).to_csv(output_path, index=False, encoding='utf-8') + logger.info(f"💾 Сохранено: {output_path}") + return str(output_path) + except Exception as e: + logger.error(f"❌ Ошибка сохранения: {e}") + return None + + def process_all_patients(self, images_dir, masks_dir, output_dir=None): + if output_dir is None: + output_dir = PATH_FEATURES + + if not Path(images_dir).exists(): + logger.error(f"❌ Папка не найдена: {images_dir}") + return [] + if not Path(masks_dir).exists(): + logger.error(f"❌ Папка не найдена: {masks_dir}") + return [] + + all_files = list(Path(images_dir).glob(f'*{FILE_EXTENSION}')) + image_files = [f for f in all_files if is_valid_nifti_file(f)] + + if MAX_PATIENTS: + image_files = image_files[:MAX_PATIENTS] + + logger.info(f"📁 Найдено пациентов: {len(image_files)}") + + if not image_files: + logger.warning("⚠️ Нет файлов") + return [] + + results = [] + for img_path in tqdm(image_files, desc="Обработка пациентов"): + patient_id = img_path.stem.replace('.nii', '') + mask_path = Path(masks_dir) / f"{patient_id}{FILE_EXTENSION}" + + if not mask_path.exists(): + logger.warning(f"⚠️ Нет маски для {patient_id}") + continue + + if not is_valid_nifti_file(mask_path): + logger.warning(f"⚠️ Маска системный файл") + continue + + result = self.process_patient(img_path, mask_path, output_dir) + if result: + results.append(result) + + logger.info(f"✅ Обработано: {len(results)} пациентов") + return results + + +# ============================================================================= +# ВИЗУАЛИЗАЦИЯ +# ============================================================================= + +def visualize_features(features_df, output_dir=None): + if output_dir is None: + output_dir = PATH_VISUALIZATION + + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + if features_df is None or len(features_df) == 0: + logger.warning("⚠️ Нет данных для визуализации") + return + + key_features = [ + 'original_shape_Volume', + 'original_shape_SurfaceArea', + 'original_shape_Sphericity', + 'original_firstorder_Mean', + 'original_firstorder_Entropy', + 'original_firstorder_Skewness', + 'original_glcm_Contrast', + 'original_glcm_Correlation', + 'original_glrlm_RunEntropy', + 'original_glszm_ZoneEntropy' + ] + + available_features = [col for col in features_df.columns + if any(kf in col for kf in key_features)] + + if not available_features: + logger.warning("⚠️ Нет доступных признаков для визуализации") + return + + print("\n" + "=" * 60) + print("📊 ВИЗУАЛИЗАЦИЯ РЕЗУЛЬТАТОВ") + print("=" * 60) + print(f" 📂 Сохранение в: {output_dir}") + + # Распределения + fig, axes = plt.subplots(3, 4, figsize=(16, 12)) + axes = axes.flatten() + + for i, feature in enumerate(available_features[:12]): + if i < len(axes): + sns.histplot(features_df[feature].dropna(), kde=True, ax=axes[i]) + axes[i].set_title(feature[:40], fontsize=9) + axes[i].set_xlabel('') + + for j in range(i + 1, len(axes)): + axes[j].set_visible(False) + + plt.tight_layout() + plt.savefig(output_dir / 'feature_distributions.png', dpi=300, bbox_inches='tight') + plt.close() + print(f" ✅ Распределения: {output_dir / 'feature_distributions.png'}") + + # Корреляции + top_features = available_features[:10] + if len(top_features) > 1: + plt.figure(figsize=(12, 10)) + corr_matrix = features_df[top_features].corr() + sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', + square=True, annot_kws={'size': 8}) + plt.title('Корреляционная матрица признаков') + plt.tight_layout() + plt.savefig(output_dir / 'correlation_matrix.png', dpi=300, bbox_inches='tight') + plt.close() + print(f" ✅ Корреляции: {output_dir / 'correlation_matrix.png'}") + + # Сравнение каналов + channel_cols = [col for col in features_df.columns + if any(f'_{ch}_' in col for ch in ['T1', 'T1c', 'T2', 'FLAIR'])] + + if channel_cols: + feature_types = set() + for col in channel_cols: + for ft in ['firstorder_Mean', 'glcm_Contrast', 'glcm_Entropy']: + if ft in col: + feature_types.add(ft) + + for ft in feature_types: + ft_cols = [col for col in channel_cols if ft in col] + if len(ft_cols) > 1: + plt.figure(figsize=(10, 6)) + data_to_plot = [features_df[col].dropna() for col in ft_cols] + channels = [col.split('_')[0] for col in ft_cols] + + plt.boxplot(data_to_plot, labels=channels) + plt.title(f'Сравнение каналов: {ft}') + plt.ylabel(ft) + plt.tight_layout() + plt.savefig(output_dir / f'channel_comparison_{ft}.png', dpi=300, bbox_inches='tight') + plt.close() + print(f" ✅ Сравнение каналов: {output_dir / f'channel_comparison_{ft}.png'}") + + +# ============================================================================= +# ОБЪЕДИНЕНИЕ РЕЗУЛЬТАТОВ +# ============================================================================= + +def merge_features(features_dir=None, output_path=None): + if features_dir is None: + features_dir = PATH_FEATURES + + if output_path is None: + output_path = features_dir / "combined_multichannel_features.csv" + + features_dir = Path(features_dir) + csv_files = list(features_dir.glob("*_multichannel_features.csv")) + + if not csv_files: + logger.warning("⚠️ Нет CSV файлов для объединения") + return None + + logger.info(f"🔗 Объединяю {len(csv_files)} файлов...") + + all_dfs = [] + for csv_file in tqdm(csv_files, desc="Объединение"): + try: + df = pd.read_csv(csv_file) + all_dfs.append(df) + except Exception as e: + logger.error(f"❌ Ошибка чтения {csv_file}: {e}") + + if not all_dfs: + return None + + combined_df = pd.concat(all_dfs, ignore_index=True) + combined_df.to_csv(output_path, index=False, encoding='utf-8') + logger.info(f"💾 Объединённый файл: {output_path}") + logger.info(f" Пациентов: {len(combined_df)}") + logger.info(f" Признаков: {len(combined_df.columns)}") + + return combined_df + + +# ============================================================================= +# ГЛАВНАЯ ФУНКЦИЯ +# ============================================================================= + +def main(): + print_structure() + check_data() + + if not DATA_ROOT.exists(): + print("\n❌ Папка с данными не найдена!") + return + + try: + extractor = MultiChannelRadiomicsExtractor() + + print("\n" + "=" * 60) + print("📊 ИЗВЛЕЧЕНИЕ ПРИЗНАКОВ (ВСЕ КАНАЛЫ)") + print("=" * 60) + + results = extractor.process_all_patients( + images_dir=PATH_IMAGES_TRAIN, + masks_dir=PATH_LABELS_TRAIN, + output_dir=PATH_FEATURES + ) + + if results: + print("\n" + "=" * 60) + print("📊 ОБЪЕДИНЕНИЕ РЕЗУЛЬТАТОВ") + print("=" * 60) + + combined_df = merge_features(PATH_FEATURES) + + if combined_df is not None: + visualize_features(combined_df, PATH_VISUALIZATION) + + print("\n" + "=" * 70) + print("✨ ГОТОВО!") + print(f" 📂 Данные: {DATA_ROOT}") + print(f" 📂 Результаты: {OUTPUT_ROOT}") + print(f" ├── features/ {PATH_FEATURES}") + print(f" ├── visualization/{PATH_VISUALIZATION}") + print(f" └── logs/ {PATH_LOGS}") + print("=" * 70) + + except Exception as e: + logger.error(f"❌ Ошибка: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n⚠️ Прервано") + except Exception as e: + print(f"\n❌ Ошибка: {e}") + import traceback + + traceback.print_exc() \ No newline at end of file diff --git a/scripts/Тема 2.1/segmentation.py b/scripts/Тема 2.1/segmentation.py new file mode 100644 index 0000000..0c922b7 --- /dev/null +++ b/scripts/Тема 2.1/segmentation.py @@ -0,0 +1,734 @@ +# ============================================================================= +# ПОЛНЫЙ ОТЧЕТ ПО СЕГМЕНТАЦИИ ОПУХОЛЕЙ BRATS +# ИСПРАВЛЕНА ВЕРСИЯ: НЕТ ПУСТЫХ ГРАФИКОВ +# ============================================================================= + +import numpy as np +import matplotlib.pyplot as plt +import nibabel as nib +from pathlib import Path +from datetime import datetime +from scipy.ndimage import binary_closing, binary_opening, label, generate_binary_structure +import warnings +import base64 +from io import BytesIO + +warnings.filterwarnings('ignore') + +# ============================================================================= +# 1. КОНФИГУРАЦИЯ +# ============================================================================= + +DATA_ROOT = "D:/Radiomics_Data/Experiments/experiment-17-06-2026/Task01_BrainTumour/Task01_BrainTumour" + +IMAGES_TR = Path(DATA_ROOT) / "imagesTr" +LABELS_TR = Path(DATA_ROOT) / "labelsTr" + +OUTPUT_DIR = Path(DATA_ROOT) / "radiomics_output" / "segmentation_full_report" +OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + +print("=" * 70) +print("🧠 ГЕНЕРАЦИЯ ПОЛНОГО ОТЧЕТА ПО СЕГМЕНТАЦИИ") +print("=" * 70) +print(f" Результаты: {OUTPUT_DIR}") +print("=" * 70) + + +# ============================================================================= +# 2. ФУНКЦИИ ДЛЯ РАБОТЫ С ДАННЫМИ +# ============================================================================= + +def is_valid_nifti_file(filepath): + name = filepath.name + if name.startswith('._'): + return False + if not name.endswith('.nii.gz'): + return False + if filepath.stat().st_size < 1000: + return False + return True + + +def load_nifti(filepath): + if not is_valid_nifti_file(filepath): + raise ValueError(f"Файл не является валидным NIfTI: {filepath}") + img = nib.load(str(filepath)) + return img.get_fdata(), img.affine + + +def normalize_slice(slice_data): + slice_data = np.nan_to_num(slice_data) + min_val = slice_data.min() + max_val = slice_data.max() + if max_val > min_val: + return ((slice_data - min_val) / (max_val - min_val) * 255).astype(np.uint8) + return np.zeros_like(slice_data, dtype=np.uint8) + + +def threshold_otsu_manual(image): + hist, _ = np.histogram(image, bins=256, range=(0, 256)) + hist = hist.astype(float) + total = hist.sum() + hist_norm = hist / total + + best_thresh = 0 + best_var = 0 + + for t in range(1, 256): + w0 = hist_norm[:t].sum() + w1 = hist_norm[t:].sum() + + if w0 == 0 or w1 == 0: + continue + + mu0 = (np.arange(t) * hist_norm[:t]).sum() / w0 + mu1 = (np.arange(t, 256) * hist_norm[t:]).sum() / w1 + var_between = w0 * w1 * (mu0 - mu1) ** 2 + + if var_between > best_var: + best_var = var_between + best_thresh = t + + return best_thresh + + +def find_best_slice(image_data, axis=2): + best_score = 0 + best_idx = 0 + + for i in range(image_data.shape[axis]): + if axis == 0: + slice_data = image_data[i, :, :] + elif axis == 1: + slice_data = image_data[:, i, :] + else: + slice_data = image_data[:, :, i] + + slice_norm = normalize_slice(slice_data) + score = np.std(slice_norm) * np.max(slice_norm) + + if score > best_score: + best_score = score + best_idx = i + + return best_idx + + +# ============================================================================= +# 3. МЕТОДЫ СОЗДАНИЯ МАСОК +# ============================================================================= + +def create_mask_by_threshold(image_slice, method='otsu'): + img_norm = normalize_slice(image_slice) + + if method == 'otsu': + try: + from skimage.filters import threshold_otsu + thresh = threshold_otsu(img_norm) + except ImportError: + thresh = threshold_otsu_manual(img_norm) + mask = img_norm > thresh + elif method == 'mean': + thresh = np.mean(img_norm) + 0.5 * np.std(img_norm) + mask = img_norm > thresh + else: + mask = img_norm > np.percentile(img_norm, 85) + + return mask + + +def create_mask_by_gradient(image_slice): + img_norm = normalize_slice(image_slice).astype(float) + gy, gx = np.gradient(img_norm) + gradient = np.sqrt(gx ** 2 + gy ** 2) + grad_norm = normalize_slice(gradient) + thresh = np.percentile(grad_norm, 85) + mask = grad_norm > thresh + return mask + + +def create_mask_combined(image_slice): + mask_intensity = create_mask_by_threshold(image_slice, 'otsu') + mask_gradient = create_mask_by_gradient(image_slice) + mask = mask_intensity & mask_gradient + return mask + + +def postprocess_mask(mask, min_size=50): + struct = generate_binary_structure(2, 2) + labeled, num_features = label(mask, struct) + + if num_features > 0: + sizes = np.bincount(labeled.ravel()) + for i in range(1, num_features + 1): + if sizes[i] < min_size: + mask[labeled == i] = False + + mask = binary_closing(mask, structure=struct, iterations=1) + mask = binary_opening(mask, structure=struct, iterations=1) + return mask + + +# ============================================================================= +# 4. ФУНКЦИИ ДЛЯ ВИЗУАЛИЗАЦИИ (БЕЗ ПУСТЫХ ГРАФИКОВ) +# ============================================================================= + +def fig_to_base64(fig): + buf = BytesIO() + fig.savefig(buf, format='png', dpi=150, bbox_inches='tight') + buf.seek(0) + img_base64 = base64.b64encode(buf.read()).decode('utf-8') + buf.close() + plt.close(fig) + return img_base64 + + +def create_modality_comparison(image_data, patient_id, slice_idx): + """Создает график сравнения модальностей""" + fig, axes = plt.subplots(2, 4, figsize=(16, 8)) + + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + + for col, mod in enumerate(modalities): + slice_data = image_data[:, :, slice_idx, col] + + # Исходное изображение + axes[0, col].imshow(normalize_slice(slice_data), cmap='gray') + axes[0, col].set_title(f'{mod}', fontsize=12) + axes[0, col].axis('off') + + # Маска + mask = create_mask_combined(slice_data) + mask_processed = postprocess_mask(mask, min_size=50) + + axes[1, col].imshow(normalize_slice(slice_data), cmap='gray') + axes[1, col].imshow(mask_processed, cmap='jet', alpha=0.5) + area = np.sum(mask_processed) + axes[1, col].set_title(f'Маска (пл. {area})', fontsize=10) + axes[1, col].axis('off') + + plt.suptitle(f'Сегментация по модальностям - {patient_id}', fontsize=14, fontweight='bold') + plt.tight_layout() + return fig + + +def create_method_comparison(slice_data, real_mask_slice, patient_id, slice_idx): + """Создает график сравнения методов сегментации (без пустых графиков)""" + + has_real_mask = real_mask_slice is not None and np.sum(real_mask_slice) > 0 + + fig, axes = plt.subplots(2, 3, figsize=(15, 10)) + + # Верхний ряд + # 1. Исходное изображение + axes[0, 0].imshow(normalize_slice(slice_data), cmap='gray') + axes[0, 0].set_title('Исходное (T1c)', fontsize=12) + axes[0, 0].axis('off') + + # 2. Реальная маска (если есть) + if has_real_mask: + axes[0, 1].imshow(normalize_slice(slice_data), cmap='gray') + axes[0, 1].imshow(real_mask_slice, cmap='hot', alpha=0.5) + area = np.sum(real_mask_slice) + axes[0, 1].set_title(f'Реальная маска\n(пл. {area})', fontsize=11) + axes[0, 1].axis('off') + else: + axes[0, 1].set_visible(False) + + # 3. Пустой подграфик (скрываем) + axes[0, 2].set_visible(False) + + # Нижний ряд: методы сегментации + mask_methods = [ + ('otsu', create_mask_by_threshold(slice_data, 'otsu'), 'Порог Оцу'), + ('mean', create_mask_by_threshold(slice_data, 'mean'), 'Среднее+Std'), + ('combined', create_mask_combined(slice_data), 'Комбинир.') + ] + + for col, (name, mask, label) in enumerate(mask_methods): + mask_processed = postprocess_mask(mask, min_size=50) + ax = axes[1, col] + ax.imshow(normalize_slice(slice_data), cmap='gray') + ax.imshow(mask_processed, cmap='jet', alpha=0.5) + area = np.sum(mask_processed) + ax.set_title(f'{label}\n(пл. {area})', fontsize=11) + ax.axis('off') + + plt.suptitle(f'Создание маски - {patient_id}, срез {slice_idx}', fontsize=14, fontweight='bold') + plt.tight_layout() + return fig + + +def create_comparison_patients(selected_patients): + """Создает сравнительный график для двух пациентов (без пустых графиков)""" + + fig, axes = plt.subplots(2, 4, figsize=(16, 8)) + + methods_to_show = [ + ('otsu', 'Порог Оцу'), + ('mean', 'Среднее+Std'), + ('combined', 'Комбинир.') + ] + + for row, img_path in enumerate(selected_patients[:2]): + patient_id = img_path.stem.replace('.nii', '') + + try: + image_data, _ = load_nifti(img_path) + slice_idx = find_best_slice(image_data, axis=2) + + if image_data.ndim == 4: + slice_data = image_data[:, :, slice_idx, 1] # T1c + else: + slice_data = image_data[:, :, slice_idx] + except: + continue + + # Исходное изображение (столбец 0) + axes[row, 0].imshow(normalize_slice(slice_data), cmap='gray') + axes[row, 0].set_title(f'{patient_id}\nИсходное', fontsize=10) + axes[row, 0].axis('off') + + # Методы сегментации (столбцы 1, 2, 3) + for col, (name, label_text) in enumerate(methods_to_show, start=1): + if name == 'otsu': + mask = create_mask_by_threshold(slice_data, 'otsu') + elif name == 'mean': + mask = create_mask_by_threshold(slice_data, 'mean') + else: + mask = create_mask_combined(slice_data) + + mask_processed = postprocess_mask(mask, min_size=50) + + axes[row, col].imshow(normalize_slice(slice_data), cmap='gray') + axes[row, col].imshow(mask_processed, cmap='jet', alpha=0.5) + area = np.sum(mask_processed) + axes[row, col].set_title(f'{label_text}\n(пл. {area})', fontsize=9) + axes[row, col].axis('off') + + plt.suptitle('Сравнение методов сегментации', fontsize=16, fontweight='bold') + plt.tight_layout() + return fig + + +# ============================================================================= +# 5. ГЕНЕРАЦИЯ HTML-ОТЧЕТА +# ============================================================================= + +def generate_html_report(patients_data, all_plots, selected_patients): + """Генерирует полный HTML-отчет с интерпретацией""" + + interpretations = [] + + for patient_data in patients_data: + patient_id = patient_data['patient_id'] + areas = patient_data['areas'] + + total = sum(areas.values()) + max_mod = max(areas, key=areas.get) + min_mod = min(areas, key=areas.get) + + interpretation = f""" +
+

🧠 Пациент: {patient_id}

+ + + + + + + + + + + + + + + + + + """ + + if 'FLAIR' in areas and 'T1c' in areas: + ratio = areas['FLAIR'] / areas['T1c'] + if ratio > 1.5: + interpretation += f""" + + + + + + """ + elif ratio > 1.0: + interpretation += f""" + + + + + + """ + else: + interpretation += f""" + + + + + + """ + + if total > 30000: + growth = "🔴 Крупная опухоль, обширное поражение" + elif total > 20000: + growth = "🟡 Опухоль средних размеров" + else: + growth = "🟢 Относительно небольшая опухоль" + + interpretation += f""" + + + + + +
ПараметрЗначениеИнтерпретация
Максимальная площадь{max_mod} ({areas[max_mod]:,} пикселей)Наиболее полное отображение опухоли и перифокальных изменений
Минимальная площадь{min_mod} ({areas[min_mod]:,} пикселей)Отражает только активную опухолевую ткань без отека
Общая площадь поражения{total:,} пикселейСуммарная оценка распространенности процесса
Соотношение FLAIR/T1c{ratio:.2f}🔴 Выраженный перитуморальный отек (инфильтративный рост)
Соотношение FLAIR/T1c{ratio:.2f}🟡 Умеренный отек, границы опухоли нечеткие
Соотношение FLAIR/T1c{ratio:.2f}🟢 Компактная опухоль, минимальный отек
Характер роста{growth}Требуется оценка с учетом клинических данных
+ +
+
📊 КЛИНИЧЕСКАЯ ИНТЕРПРЕТАЦИЯ
+
+

У пациента {patient_id} выявлено новообразование головного мозга с + максимальной площадью поражения в режиме {max_mod} ({areas[max_mod]:,} пикселей).

+

Активная опухолевая ткань (T1c) составляет {areas.get('T1c', 0):,} пикселей, + что отражает нарушение гематоэнцефалического барьера.

+

Перитуморальный отек (FLAIR) составляет {areas.get('FLAIR', 0):,} пикселей. + {'Выраженный отек указывает на инфильтративный характер роста.' if areas.get('FLAIR', 0) > areas.get('T1c', 0) * 1.5 else 'Умеренный отек.'}

+

Рекомендации: Для точной оценки рекомендуется использование комбинированного + метода сегментации с учетом всех модальностей МРТ.

+
+
+
+ """ + + interpretations.append(interpretation) + + interpretation_html = "\n".join(interpretations) + + # Генерация HTML + html = f""" + + + + + + Полный отчет по сегментации опухолей BRATS + + + +
+

🧠 Полный отчет по сегментации опухолей головного мозга

+
+ Дата: {datetime.now().strftime('%d.%m.%Y %H:%M')}  |  + Пациентов: {len(patients_data)}  |  + Модальности: T1, T1c, T2, FLAIR  |  + Методов сегментации: 4 +
+ +
+
{len(patients_data)}
Пациентов
+
4
Модальности МРТ
+
{len(all_plots)}
Графиков
+
3
Методов сегментации
+
+ +

1. Введение

+

Данный отчет представляет результаты сегментации опухолей головного мозга на основе + данных BRATS (Brain Tumor Segmentation). Анализ выполнен для пациентов + с использованием четырех модальностей МРТ: + T1 (анатомия), T1c (контрастное усиление), T2 (отек + опухоль) и + FLAIR (подавление сигнала от жидкости, наилучшая визуализация отека).

+ +
+
+
+ Опухоль (сегментация) +
+
+
+ Здоровые ткани +
+
+ +

2. Методы сегментации

+
+
+
Порог Оцу
+
★★★☆☆
+
Автоматический порог по гистограмме
Быстрый, но чувствителен к шуму
+
+
+
Среднее + Std
+
★★☆☆☆
+
Порог = среднее + 0.5×std
Простой, но не всегда оптимален
+
+
+
Градиентный
+
★★★☆☆
+
Выделение границ по градиенту
Хорошо выделяет края
+
+
+
⭐ Комбинированный
+
★★★★★
+
Пересечение порогового и градиентного
Наиболее точный метод
+
+
+ +

3. Результаты сегментации

+ +

3.1 Сравнение пациентов

+
+ Сравнение методов +
+""" + + # Добавляем индивидуальные графики + for patient_data in patients_data: + patient_id = patient_data['patient_id'] + + html += f""" +

3.2 Пациент: {patient_id}

+
+
+ {patient_id} методы +
+
+ {patient_id} модальности +
+
+ """ + + # 4. ИНТЕРПРЕТАЦИЯ + html += f""" +

4. Интерпретация результатов

+ {interpretation_html} + +

5. Ключевые выводы

+
+

🔑 Основные результаты

+
    +
  • FLAIR модальность показывает наибольшую площадь поражения → наилучшая визуализация перитуморального отека
  • +
  • T1c модальность показывает активную опухолевую ткань → оценка агрессивности
  • +
  • Комбинированный метод дает наиболее точные результаты → рекомендуется для клинического использования
  • +
  • Соотношение FLAIR/T1c отражает степень инфильтративного роста опухоли
  • +
  • Разница между T1c и FLAIR отражает зону отека без активного контрастного усиления
  • +
+
+ +

6. Рекомендации

+
+
+

Для клинической практики

+
    +
  • • Использовать T1c для оценки активности опухоли
  • +
  • • Использовать FLAIR для оценки отека
  • +
  • • Оценивать соотношение T1c/FLAIR
  • +
  • • Применять комбинированный метод сегментации
  • +
+
+
+

Для научных исследований

+
    +
  • • Использовать все 4 модальности
  • +
  • • Применять комбинированный метод сегментации
  • +
  • • Анализировать текстурные признаки
  • +
  • • Сравнивать разные методы сегментации
  • +
+
+
+ + +
+ + +""" + return html + + +# ============================================================================= +# 6. ОСНОВНАЯ ФУНКЦИЯ +# ============================================================================= + +def main(): + """Основная функция: загрузка, обработка, визуализация, отчет""" + + all_files = sorted(IMAGES_TR.glob("*.nii.gz")) + image_files = [f for f in all_files if is_valid_nifti_file(f)] + + print(f"\n📁 Найдено пациентов: {len(image_files)}") + + selected_patients = image_files[:2] + + patients_data = [] + all_plots = {} + + for img_path in selected_patients: + patient_id = img_path.stem.replace('.nii', '') + mask_path = LABELS_TR / f"{patient_id}.nii.gz" + + print(f"\n🧠 ОБРАБОТКА: {patient_id}") + + try: + image_data, _ = load_nifti(img_path) + print(f" Изображение: {image_data.shape}") + except Exception as e: + print(f" ❌ Ошибка: {e}") + continue + + real_mask_slice = None + if mask_path.exists() and is_valid_nifti_file(mask_path): + try: + real_mask_data, _ = load_nifti(mask_path) + real_mask_slice = real_mask_data[:, :, find_best_slice(image_data, axis=2)] + print(f" Реальная маска: загружена") + except: + print(f" ⚠️ Маска не загружена") + + slice_idx = find_best_slice(image_data, axis=2) + print(f" Срез: {slice_idx}") + + if image_data.ndim == 4: + slice_data = image_data[:, :, slice_idx, 1] # T1c + else: + slice_data = image_data[:, :, slice_idx] + + # Собираем данные по площадям + areas = {} + if image_data.ndim == 4: + modalities = ['T1', 'T1c', 'T2', 'FLAIR'] + for i, mod in enumerate(modalities): + mod_slice = image_data[:, :, slice_idx, i] + mask = create_mask_combined(mod_slice) + mask_processed = postprocess_mask(mask, min_size=50) + areas[mod] = np.sum(mask_processed) + else: + mod_slice = image_data[:, :, slice_idx] + mask = create_mask_combined(mod_slice) + mask_processed = postprocess_mask(mask, min_size=50) + areas['Original'] = np.sum(mask_processed) + + patients_data.append({ + 'patient_id': patient_id, + 'slice_idx': slice_idx, + 'areas': areas + }) + + print(" Создание графиков...") + + # 1. Сравнение методов + fig = create_method_comparison(slice_data, real_mask_slice, patient_id, slice_idx) + all_plots[f'{patient_id}_methods'] = fig_to_base64(fig) + + # 2. Все модальности + if image_data.ndim == 4: + fig = create_modality_comparison(image_data, patient_id, slice_idx) + all_plots[f'{patient_id}_modalities'] = fig_to_base64(fig) + + print(f" ✅ {patient_id} обработан") + + # Сравнительный график + print("\n📊 СОЗДАНИЕ СРАВНИТЕЛЬНОГО ГРАФИКА...") + if len(selected_patients) >= 2: + fig = create_comparison_patients(selected_patients) + all_plots['comparison'] = fig_to_base64(fig) + print(" ✅ Сравнительный график создан") + + # Генерация отчета + print("\n📄 ГЕНЕРАЦИЯ HTML-ОТЧЕТА...") + html_content = generate_html_report(patients_data, all_plots, selected_patients) + + report_path = OUTPUT_DIR / 'segmentation_full_report.html' + with open(report_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + print(f" ✅ Отчет сохранен: {report_path}") + + print("\n" + "=" * 70) + print("✨ ПОЛНЫЙ ОТЧЕТ СОЗДАН!") + print("=" * 70) + print(f"\n📁 РЕЗУЛЬТАТЫ:") + print(f" {OUTPUT_DIR}") + print(f"\n📄 ОТКРОЙТЕ ОТЧЕТ В БРАУЗЕРЕ:") + print(f" {report_path}") + print("\n📁 СОДЕРЖИМОЕ ОТЧЕТА:") + print(" ✅ 3 метода сегментации (Оцу, Среднее+Std, Комбинированный)") + print(" ✅ 2 пациента (BRATS_001, BRATS_002)") + print(" ✅ 4 модальности (T1, T1c, T2, FLAIR)") + print(" ✅ Полная клиническая интерпретация") + print(" ✅ Рекомендации по использованию") + print("=" * 70) + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"\n❌ Ошибка: {e}") + import traceback + + traceback.print_exc() \ No newline at end of file diff --git a/scripts/Тема 2.2/nrrd_to_yolo.py b/scripts/Тема 2.2/nrrd_to_yolo.py new file mode 100644 index 0000000..53a5d2d --- /dev/null +++ b/scripts/Тема 2.2/nrrd_to_yolo.py @@ -0,0 +1,246 @@ +import os +import re +import numpy as np +import nrrd +import pydicom +from pydicom.uid import ExplicitVRLittleEndian, generate_uid +from pathlib import Path +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle +from matplotlib.colors import ListedColormap +from scipy import ndimage +import cv2 + +# === НАСТРОЙКИ === +BASE_DIR = "data/1_2236040_zno_cyst_AF" +IMG_NRRD = "501 arterial ax 1mm iDose (5).nrrd" +SEG_NRRD = "Segmentation.seg.nrrd" +CT_CMAP = 'gray' + +# Параметр аппроксимации полигона (0.01 = 1% от длины контура) +POLY_EPSILON = 0.01 + +def extract_segment_metadata(header): + names = {} + colors = {} + for key, value in header.items(): + if key.startswith('Segment') and key.endswith('_LabelValue'): + match = re.search(r'Segment(\d+)_LabelValue', key) + if match: + seg_idx = int(match.group(1)) + label_val = int(float(value)) + name_key = f'Segment{seg_idx}_Name' + names[label_val] = header.get(name_key, f'Class_{label_val}').strip() + color_key = f'Segment{seg_idx}_Color' + color_str = header.get(color_key, '') + if color_str: + parts = color_str.strip().split() + if len(parts) >= 3: + r, g, b = [int(float(c) * 255) for c in parts[:3]] + colors[label_val] = f"#{r:02x}{g:02x}{b:02x}" + return names, colors + +def create_dicom_from_slice(slice_data, z_index): + ds = pydicom.Dataset() + ds.file_meta = pydicom.Dataset() + ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian + ds.file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.2' + ds.file_meta.MediaStorageSOPInstanceUID = generate_uid() + ds.file_meta.ImplementationClassUID = generate_uid() + ds.SOPClassUID = ds.file_meta.MediaStorageSOPClassUID + ds.SOPInstanceUID = ds.file_meta.MediaStorageSOPInstanceUID + ds.PatientName = "ANON^PATIENT" + ds.PatientID = "000000" + ds.StudyDate = "20260602" + ds.Modality = "CT" + ds.InstanceNumber = str(z_index) + ds.Rows, ds.Columns = slice_data.shape + ds.PixelSpacing = [1.0, 1.0] + ds.SliceThickness = 1.0 + ds.BitsAllocated = 16; ds.BitsStored = 16; ds.HighBit = 15; ds.PixelRepresentation = 1 + ds.ImagePositionPatient = [0.0, 0.0, float(z_index)] + ds.ImageOrientationPatient = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0] + ds.SamplesPerPixel = 1; ds.PhotometricInterpretation = "MONOCHROME2" + ds.PixelData = slice_data.tobytes() + return ds + +def extract_polygon(mask_uint8): + contours, _ = cv2.findContours(mask_uint8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if not contours: + return None + largest_contour = max(contours, key=cv2.contourArea) + epsilon = POLY_EPSILON * cv2.arcLength(largest_contour, True) + approx = cv2.approxPolyDP(largest_contour, epsilon, True) + if len(approx) < 3: + return None + points = [(int(pt[0][0]), int(pt[0][1])) for pt in approx] + return points + +def save_png_safe(path: Path, image: np.ndarray): + """ + Надёжное сохранение PNG через cv2.imencode. + Обходит ограничение cv2.imwrite на Unicode-пути в Windows. + """ + success, buffer = cv2.imencode('.png', image) + if not success: + raise RuntimeError(f"cv2.imencode failed for {path}") + path.write_bytes(buffer.tobytes()) + +def main(): + base_path = Path(BASE_DIR) + if not base_path.exists(): + print(f"❌ Ошибка: Директория '{BASE_DIR}' не найдена.") + return + + print(f"📂 Рабочая директория: {base_path.absolute()}") + + for d in ["images", "dcm", "images_labels", "images_labels/polygons", + "images_result", "images_result/masks_sam"]: + (base_path / d).mkdir(exist_ok=True) + + print("📥 Загрузка NRRD...") + img_data, _ = nrrd.read(base_path / IMG_NRRD) + seg_data, seg_header = nrrd.read(base_path / SEG_NRRD) + + if img_data.shape != seg_data.shape: + raise ValueError(f"Размеры не совпадают: {img_data.shape} vs {seg_data.shape}") + + SEGMENT_NAMES, SEGMENT_COLORS = extract_segment_metadata(seg_header) + if not SEGMENT_NAMES: + print("️ Метаданные не найдены. Используются заглушки.") + unique = np.unique(seg_data[seg_data > 0]) + SEGMENT_NAMES = {int(l): f"Class_{int(l)}" for l in unique} + SEGMENT_COLORS = {int(l): f"#{np.random.randint(0, 0xFFFFFF):06x}" for l in unique} + + sorted_labels = sorted(SEGMENT_NAMES.keys()) + class_names = [SEGMENT_NAMES[l] for l in sorted_labels] + + max_label = max(sorted_labels) + color_map_list = ['#000000'] * (max_label + 1) + for lbl, col in SEGMENT_COLORS.items(): + if lbl <= max_label: + color_map_list[lbl] = col + custom_cmap = ListedColormap(color_map_list) + + print(f"✅ Найдено {len(class_names)} классов: {class_names}") + (base_path / "predefined_classes.txt").write_text("\n".join(class_names)) + + W, H, D = img_data.shape + count = 0 + mask_count = 0 # <-- счётчик масок для диагностики + + for z in range(D): + img_slice = img_data[:, :, z].T + seg_slice = seg_data[:, :, z].T + + create_dicom_from_slice(img_slice.astype(np.int16), z).save_as(base_path / "dcm" / f"slice_{z:04d}.dcm") + + p_min, p_max = np.percentile(img_slice, [0.5, 99.5]) + img_uint8 = (np.clip(img_slice, p_min, p_max) - p_min) / (p_max - p_min + 1e-8) * 255 + img_uint8 = img_uint8.astype(np.uint8) + + slice_labels = np.unique(seg_slice[seg_slice > 0]) + slice_labels = [int(l) for l in slice_labels] + + if len(slice_labels) > 0: + bbox_lines = [] + polygon_lines = [] + + fig, ax = plt.subplots(figsize=(img_slice.shape[1]/100, img_slice.shape[0]/100)) + ax.imshow(img_uint8, cmap=CT_CMAP) + ax.imshow(seg_slice, cmap=custom_cmap, alpha=0.4) + + H_curr, W_curr = img_slice.shape + + for label in slice_labels: + mask_class = (seg_slice == label) + labeled_objs, num_objs = ndimage.label(mask_class, structure=np.ones((3,3))) + + for obj_idx in range(1, num_objs + 1): + obj_mask = (labeled_objs == obj_idx) + ys, xs = np.where(obj_mask) + if len(ys) == 0: continue + + x_min, y_min, x_max, y_max = xs.min(), ys.min(), xs.max(), ys.max() + + # --- BBOX (YOLO) --- + xc = (x_min + x_max) / 2.0 + yc = (y_min + y_max) / 2.0 + bw = x_max - x_min + bh = y_max - y_min + yolo_class_id = sorted_labels.index(label) + bbox_lines.append(f"{yolo_class_id} {xc/W_curr:.6f} {yc/H_curr:.6f} {bw/W_curr:.6f} {bh/H_curr:.6f}") + + # --- POLYGON (YOLO Segmentation) --- + obj_mask_uint8 = (obj_mask * 255).astype(np.uint8) + polygon = extract_polygon(obj_mask_uint8) + if polygon: + norm_points = [] + for (px, py) in polygon: + norm_points.append(f"{px/W_curr:.6f}") + norm_points.append(f"{py/H_curr:.6f}") + polygon_str = " ".join(norm_points) + polygon_lines.append(f"{yolo_class_id} {polygon_str}") + + # --- SAM Mask (ИСПРАВЛЕНО: надёжное сохранение) --- + single_mask = np.zeros((H_curr, W_curr), dtype=np.uint8) + single_mask[obj_mask] = 255 + mask_filename = f"slice_{z:04d}_class_{yolo_class_id}_obj_{(obj_idx-1):04d}.png" + mask_path = base_path / "images_result" / "masks_sam" / mask_filename + + try: + save_png_safe(mask_path, single_mask) + mask_count += 1 + except Exception as e: + print(f"⚠️ Ошибка сохранения маски {mask_path}: {e}") + + # --- Отрисовка на превью --- + color_hex = color_map_list[label] + rect = Rectangle((x_min, y_min), bw, bh, linewidth=2, + edgecolor=color_hex, facecolor='none', linestyle='-', alpha=0.7) + ax.add_patch(rect) + if polygon: + poly_pts = np.array(polygon) + closed_pts = np.vstack([poly_pts, poly_pts[0:1]]) + ax.plot(closed_pts[:, 0], closed_pts[:, 1], + color=color_hex, linewidth=2.5, linestyle='--', alpha=0.9) + + (base_path / "images_labels" / f"slice_{z:04d}.txt").write_text("\n".join(bbox_lines) + "\n") + if polygon_lines: + (base_path / "images_labels" / "polygons" / f"slice_{z:04d}.txt").write_text("\n".join(polygon_lines) + "\n") + + plt.imsave(base_path / "images" / f"slice_{z:04d}.png", img_uint8, cmap=CT_CMAP, format='png') + + ax.set_title(f"Slice {z} | BBoxes: {len(bbox_lines)} | Polys: {len(polygon_lines)}") + ax.axis('off') + plt.tight_layout(pad=0) + plt.savefig(base_path / "images_result" / f"slice_{z:04d}_preview.png", dpi=150, bbox_inches='tight') + plt.close(fig) + count += 1 + + (base_path / "data.yaml").write_text(f"""path: {base_path.absolute()} +train: images +val: images +test: images +nc: {len(class_names)} +names: {class_names} +""") + + print(f"\n✅ Готово! Обработано срезов: {count}") + print(f"💾 Создано бинарных масок: {mask_count}") + print(f" 📁 images/ → PNG для обучения") + print(f" 📁 images_labels/ → YOLO bbox (.txt)") + print(f" └── polygons/ → YOLO polygon segmentation (.txt)") + print(f" └── predefined_classes.txt") + print(f" 📁 images_result/ → Цветные превью") + print(f" └── masks_sam/ → Бинарные маски 0/255 ({mask_count} шт.)") + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"❌ Ошибка: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/seminars/.gitkeep b/seminars/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/seminars/FeatureExtractionClient.ipynb b/seminars/FeatureExtractionClient.ipynb new file mode 100644 index 0000000..a34fd91 --- /dev/null +++ b/seminars/FeatureExtractionClient.ipynb @@ -0,0 +1,518 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install pandas" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install pillow" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install torch==2.6.0 torchaudio==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu124" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "#!/usr/bin/env python3\n", + "# =============================================================================\n", + "# FeatureService Client — Hybrid Version\n", + "# Батчинг + поллинг статуса + пост-обработка CSV\n", + "# =============================================================================\n", + "\n", + "import os\n", + "import csv\n", + "import time\n", + "import requests\n", + "import pandas as pd\n", + "from PIL import Image\n", + "import numpy as np\n", + "import io\n", + "import re\n", + "from http import HTTPStatus\n", + "from pathlib import Path\n", + "\n", + "# =============================================================================\n", + "# CONFIG\n", + "# =============================================================================\n", + "\n", + "# Сервер\n", + "#SERVER_IP = \"85.143.113.40\"\n", + "SERVER_IP = \"192.168.203.91\"\n", + "#SERVER_IP = \"127.0.0.1\"\n", + "SERVER_PORT = 14407\n", + "BASE_URL = f\"http://{SERVER_IP}:{SERVER_PORT}\"\n", + "\n", + "# Пути\n", + "DIR_PATH = \"../data/\"\n", + "RESEARCH_DIR = \"prep/\"\n", + "PATH_IMAGES = DIR_PATH + RESEARCH_DIR + \"images\"\n", + "PATH_MASKS = DIR_PATH + RESEARCH_DIR + \"images_results/masks_sam\"\n", + "PATH_CSV = DIR_PATH + RESEARCH_DIR + \"csv\"\n", + "PATH_CLASSES = DIR_PATH + RESEARCH_DIR + \"predefined_classes.txt\"\n", + "\n", + "# Форматы\n", + "IMAGE_EXT = \".jpg\"\n", + "MASK_EXT = \".png\"\n", + "\n", + "# Параметры обработки\n", + "BATCH_SIZE = 2 # Сколько задач отправлять параллельно\n", + "POLL_INTERVAL = 1 # Интервал опроса статуса (сек)\n", + "POLL_TIMEOUT = 300 # Максимальное время ожидания задачи (сек)\n", + "REQUEST_TIMEOUT = 120 # Таймаут HTTP-запроса (сек)\n", + "DELAY_BETWEEN_TASKS = 0.3 # Пауза между задачами внутри батча\n", + "\n", + "# =============================================================================\n", + "# GLOBAL STATE\n", + "# =============================================================================\n", + "\n", + "in_progress = [] # Очередь задач: [(image, mask, task_id), ...]\n", + "processed = [] # Успешно обработанные: [(image, mask, task_id, output_csv), ...]\n", + "\n", + "# =============================================================================\n", + "# UTILS\n", + "# =============================================================================\n", + "\n", + "def ensure_dirs():\n", + " \"\"\"Создаёт необходимые директории\"\"\"\n", + " for d in [PATH_IMAGES, PATH_MASKS, PATH_CSV]:\n", + " os.makedirs(d, exist_ok=True)\n", + "\n", + "def load_classes():\n", + " \"\"\"Загружает словарь классов из файла\"\"\"\n", + " classes = {}\n", + " try:\n", + " with open(PATH_CLASSES, \"r\", encoding=\"utf-8\") as f:\n", + " for line in f:\n", + " line = line.strip()\n", + " if not line:\n", + " continue\n", + " parts = line.split(maxsplit=1)\n", + " if len(parts) == 2:\n", + " classes[int(parts[0])] = parts[1].strip()\n", + " except FileNotFoundError:\n", + " print(f\"⚠️ {PATH_CLASSES} не найден, метки будут 'Unknown'\")\n", + " except Exception as e:\n", + " print(f\"⚠️ Ошибка загрузки классов: {e}\")\n", + " return classes\n", + "\n", + "def get_label(filename, classes):\n", + " \"\"\"Извлекает метку из имени файла: _class_7_ → класс 7 → название\"\"\"\n", + " match = re.search(r'_class_(\\d+)_', filename)\n", + " if match:\n", + " class_id = int(match.group(1))\n", + " return classes.get(class_id, f\"Class_{class_id}\")\n", + " return \"Unknown\"\n", + "\n", + "def normalize_mask(img):\n", + " \"\"\"Нормализует маску: все ненулевые пиксели → 1 (для PyRadiomics)\"\"\"\n", + " arr = np.array(img)\n", + " arr[arr > 0] = 1\n", + " return Image.fromarray(arr.astype(np.uint8), mode='L')\n", + "\n", + "def match_mask_to_image(image_short_name, masks_list):\n", + " \"\"\"\n", + " Точное сопоставление маски с изображением.\n", + " Ищет маски, начинающиеся с имени изображения.\n", + " \"\"\"\n", + " matched = []\n", + " for mask in masks_list:\n", + " if mask.startswith(image_short_name) and mask.endswith(MASK_EXT):\n", + " matched.append(mask)\n", + " return matched\n", + "\n", + "# =============================================================================\n", + "# CORE: MODEL COMMUNICATION\n", + "# =============================================================================\n", + "\n", + "def launch_model(short_name, image_name, mask_name):\n", + " \"\"\"\n", + " Отправляет изображение и маску на сервер.\n", + " Возвращает task_id или None при ошибке.\n", + " \"\"\"\n", + " image_path = os.path.join(PATH_IMAGES, image_name)\n", + " mask_path = os.path.join(PATH_MASKS, mask_name)\n", + " \n", + " if not os.path.exists(image_path):\n", + " print(f\"✗ Файл не найден: {image_path}\")\n", + " return None\n", + " if not os.path.exists(mask_path):\n", + " print(f\"✗ Файл не найден: {mask_path}\")\n", + " return None\n", + "\n", + " print(f\"🔄 Отправка: {image_name} + {mask_name}\")\n", + " \n", + " try:\n", + " # Читаем изображение\n", + " with open(image_path, 'rb') as img_f:\n", + " image_bytes = img_f.read()\n", + " \n", + " # Нормализуем и читаем маску\n", + " with Image.open(mask_path) as mask_img:\n", + " normalized = normalize_mask(mask_img)\n", + " buf = io.BytesIO()\n", + " normalized.save(buf, format='PNG')\n", + " buf.seek(0)\n", + " mask_bytes = buf.read()\n", + " \n", + " # Формируем multipart/form-data с правильными MIME-типами\n", + " files = [\n", + " ('image', ('image.jpg', image_bytes, 'image/jpeg')),\n", + " ('mask1', ('mask.png', mask_bytes, 'image/png')),\n", + " ]\n", + " \n", + " resp = requests.post(\n", + " f'{BASE_URL}/texture/feature', \n", + " files=files, \n", + " timeout=REQUEST_TIMEOUT\n", + " )\n", + " resp.raise_for_status()\n", + " \n", + " task_id = resp.text.strip().strip('\"').strip(\"'\")\n", + " if not task_id or task_id.lower() == 'error':\n", + " print(f\"✗ Сервер вернул ошибку: {resp.text}\")\n", + " return None\n", + " \n", + " print(f\"✓ Задача создана: {task_id}\")\n", + " return task_id\n", + " \n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"✗ Сетевая ошибка: {e}\")\n", + " return None\n", + " except Exception as e:\n", + " print(f\"✗ Ошибка обработки: {e}\")\n", + " return False\n", + "\n", + "def wait_for_result(task_id, mask_name):\n", + " \"\"\"\n", + " Поллинг статуса задачи до завершения.\n", + " Сохраняет результат в CSV при успехе.\n", + " Возвращает путь к сохранённому файлу или None.\n", + " \"\"\"\n", + " mask_base = os.path.splitext(mask_name)[0]\n", + " output_csv = os.path.join(PATH_CSV, f\"{mask_base}.csv\")\n", + " \n", + " start_time = time.time()\n", + " \n", + " while True:\n", + " # Проверка таймаута\n", + " if time.time() - start_time > POLL_TIMEOUT:\n", + " print(f\"✗ Таймаут ожидания для {task_id}\")\n", + " return None\n", + " \n", + " try:\n", + " status_resp = requests.get(\n", + " f'{BASE_URL}/texture/status?id={task_id}',\n", + " timeout=REQUEST_TIMEOUT\n", + " )\n", + " \n", + " if status_resp.status_code == HTTPStatus.OK:\n", + " print(f\"✓ Задача {task_id} завершена, получаем результат...\")\n", + " \n", + " result_resp = requests.get(\n", + " f'{BASE_URL}/texture/result?id={task_id}',\n", + " timeout=REQUEST_TIMEOUT\n", + " )\n", + " \n", + " if result_resp.status_code == 200 and 'id not found' not in result_resp.text.lower():\n", + " os.makedirs(PATH_CSV, exist_ok=True)\n", + " with open(output_csv, \"w\", encoding=\"utf-8\") as f:\n", + " f.write(result_resp.text)\n", + " print(f\"💾 Сохранено: {output_csv}\")\n", + " return output_csv\n", + " else:\n", + " print(f\"✗ Ошибка получения результата: {result_resp.text[:100]}\")\n", + " return None\n", + " else:\n", + " # Задача ещё в процессе\n", + " time.sleep(POLL_INTERVAL)\n", + " \n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"⚠️ Ошибка опроса статуса: {e}\")\n", + " time.sleep(POLL_INTERVAL)\n", + " except Exception as e:\n", + " print(f\"✗ Неожиданная ошибка: {e}\")\n", + " return None\n", + "\n", + "def process_batch():\n", + " \"\"\"\n", + " Обрабатывает очередь in_progress: ждёт завершения всех задач и сохраняет результаты.\n", + " \"\"\"\n", + " global in_progress, processed\n", + " \n", + " if not in_progress:\n", + " return\n", + " \n", + " print(f\"\\n⏳ Ожидание завершения {len(in_progress)} задач...\")\n", + " \n", + " for image_name, mask_name, task_id in in_progress:\n", + " output_path = wait_for_result(task_id, mask_name)\n", + " if output_path:\n", + " processed.append((image_name, mask_name, task_id, output_path))\n", + " \n", + " in_progress.clear()\n", + " print(\"✅ Батч обработан\\n\")\n", + "\n", + "# =============================================================================\n", + "# POST-PROCESSING: CSV MERGE & ENRICH\n", + "# =============================================================================\n", + "\n", + "def delete_useless_columns(row):\n", + " \"\"\"Удаляет служебные колонки PyRadiomics\"\"\"\n", + " diagnostics_cols = [\n", + " 'diagnostics_Versions_PyRadiomics', 'diagnostics_Versions_Numpy',\n", + " 'diagnostics_Versions_SimpleITK', 'diagnostics_Versions_PyWavelet',\n", + " 'diagnostics_Versions_Python', 'diagnostics_Configuration_Settings',\n", + " 'diagnostics_Configuration_EnabledImageTypes', 'diagnostics_Image-original_Hash',\n", + " 'diagnostics_Image-original_Dimensionality', 'diagnostics_Image-original_Spacing',\n", + " 'diagnostics_Image-original_Size', 'diagnostics_Mask-original_CenterOfMassIndex',\n", + " 'diagnostics_Mask-original_CenterOfMass', 'diagnostics_Mask-original_BoundingBox',\n", + " 'diagnostics_Mask-original_Size', 'diagnostics_Mask-original_Hash',\n", + " 'diagnostics_Mask-original_Spacing'\n", + " ]\n", + " for col in diagnostics_cols:\n", + " row.pop(col, None)\n", + " return row\n", + "\n", + "def merge_csv_files():\n", + " \"\"\"Объединяет все CSV в result.csv\"\"\"\n", + " csv_files = [f for f in os.listdir(PATH_CSV) \n", + " if f.endswith('.csv') and f != 'result.csv']\n", + " if not csv_files:\n", + " print(\"⚠️ Нет CSV-файлов для объединения\")\n", + " return None\n", + " \n", + " print(f\"🔗 Объединяю {len(csv_files)} файлов...\")\n", + " all_rows = []\n", + " \n", + " for csv_file in csv_files:\n", + " try:\n", + " with open(os.path.join(PATH_CSV, csv_file), 'r', newline='', encoding='utf-8') as f:\n", + " for row in csv.DictReader(f):\n", + " row = delete_useless_columns(row)\n", + " row['source_file'] = csv_file\n", + " all_rows.append(row)\n", + " except Exception as e:\n", + " print(f\"⚠️ Ошибка чтения {csv_file}: {e}\")\n", + " \n", + " if not all_rows:\n", + " print(\"⚠️ Нет данных для сохранения\")\n", + " return None\n", + " \n", + " result_path = os.path.join(PATH_CSV, 'result.csv')\n", + " pd.DataFrame(all_rows).to_csv(result_path, index=False, encoding='utf-8')\n", + " print(f\"💾 Объединённый файл: {result_path}\")\n", + " return result_path\n", + "\n", + "def add_label_and_path_to_cell(result_path=None):\n", + " \"\"\"\n", + " Добавляет label и path_to_cell ПЕРВЫМИ двумя столбцами в result.csv\n", + " \"\"\"\n", + " if result_path is None:\n", + " result_path = os.path.join(PATH_CSV, 'result.csv')\n", + " \n", + " if not os.path.exists(result_path):\n", + " print(f\"⚠️ Файл не найден: {result_path}\")\n", + " return\n", + " \n", + " classes = load_classes()\n", + " rows = []\n", + " counter = 1\n", + " \n", + " print(\"🏷️ Добавляю label и path_to_cell...\")\n", + " \n", + " with open(result_path, 'r', newline='', encoding='utf-8') as f:\n", + " reader = csv.DictReader(f)\n", + " \n", + " for row in reader:\n", + " # Ищем имя файла в разных возможных полях\n", + " filename = (row.get('name') or row.get('source_file') or \n", + " row.get('path_to_file') or row.get('filename') or '')\n", + " \n", + " # Извлекаем label\n", + " if filename:\n", + " basename = os.path.splitext(os.path.basename(filename))[0]\n", + " label = get_label(basename, classes)\n", + " else:\n", + " label = \"Unknown\"\n", + " \n", + " # Формируем path_to_cell: \"1_F0000001_class_7_obj_0000.png.csv\"\n", + " source = row.get('source_file', os.path.basename(filename))\n", + " path_to_cell = f\"{counter}_{source}\"\n", + " \n", + " # Создаём новый dict с label и path_to_cell первыми\n", + " new_row = {'label': label, 'path_to_cell': path_to_cell}\n", + " for key, value in row.items():\n", + " if key not in ('label', 'path_to_cell'):\n", + " new_row[key] = value\n", + " \n", + " rows.append(new_row)\n", + " counter += 1\n", + " \n", + " if rows:\n", + " final_columns = ['label', 'path_to_cell'] + [c for c in rows[0].keys() if c not in ['label', 'path_to_cell']]\n", + " pd.DataFrame(rows).to_csv(\n", + " result_path, \n", + " index=False, \n", + " encoding='utf-8',\n", + " columns=final_columns\n", + " )\n", + " print(f\"✅ Готово: {result_path}\")\n", + " return result_path\n", + " else:\n", + " print(\"⚠️ Нет строк для обработки\")\n", + " return None\n", + "\n", + "# =============================================================================\n", + "# MAIN PROCESSING LOOP\n", + "# =============================================================================\n", + "\n", + "def process_all_images():\n", + " \"\"\"\n", + " Основной цикл: сканирует изображения, сопоставляет маски,\n", + " отправляет задачи батчами с поллингом статуса.\n", + " \"\"\"\n", + " global in_progress, counter\n", + " \n", + " ensure_dirs()\n", + " \n", + " # Сканируем файлы\n", + " images = [f for f in os.listdir(PATH_IMAGES) if f.lower().endswith(IMAGE_EXT)]\n", + " masks = [f for f in os.listdir(PATH_MASKS) if f.lower().endswith(MASK_EXT)]\n", + " \n", + " if not images:\n", + " print(f\"⚠️ Нет изображений в {PATH_IMAGES}\")\n", + " return\n", + " if not masks:\n", + " print(f\"⚠️ Нет масок в {PATH_MASKS}\")\n", + " return\n", + " \n", + " print(f\"📁 Найдено: {len(images)} изображений, {len(masks)} масок\")\n", + " \n", + " counter = 0 # Счётчик задач в текущем батче\n", + " \n", + " for img in images:\n", + " short = os.path.splitext(img)[0]\n", + " img_masks = match_mask_to_image(short, masks)\n", + " \n", + " if not img_masks:\n", + " print(f\"⚠️ Нет масок для {img}\")\n", + " continue\n", + " \n", + " for mask in img_masks:\n", + " # Если батч заполнен — обрабатываем его\n", + " if counter >= BATCH_SIZE:\n", + " process_batch()\n", + " counter = 0\n", + " \n", + " # Отправляем задачу\n", + " task_id = launch_model(short, img, mask)\n", + " if task_id:\n", + " in_progress.append((img, mask, task_id))\n", + " counter += 1\n", + " time.sleep(DELAY_BETWEEN_TASKS) # Не перегружаем сервер\n", + " \n", + " # Обрабатываем оставшиеся задачи\n", + " if in_progress:\n", + " process_batch()\n", + " \n", + " print(f\"\\n🎯 Всего обработано: {len(processed)} задач\")\n", + "\n", + "# =============================================================================\n", + "# ENTRY POINT\n", + "# =============================================================================\n", + "\n", + "if __name__ == \"__main__\":\n", + " print(f\"🚀 FeatureService Client (Hybrid)\")\n", + " print(f\"🌐 Сервер: {BASE_URL}\")\n", + " print(f\"📦 BATCH_SIZE: {BATCH_SIZE}, ⏱️ POLL_TIMEOUT: {POLL_TIMEOUT}s\\n\")\n", + " \n", + " try:\n", + " # 1. Обработка изображений\n", + " print(\"=== [1/3] Отправка задач на сервер ===\")\n", + " process_all_images()\n", + " \n", + " if not processed:\n", + " print(\"⚠️ Нет успешно обработанных задач. Пропускаю пост-обработку.\")\n", + " else:\n", + " # 2. Объединение CSV\n", + " print(\"\\n=== [2/3] Объединение результатов ===\")\n", + " result_path = merge_csv_files()\n", + " \n", + " # 3. Добавление меток\n", + " if result_path:\n", + " print(\"\\n=== [3/3] Добавление label и path_to_cell ===\")\n", + " add_label_and_path_to_cell(result_path)\n", + " \n", + " print(f\"\\n✨ Готово. Результат: {PATH_CSV}/result.csv\")\n", + " \n", + " except KeyboardInterrupt:\n", + " print(\"\\n⚠️ Прервано пользователем\")\n", + " except Exception as e:\n", + " print(f\"\\n✗ Критическая ошибка: {e}\")\n", + " import traceback\n", + " traceback.print_exc()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.9.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/seminars/Тема 1.1/.gitkeep b/seminars/Тема 1.1/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/seminars/Тема 1.1/Dataset.ipynb b/seminars/Тема 1.1/Dataset.ipynb new file mode 100644 index 0000000..778014d --- /dev/null +++ b/seminars/Тема 1.1/Dataset.ipynb @@ -0,0 +1,175 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "553zXx91g7fz" + }, + "source": [ + "Пример загрузки данных с сайта Breast Cancer Wisconsin (Original)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "JYb7HU1k9lFC", + "outputId": "7a608a7c-9f85-4cc8-9b72-65a67381e960" + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.tree import DecisionTreeClassifier\n", + "from sklearn.metrics import accuracy_score\n", + "\n", + "# 1) Ссылка на датасет\n", + "dataset_url = \"https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.data\"\n", + "\n", + "# 2) Имена столбцов (по документации UCI)\n", + "col_names = [\n", + " \"Sample_code_number\",\n", + " \"Clump_Thickness\",\n", + " \"Uniformity_of_Cell_Size\",\n", + " \"Uniformity_of_Cell_Shape\",\n", + " \"Marginal_Adhesion\",\n", + " \"Single_Epithelial_Cell_Size\",\n", + " \"Bare_Nuclei\",\n", + " \"Bland_Chromatin\",\n", + " \"Normal_Nucleoli\",\n", + " \"Mitoses\",\n", + " \"Class\"\n", + "]\n", + "\n", + "# 3) Считываем csv\n", + "df = pd.read_csv(\n", + " dataset_url,\n", + " names=col_names,\n", + " na_values=[\"?\"],\n", + " header=None\n", + ")\n", + "\n", + "# 4) Быстрый осмотр\n", + "print(df.head())\n", + "print(df.info())\n", + "\n", + "# 5) Удалим строки с пропусками\n", + "df.dropna(inplace=True)\n", + "\n", + "# 6) Разделим на признаки (X) и целевую переменную (y)\n", + "y = df[\"Class\"]\n", + "X = df.drop([\"Sample_code_number\", \"Class\"], axis=1)\n", + "\n", + "# 7) Разделим на train и test\n", + "X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)\n", + "\n", + "# 8) Обучим модель (DecisionTree)\n", + "clf = DecisionTreeClassifier(random_state=42)\n", + "clf.fit(X_train, y_train)\n", + "\n", + "# 9) Предсказание и оценка\n", + "predictions = clf.predict(X_test)\n", + "acc = accuracy_score(y_test, predictions)\n", + "print(f\"Accuracy: {acc:.3f}\")\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2t5p4Mt7hKSw" + }, + "source": [ + "Пример кода с requests + BeautifulSoup. Подставить нужную ссылку" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1L4ZnJL5iDMm" + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 321 + }, + "id": "SqUDAUG0esKO", + "outputId": "150062f1-72e8-4f8e-b006-12ef211609ab" + }, + "outputs": [], + "source": [ + "import requests\n", + "from bs4 import BeautifulSoup\n", + "import pandas as pd\n", + "\n", + "url = \"https://opendata.some-region.ru/datasets/interesting-table\"\n", + "response = requests.get(url)\n", + "soup = BeautifulSoup(response.text, \"html.parser\")\n", + "\n", + "# Предположим, на странице есть с классом \"data-table\"\n", + "table_html = soup.find(\"table\", {\"class\": \"data-table\"})\n", + "\n", + "# Преобразуем в DataFrame, если таблица более-менее стандартная\n", + "df_list = pd.read_html(str(table_html)) # возвращает список df\n", + "df = df_list[0] # берем первую таблицу\n", + "\n", + "df.head()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dl9b0hqOiFII" + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "excel_url = \"https://data.gov.ru/files/dataset.xls\"\n", + "df_excel = pd.read_excel(excel_url, sheet_name=\"Sheet1\")\n", + "print(df_excel.columns)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "A_13q1K9iMr2" + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "json_url = \"https://data.gov.ru/files/dataset.json\"\n", + "df_json = pd.read_json(json_url, orient=\"records\")\n", + "print(df_json.info())\n" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/seminars/Тема 1.1/Noise.ipynb b/seminars/Тема 1.1/Noise.ipynb new file mode 100644 index 0000000..cf08ab6 --- /dev/null +++ b/seminars/Тема 1.1/Noise.ipynb @@ -0,0 +1,1521 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "btIcF4lpu_Xx" + }, + "source": [ + "import requests\n", + "\n", + "# Пример запроса к TCIA API\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "j4rCtvc9u_XD" + }, + "source": [ + "**Шаг 1**. The Cancer Imaging Archive (TCIA) предоставляет доступ к медицинским изображениям через API. Вы можете использовать библиотеку requests для загрузки данных из TCIA:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 6172, + "status": "ok", + "timestamp": 1733077549943, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "uR4WCv5yrzNl", + "outputId": "0a2a3495-5147-4bed-a209-c800c26cda8d" + }, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "# Пример запроса к TCIA API\n", + "url = \"https://services.cancerimagingarchive.net/services/v3/TCIA/query/getSeries?Collection=TCGA-BRCA\"\n", + "response = requests.get(url)\n", + "\n", + "# Проверка ответа\n", + "if response.status_code == 200:\n", + " print(\"Данные получены успешно\")\n", + "else:\n", + " print(\"Ошибка получения данных\")\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AlwoAu-ju7SC" + }, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LuW1z73XvxYF" + }, + "source": [ + "**Шаг 2.** Данные приходят в JSON формате.\n", + "В этом примере запрос отправляется с параметром format=json, чтобы вернуть данные в формате JSON.\n", + "Преобразование ответа в JSON:\n", + "response.json() позволяет конвертировать ответ в словарь Python, с которым будем работать дальше.\n", + "Обработка JSON-ответа:\n", + "Сначала печатаем весь JSON для ознакомления." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 7356, + "status": "ok", + "timestamp": 1733077563733, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "0rA31BsAuGeM", + "outputId": "fac4ab13-ddbb-41fd-ade9-279df305554a" + }, + "outputs": [], + "source": [ + "import requests\n", + "import json\n", + "\n", + "# Пример запроса к TCIA API, предполагающий JSON формат\n", + "url = \"https://services.cancerimagingarchive.net/services/v3/TCIA/query/getSeries?Collection=TCGA-BRCA&format=json\"\n", + "response = requests.get(url)\n", + "\n", + "# Проверка ответа\n", + "if response.status_code == 200:\n", + " print(\"Данные получены успешно\")\n", + "\n", + " # Преобразование ответа в JSON\n", + " data = response.json()\n", + " print(json.dumps(data, indent=4)) # Печать данных в формате JSON с отступами\n", + "\n", + " # Обработка данных\n", + " for item in data:\n", + " series_instance_uid = item.get('SeriesInstanceUID', 'N/A')\n", + " modality = item.get('Modality', 'N/A')\n", + " protocol_name = item.get('ProtocolName', 'N/A')\n", + " body_part_examined = item.get('BodyPartExamined', 'N/A')\n", + "\n", + " # Печать извлеченной информации\n", + " print(f\"SeriesInstanceUID: {series_instance_uid}\")\n", + " print(f\"Modality: {modality}\")\n", + " print(f\"ProtocolName: {protocol_name}\")\n", + " print(f\"BodyPartExamined: {body_part_examined}\")\n", + " print('-' * 30)\n", + "else:\n", + " print(\"Ошибка получения данных\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "G3VSxAgX6RbK" + }, + "source": [ + "**Шаг3**.**Получение идентификаторов для загрузки изображений**\n", + "\n", + "Используя первый запрос к TCIA API, получили метаданные, такие как SeriesInstanceUID для каждой серии изображений.\n", + "Эти идентификаторы нужны для следующего шага — получения ссылок на сами изображения.\n", + "Запрос на загрузку изображений с использованием SeriesInstanceUID\n", + "Чтобы загрузить все изображения в определенной серии, нужно выполнить запрос к TCIA API с использованием SeriesInstanceUID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 17756, + "status": "ok", + "timestamp": 1733077587143, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "AexV0o2gJahr", + "outputId": "06c3167c-931f-4f15-917e-a5fe6d7463d5" + }, + "outputs": [], + "source": [ + "import os\n", + "import requests\n", + "\n", + "# Пример использования SeriesInstanceUID\n", + "series_instance_uid = \"1.3.6.1.4.1.14519.5.2.1.6450.4002.265963154110062013582166845425\" # Пример идентификатора\n", + "\n", + "# URL для загрузки изображений\n", + "url = f\"https://services.cancerimagingarchive.net/services/v3/TCIA/query/getImage?SeriesInstanceUID={series_instance_uid}\"\n", + "response = requests.get(url, stream=True)\n", + "\n", + "# Путь для сохранения ZIP-файла\n", + "output_path = \"/content/images.zip\"\n", + "\n", + "# Создание необходимых папок, если их нет\n", + "folder_path = os.path.dirname(output_path)\n", + "os.makedirs(folder_path, exist_ok=True)\n", + "\n", + "# Проверка ответа и сохранение изображения\n", + "if response.status_code == 200:\n", + " print(f\"Сохранение файла в {output_path}\")\n", + " with open(output_path, \"wb\") as file:\n", + " for chunk in response.iter_content(chunk_size=128):\n", + " file.write(chunk)\n", + " print(f\"Изображения успешно загружены и сохранены как {output_path}\")\n", + "else:\n", + " print(\"Ошибка при загрузке изображений\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 380, + "status": "ok", + "timestamp": 1733077591492, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "-PwPLc8aKV9h", + "outputId": "821075b5-c777-451e-e84f-6ea3f69c092a" + }, + "outputs": [], + "source": [ + "import zipfile\n", + "\n", + "# Распаковка загруженного ZIP-файла\n", + "with zipfile.ZipFile(\"images.zip\", 'r') as zip_ref:\n", + " zip_ref.extractall(\"images\") # Папка для распакованных изображений\n", + "print(\"Файлы успешно распакованы\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DVVYma3wM4vf" + }, + "source": [ + "**Шаг. 4**# Для чтения DICOM файлов используйте pydicom, а для визуализации изображений — matplotlib." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 27607, + "status": "ok", + "timestamp": 1733077622368, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "Tw2PTwQrOlLe", + "outputId": "d21f3b3b-c985-45ad-fdc6-a8c36cd10335" + }, + "outputs": [], + "source": [ + "!pip install pydicom\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 839 + }, + "executionInfo": { + "elapsed": 13, + "status": "ok", + "timestamp": 1732301923418, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "OLMmOFkCM59-", + "outputId": "f4370e76-ad70-45da-8192-fe2de2232455" + }, + "outputs": [], + "source": [ + "import os\n", + "import pydicom\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Папка с распакованными изображениями\n", + "dicom_folder = \"images\"\n", + "\n", + "# Чтение всех DICOM-файлов в папке и визуализация первого изображения\n", + "for filename in os.listdir(dicom_folder):\n", + " if filename.endswith(\".dcm\"):\n", + " dicom_path = os.path.join(dicom_folder, filename)\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + "\n", + " # Получение массива пикселей изображения\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Визуализация изображения\n", + " plt.imshow(image, cmap=\"gray\")\n", + " plt.title(f\"Пациент ID: {dicom_data.PatientID}\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n", + " break # Визуализируем только первое изображение\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "U-9TfVjKQsFV" + }, + "source": [ + " библиотеки, как OpenCV, scikit-image, и NumPy предоставляют множество инструментов для фильтрации, повышения контраста и удаления шума." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rCrnMoLORzQn" + }, + "source": [ + "**Фильтрация для уменьшения шума**\n", + "Использование фильтра Гаусса для сглаживания изображения и уменьшения шума" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 429 + }, + "executionInfo": { + "elapsed": 1602, + "status": "ok", + "timestamp": 1732302625198, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "xtnFVnjdRtLn", + "outputId": "b09651c6-b009-4982-fe00-7baeea8f7b19" + }, + "outputs": [], + "source": [ + "import cv2\n", + "import numpy as np\n", + "\n", + "# Применение фильтра Гаусса для уменьшения шума\n", + "denoised_image = cv2.GaussianBlur(image, (5, 5), 0)\n", + "\n", + "# Визуализация результата\n", + "plt.imshow(denoised_image, cmap=\"gray\")\n", + "plt.title(\"Изображение после применения фильтра Гаусса (уменьшение шума)\")\n", + "plt.axis(\"off\")\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "executionInfo": { + "elapsed": 8014, + "status": "ok", + "timestamp": 1732303361685, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "iPL6bf_nU02U", + "outputId": "749deb76-b4bb-4965-f988-25a87c298274" + }, + "outputs": [], + "source": [ + "import os\n", + "import cv2\n", + "import numpy as np\n", + "import pydicom\n", + "import matplotlib.pyplot as plt\n", + "from skimage import exposure\n", + "\n", + "# Папка с распакованными изображениями\n", + "dicom_folder = \"images\"\n", + "\n", + "# Чтение первого DICOM-файла в папке\n", + "dicom_path = os.path.join(dicom_folder, os.listdir(dicom_folder)[0])\n", + "dicom_data = pydicom.dcmread(dicom_path)\n", + "\n", + "# Получение массива пикселей изображения\n", + "image = dicom_data.pixel_array\n", + "\n", + "# Функция для отображения сравнения изображений \"До\" и \"После\"\n", + "def show_comparison(original, processed, title):\n", + " plt.figure(figsize=(10, 5))\n", + " # Оригинальное изображение\n", + " plt.subplot(1, 2, 1)\n", + " plt.imshow(original, cmap='gray')\n", + " plt.title('До обработки')\n", + " plt.axis('off')\n", + " # Обработанное изображение\n", + " plt.subplot(1, 2, 2)\n", + " plt.imshow(processed, cmap='gray')\n", + " plt.title(title)\n", + " plt.axis('off')\n", + " plt.show()\n", + "\n", + "# 1. Применение фильтра Гаусса для уменьшения шума\n", + "denoised_image = cv2.GaussianBlur(image, (5, 5), 0)\n", + "show_comparison(image, denoised_image, 'После фильтра Гаусса (уменьшение шума)')\n", + "\n", + "# 2. Выравнивание гистограммы для улучшения контраста\n", + "equalized_image = exposure.equalize_hist(image)\n", + "show_comparison(image, equalized_image, 'Выравнивание гистограммы (улучшение контраста)')\n", + "\n", + "# 3. Адаптивное выравнивание гистограммы (CLAHE)\n", + "# Преобразование типа данных изображения к uint8\n", + "image_uint8 = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n", + "\n", + "# Применение CLAHE\n", + "clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))\n", + "clahe_image = clahe.apply(image_uint8)\n", + "show_comparison(image, clahe_image, 'Адаптивное выравнивание гистограммы (CLAHE)')\n", + "\n", + "# 4. Выделение краев с использованием фильтра Собеля\n", + "sobelx = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=3)\n", + "sobely = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=3)\n", + "sobel_image = np.hypot(sobelx, sobely)\n", + "show_comparison(image, sobel_image, 'Выделение краев (фильтр Собеля)')\n", + "\n", + "# 5. Увеличение резкости изображения (Unsharp Masking)\n", + "blurred_image = cv2.GaussianBlur(image, (9, 9), 10.0)\n", + "sharpened_image = cv2.addWeighted(image, 1.5, blurred_image, -0.5, 0)\n", + "show_comparison(image, sharpened_image, 'Увеличение резкости (Unsharp Masking)')\n", + "\n", + "# 6. Пороговое преобразование для удаления фона\n", + "_, threshold_image = cv2.threshold(image, 100, 255, cv2.THRESH_BINARY)\n", + "show_comparison(image, threshold_image, 'Пороговое преобразование (удаление фона)')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 3476, + "status": "ok", + "timestamp": 1733077641046, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "tpL-XSGBdKhg", + "outputId": "fe5ae1ba-c850-4520-8e8f-edf4e6ed96f1" + }, + "outputs": [], + "source": [ + "!pip install scikit-image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 13600, + "status": "ok", + "timestamp": 1733077659472, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "wMIQ8oaTdhm8", + "outputId": "cd993480-6b9f-436d-efa7-e9b1f029e78b" + }, + "outputs": [], + "source": [ + "!pip install SimpleITK\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 8678, + "status": "ok", + "timestamp": 1733077668145, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "d-nLd0SGd08v", + "outputId": "15c4bb63-d405-4a0a-a6ae-e91e1c5e5b24" + }, + "outputs": [], + "source": [ + "!pip install bm3d\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7GMcFMIQGJd_" + }, + "source": [ + "Частотные фильтры" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "executionInfo": { + "elapsed": 6330, + "status": "ok", + "timestamp": 1732797241901, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "sDhJ92f5eow1", + "outputId": "7fe797a2-778d-486c-834e-5185734ae3f5" + }, + "outputs": [], + "source": [ + "import os\n", + "import cv2\n", + "import numpy as np\n", + "import pydicom\n", + "import matplotlib.pyplot as plt\n", + "from skimage import exposure\n", + "\n", + "# Папка с распакованными изображениями\n", + "dicom_folder = \"images\"\n", + "\n", + "# Чтение всех DICOM-файлов в папке и наложение изображений\n", + "dicom_files = [os.path.join(dicom_folder, f) for f in os.listdir(dicom_folder) if f.endswith('.dcm')]\n", + "combined_image = None\n", + "\n", + "for dicom_path in dicom_files:\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Преобразование изображения в формат uint8\n", + " image_uint8 = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n", + "\n", + " if combined_image is None:\n", + " combined_image = image_uint8\n", + " else:\n", + " combined_image = cv2.addWeighted(combined_image, 0.5, image_uint8, 0.5, 0)\n", + "\n", + "# Функция для отображения изображения\n", + "def show_image(image, title):\n", + " plt.figure(figsize=(6, 6))\n", + " plt.imshow(image, cmap='gray')\n", + " plt.title(title)\n", + " plt.axis('off')\n", + " plt.show()\n", + "\n", + "# Гауссовский НЧ фильтр (Low Pass Filter)\n", + "gaussian_low_pass = cv2.GaussianBlur(combined_image, (15, 15), 0)\n", + "\n", + "# Гауссовский ВЧ фильтр (High Pass Filter)\n", + "gaussian_blurred = cv2.GaussianBlur(combined_image, (15, 15), 0)\n", + "gaussian_high_pass = cv2.addWeighted(combined_image, 1.5, gaussian_blurred, -0.5, 0)\n", + "\n", + "# Идеальный НЧ фильтр в частотной области\n", + "def ideal_low_pass_filter(image, D0):\n", + " dft = cv2.dft(np.float32(image), flags=cv2.DFT_COMPLEX_OUTPUT)\n", + " dft_shift = np.fft.fftshift(dft)\n", + "\n", + " rows, cols = image.shape\n", + " crow, ccol = rows // 2 , cols // 2\n", + " mask = np.zeros((rows, cols, 2), np.uint8)\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " if np.sqrt((i - crow) ** 2 + (j - ccol) ** 2) <= D0:\n", + " mask[i, j] = 1\n", + "\n", + " fshift = dft_shift * mask\n", + " f_ishift = np.fft.ifftshift(fshift)\n", + " img_back = cv2.idft(f_ishift)\n", + " img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])\n", + " return np.uint8(cv2.normalize(img_back, None, 0, 255, cv2.NORM_MINMAX))\n", + "\n", + "ideal_low_pass = ideal_low_pass_filter(combined_image, 30)\n", + "\n", + "# Идеальный ВЧ фильтр в частотной области\n", + "def ideal_high_pass_filter(image, D0):\n", + " dft = cv2.dft(np.float32(image), flags=cv2.DFT_COMPLEX_OUTPUT)\n", + " dft_shift = np.fft.fftshift(dft)\n", + "\n", + " rows, cols = image.shape\n", + " crow, ccol = rows // 2 , cols // 2\n", + " mask = np.ones((rows, cols, 2), np.uint8)\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " if np.sqrt((i - crow) ** 2 + (j - ccol) ** 2) <= D0:\n", + " mask[i, j] = 0\n", + "\n", + " fshift = dft_shift * mask\n", + " f_ishift = np.fft.ifftshift(fshift)\n", + " img_back = cv2.idft(f_ishift)\n", + " img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])\n", + " return np.uint8(cv2.normalize(img_back, None, 0, 255, cv2.NORM_MINMAX))\n", + "\n", + "ideal_high_pass = ideal_high_pass_filter(combined_image, 30)\n", + "\n", + "# НЧ фильтр Баттерворта\n", + "def butterworth_low_pass_filter(image, D0, n):\n", + " dft = cv2.dft(np.float32(image), flags=cv2.DFT_COMPLEX_OUTPUT)\n", + " dft_shift = np.fft.fftshift(dft)\n", + "\n", + " rows, cols = image.shape\n", + " crow, ccol = rows // 2 , cols // 2\n", + " mask = np.zeros((rows, cols, 2), np.float32)\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " D = np.sqrt((i - crow) ** 2 + (j - ccol) ** 2)\n", + " mask[i, j] = 1 / (1 + (D / D0) ** (2 * n))\n", + "\n", + " fshift = dft_shift * mask\n", + " f_ishift = np.fft.ifftshift(fshift)\n", + " img_back = cv2.idft(f_ishift)\n", + " img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])\n", + " return np.uint8(cv2.normalize(img_back, None, 0, 255, cv2.NORM_MINMAX))\n", + "\n", + "butterworth_low_pass = butterworth_low_pass_filter(combined_image, 30, 2)\n", + "\n", + "# ВЧ фильтр Баттерворта\n", + "def butterworth_high_pass_filter(image, D0, n):\n", + " dft = cv2.dft(np.float32(image), flags=cv2.DFT_COMPLEX_OUTPUT)\n", + " dft_shift = np.fft.fftshift(dft)\n", + "\n", + " rows, cols = image.shape\n", + " crow, ccol = rows // 2 , cols // 2\n", + " mask = np.ones((rows, cols, 2), np.float32)\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " D = np.sqrt((i - crow) ** 2 + (j - ccol) ** 2)\n", + " mask[i, j] = 1 / (1 + (D0 / D) ** (2 * n))\n", + "\n", + " fshift = dft_shift * mask\n", + " f_ishift = np.fft.ifftshift(fshift)\n", + " img_back = cv2.idft(f_ishift)\n", + " img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])\n", + " return np.uint8(cv2.normalize(img_back, None, 0, 255, cv2.NORM_MINMAX))\n", + "\n", + "butterworth_high_pass = butterworth_high_pass_filter(combined_image, 30, 2)\n", + "\n", + "# Отображение всех изображений\n", + "show_image(combined_image, 'Исходное изображение')\n", + "show_image(gaussian_low_pass, 'Гауссовский НЧ фильтр')\n", + "show_image(gaussian_high_pass, 'Гауссовский ВЧ фильтр')\n", + "show_image(ideal_low_pass, 'Идеальный НЧ фильтр')\n", + "show_image(ideal_high_pass, 'Идеальный ВЧ фильтр')\n", + "show_image(butterworth_low_pass, 'НЧ фильтр Баттерворта')\n", + "show_image(butterworth_high_pass, 'ВЧ фильтр Баттерворта')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "executionInfo": { + "elapsed": 7867, + "status": "ok", + "timestamp": 1732974353790, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "TUaa8cviKcC7", + "outputId": "5e755024-a392-4d61-88a6-6efbcb852901" + }, + "outputs": [], + "source": [ + "import os\n", + "import cv2\n", + "import numpy as np\n", + "import pydicom\n", + "import matplotlib.pyplot as plt\n", + "from skimage import exposure\n", + "\n", + "# Папка с распакованными изображениями\n", + "dicom_folder = \"images\"\n", + "\n", + "# Чтение всех DICOM-файлов в папке и наложение изображений\n", + "dicom_files = [os.path.join(dicom_folder, f) for f in os.listdir(dicom_folder) if f.endswith('.dcm')]\n", + "combined_image = None\n", + "\n", + "for dicom_path in dicom_files:\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Преобразование изображения в формат uint8\n", + " image_uint8 = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n", + "\n", + " if combined_image is None:\n", + " combined_image = image_uint8\n", + " else:\n", + " combined_image = cv2.addWeighted(combined_image, 0.5, image_uint8, 0.5, 0)\n", + "\n", + "# Функция для отображения изображения\n", + "def show_image(image, title):\n", + " plt.figure(figsize=(6, 6))\n", + " plt.imshow(image, cmap='gray')\n", + " plt.title(title)\n", + " plt.axis('off')\n", + " plt.show()\n", + "\n", + "# Гауссовский НЧ фильтр (Low Pass Filter)\n", + "gaussian_low_pass = cv2.GaussianBlur(combined_image, (15, 15), 0)\n", + "\n", + "# Гауссовский ВЧ фильтр (High Pass Filter)\n", + "gaussian_blurred = cv2.GaussianBlur(combined_image, (15, 15), 0)\n", + "gaussian_high_pass = cv2.addWeighted(combined_image, 1.5, gaussian_blurred, -0.5, 0)\n", + "\n", + "# Идеальный НЧ фильтр в частотной области\n", + "def ideal_low_pass_filter(image, D0):\n", + " dft = cv2.dft(np.float32(image), flags=cv2.DFT_COMPLEX_OUTPUT)\n", + " dft_shift = np.fft.fftshift(dft)\n", + "\n", + " rows, cols = image.shape\n", + " crow, ccol = rows // 2 , cols // 2\n", + " mask = np.zeros((rows, cols, 2), np.uint8)\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " if np.sqrt((i - crow) ** 2 + (j - ccol) ** 2) <= D0:\n", + " mask[i, j] = 1\n", + "\n", + " fshift = dft_shift * mask\n", + " f_ishift = np.fft.ifftshift(fshift)\n", + " img_back = cv2.idft(f_ishift)\n", + " img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])\n", + " return np.uint8(cv2.normalize(img_back, None, 0, 255, cv2.NORM_MINMAX))\n", + "\n", + "ideal_low_pass = ideal_low_pass_filter(combined_image, 30)\n", + "\n", + "# Идеальный ВЧ фильтр в частотной области\n", + "def ideal_high_pass_filter(image, D0):\n", + " dft = cv2.dft(np.float32(image), flags=cv2.DFT_COMPLEX_OUTPUT)\n", + " dft_shift = np.fft.fftshift(dft)\n", + "\n", + " rows, cols = image.shape\n", + " crow, ccol = rows // 2 , cols // 2\n", + " mask = np.ones((rows, cols, 2), np.uint8)\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " if np.sqrt((i - crow) ** 2 + (j - ccol) ** 2) <= D0:\n", + " mask[i, j] = 0\n", + "\n", + " fshift = dft_shift * mask\n", + " f_ishift = np.fft.ifftshift(fshift)\n", + " img_back = cv2.idft(f_ishift)\n", + " img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])\n", + " return np.uint8(cv2.normalize(img_back, None, 0, 255, cv2.NORM_MINMAX))\n", + "\n", + "ideal_high_pass = ideal_high_pass_filter(combined_image, 30)\n", + "\n", + "# НЧ фильтр Баттерворта\n", + "def butterworth_low_pass_filter(image, D0, n):\n", + " dft = cv2.dft(np.float32(image), flags=cv2.DFT_COMPLEX_OUTPUT)\n", + " dft_shift = np.fft.fftshift(dft)\n", + "\n", + " rows, cols = image.shape\n", + " crow, ccol = rows // 2 , cols // 2\n", + " mask = np.zeros((rows, cols, 2), np.float32)\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " D = np.sqrt((i - crow) ** 2 + (j - ccol) ** 2)\n", + " mask[i, j] = 1 / (1 + (D / D0) ** (2 * n))\n", + "\n", + " fshift = dft_shift * mask\n", + " f_ishift = np.fft.ifftshift(fshift)\n", + " img_back = cv2.idft(f_ishift)\n", + " img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])\n", + " return np.uint8(cv2.normalize(img_back, None, 0, 255, cv2.NORM_MINMAX))\n", + "\n", + "butterworth_low_pass = butterworth_low_pass_filter(combined_image, 30, 2)\n", + "\n", + "# ВЧ фильтр Баттерворта\n", + "def butterworth_high_pass_filter(image, D0, n):\n", + " dft = cv2.dft(np.float32(image), flags=cv2.DFT_COMPLEX_OUTPUT)\n", + " dft_shift = np.fft.fftshift(dft)\n", + "\n", + " rows, cols = image.shape\n", + " crow, ccol = rows // 2 , cols // 2\n", + " mask = np.ones((rows, cols, 2), np.float32)\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " D = np.sqrt((i - crow) ** 2 + (j - ccol) ** 2)\n", + " mask[i, j] = 1 / (1 + (D0 / D) ** (2 * n))\n", + "\n", + " fshift = dft_shift * mask\n", + " f_ishift = np.fft.ifftshift(fshift)\n", + " img_back = cv2.idft(f_ishift)\n", + " img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])\n", + " return np.uint8(cv2.normalize(img_back, None, 0, 255, cv2.NORM_MINMAX))\n", + "\n", + "butterworth_high_pass = butterworth_high_pass_filter(combined_image, 30, 2)\n", + "\n", + "# Билатеральный фильтр (Bilateral Filter)\n", + "bilateral_filtered = cv2.bilateralFilter(combined_image, d=15, sigmaColor=7, sigmaSpace=7)\n", + "\n", + "# Отображение всех изображений\n", + "show_image(combined_image, 'Исходное изображение')\n", + "show_image(gaussian_low_pass, 'Гауссовский НЧ фильтр')\n", + "show_image(gaussian_high_pass, 'Гауссовский ВЧ фильтр')\n", + "show_image(ideal_low_pass, 'Идеальный НЧ фильтр')\n", + "show_image(ideal_high_pass, 'Идеальный ВЧ фильтр')\n", + "show_image(butterworth_low_pass, 'НЧ фильтр Баттерворта')\n", + "show_image(butterworth_high_pass, 'ВЧ фильтр Баттерворта')\n", + "show_image(bilateral_filtered, 'Билатеральный фильтр')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_zwhKwJVL7gT" + }, + "source": [ + "С использованием изображения с компьютера" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "output_embedded_package_id": "1owFR5aZk0s6YEDFr_aMh3c1WryCNolwa" + }, + "executionInfo": { + "elapsed": 25693, + "status": "ok", + "timestamp": 1732877156436, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "1pelwlcrU3Kh", + "outputId": "a840dd2b-8380-4c13-b44c-6ee1f5e3e5c8" + }, + "outputs": [], + "source": [ + "import os\n", + "import cv2\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from skimage import exposure\n", + "\n", + "# Загрузка изображения с компьютера\n", + "image_path = \"/content/3.jpg\" # Укажите путь к вашему изображению\n", + "combined_image = cv2.imread(image_path) # Загрузка цветного изображения\n", + "\n", + "# Проверка успешной загрузки изображения\n", + "if combined_image is None:\n", + " raise FileNotFoundError(f\"Не удалось загрузить изображение по указанному пути: {image_path}\")\n", + "\n", + "# Функция для отображения изображения\n", + "def show_image(image, title):\n", + " plt.figure(figsize=(6, 6))\n", + " plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))\n", + " plt.title(title)\n", + " plt.axis('off')\n", + " plt.show()\n", + "\n", + "# Вывод загруженного изображения\n", + "show_image(combined_image, 'Загруженное цветное изображение')\n", + "\n", + "# Гауссовский НЧ фильтр (Low Pass Filter)\n", + "gaussian_low_pass = cv2.GaussianBlur(combined_image, (15, 15), 0)\n", + "\n", + "# Гауссовский ВЧ фильтр (High Pass Filter)\n", + "gaussian_blurred = cv2.GaussianBlur(combined_image, (15, 15), 0)\n", + "gaussian_high_pass = cv2.addWeighted(combined_image, 1.5, gaussian_blurred, -0.5, 0)\n", + "\n", + "# Применение DFT к трехканальному изображению (каждому каналу отдельно)\n", + "def dft_three_channels(image, D0, filter_type='low'):\n", + " # Разделение цветного изображения на три канала (B, G, R)\n", + " channels = cv2.split(image)\n", + " filtered_channels = []\n", + "\n", + " for channel in channels:\n", + " # Преобразование Фурье каждого канала\n", + " dft = cv2.dft(np.float32(channel), flags=cv2.DFT_COMPLEX_OUTPUT)\n", + " dft_shift = np.fft.fftshift(dft)\n", + "\n", + " # Создание маски для фильтрации (идеальный НЧ или ВЧ фильтр)\n", + " rows, cols = channel.shape\n", + " crow, ccol = rows // 2, cols // 2\n", + " mask = np.zeros((rows, cols, 2), np.uint8)\n", + "\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " distance = np.sqrt((i - crow) ** 2 + (j - ccol) ** 2)\n", + " if filter_type == 'low':\n", + " mask[i, j] = 1 if distance <= D0 else 0\n", + " elif filter_type == 'high':\n", + " mask[i, j] = 0 if distance <= D0 else 1\n", + "\n", + " # Применение фильтра в частотной области\n", + " fshift = dft_shift * mask\n", + " f_ishift = np.fft.ifftshift(fshift)\n", + " img_back = cv2.idft(f_ishift)\n", + " img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])\n", + "\n", + " # Нормализация изображения и добавление в список\n", + " filtered_channels.append(np.uint8(cv2.normalize(img_back, None, 0, 255, cv2.NORM_MINMAX)))\n", + "\n", + " # Объединение каналов обратно в цветное изображение\n", + " filtered_image = cv2.merge(filtered_channels)\n", + " return filtered_image\n", + "\n", + "# Применение фильтров к трехканальному изображению\n", + "ideal_low_pass_color = dft_three_channels(combined_image, 30, filter_type='low')\n", + "ideal_high_pass_color = dft_three_channels(combined_image, 30, filter_type='high')\n", + "\n", + "# НЧ фильтр Баттерворта для цветного изображения\n", + "def butterworth_filter_three_channels(image, D0, n, filter_type='low'):\n", + " channels = cv2.split(image)\n", + " filtered_channels = []\n", + "\n", + " for channel in channels:\n", + " dft = cv2.dft(np.float32(channel), flags=cv2.DFT_COMPLEX_OUTPUT)\n", + " dft_shift = np.fft.fftshift(dft)\n", + "\n", + " rows, cols = channel.shape\n", + " crow, ccol = rows // 2, cols // 2\n", + " mask = np.zeros((rows, cols, 2), np.float32)\n", + "\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " D = np.sqrt((i - crow) ** 2 + (j - ccol) ** 2)\n", + " if filter_type == 'low':\n", + " mask[i, j] = 1 / (1 + (D / D0) ** (2 * n))\n", + " elif filter_type == 'high':\n", + " mask[i, j] = 1 / (1 + (D0 / D) ** (2 * n))\n", + "\n", + " fshift = dft_shift * mask\n", + " f_ishift = np.fft.ifftshift(fshift)\n", + " img_back = cv2.idft(f_ishift)\n", + " img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])\n", + "\n", + " filtered_channels.append(np.uint8(cv2.normalize(img_back, None, 0, 255, cv2.NORM_MINMAX)))\n", + "\n", + " filtered_image = cv2.merge(filtered_channels)\n", + " return filtered_image\n", + "\n", + "# Применение фильтров Баттерворта к трехканальному изображению\n", + "butterworth_low_pass_color = butterworth_filter_three_channels(combined_image, 30, 2, filter_type='low')\n", + "butterworth_high_pass_color = butterworth_filter_three_channels(combined_image, 30, 2, filter_type='high')\n", + "\n", + "# Билатеральный фильтр (Bilateral Filter)\n", + "bilateral_filtered = cv2.bilateralFilter(combined_image, d=9, sigmaColor=100, sigmaSpace=100)\n", + "\n", + "# Отображение всех изображений\n", + "show_image(combined_image, 'Исходное изображение')\n", + "show_image(gaussian_low_pass, 'Гауссовский НЧ фильтр')\n", + "show_image(gaussian_high_pass, 'Гауссовский ВЧ фильтр')\n", + "show_image(ideal_low_pass_color, 'Идеальный НЧ фильтр для цветного изображения')\n", + "show_image(ideal_high_pass_color, 'Идеальный ВЧ фильтр для цветного изображения')\n", + "show_image(butterworth_low_pass_color, 'НЧ фильтр Баттерворта для цветного изображения')\n", + "show_image(butterworth_high_pass_color, 'ВЧ фильтр Баттерворта для цветного изображения')\n", + "show_image(bilateral_filtered, 'Билатеральный фильтр')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "output_embedded_package_id": "1uvbcPc5Mg3W5hnDhdi9U0B3JKTxiY1Ny" + }, + "executionInfo": { + "elapsed": 8791, + "status": "ok", + "timestamp": 1732877745096, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "zrzUTEYMj4Od", + "outputId": "42281a9c-4adb-4f47-8bd2-74d360a6a00c" + }, + "outputs": [], + "source": [ + "import os\n", + "import cv2\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from skimage import exposure\n", + "\n", + "# Загрузка изображения с компьютера\n", + "image_path = \"/content/3.jpg\" # Укажите путь к вашему изображению\n", + "combined_image = cv2.imread(image_path) # Загрузка цветного изображения\n", + "\n", + "# Добавление шума на исходное изображение (цветные точки)\n", + "def add_salt_and_pepper_noise(image, amount=0.02):\n", + " noisy_image = image.copy()\n", + " num_salt = np.ceil(amount * image.size * 0.5)\n", + " coords = [np.random.randint(0, i - 1, int(num_salt)) for i in image.shape]\n", + " noisy_image[coords[0], coords[1], :] = [255, 255, 255] # Белые точки (\"соль\")\n", + "\n", + " num_pepper = np.ceil(amount * image.size * 0.5)\n", + " coords = [np.random.randint(0, i - 1, int(num_pepper)) for i in image.shape]\n", + " noisy_image[coords[0], coords[1], :] = [0, 0, 0] # Черные точки (\"перец\")\n", + "\n", + " return noisy_image\n", + "\n", + "combined_image = add_salt_and_pepper_noise(combined_image)\n", + "\n", + "# Проверка успешной загрузки изображения\n", + "if combined_image is None:\n", + " raise FileNotFoundError(f\"Не удалось загрузить изображение по указанному пути: {image_path}\")\n", + "\n", + "# Функция для отображения изображения\n", + "def show_image(image, title):\n", + " plt.figure(figsize=(6, 6))\n", + " plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))\n", + " plt.title(title)\n", + " plt.axis('off')\n", + " plt.show()\n", + "\n", + "# Вывод изображения с добавленным шумом\n", + "show_image(combined_image, 'Загруженное цветное изображение')\n", + "\n", + "# Гауссовский НЧ фильтр (Low Pass Filter)\n", + "gaussian_low_pass = cv2.GaussianBlur(combined_image, (15, 15), 0)\n", + "\n", + "# Медианный фильтр для удаления шума \"соль и перец\"\n", + "median_filtered = cv2.medianBlur(combined_image, 5)\n", + "\n", + "# Билатеральный фильтр (Bilateral Filter)\n", + "bilateral_filtered = cv2.bilateralFilter(median_filtered, d=9, sigmaColor=100, sigmaSpace=100)\n", + "\n", + "# Отображение всех изображений\n", + "show_image(median_filtered, 'Медианный фильтр')\n", + "show_image(combined_image, 'Исходное изображение')\n", + "show_image(gaussian_low_pass, 'Гауссовский НЧ фильтр')\n", + "show_image(bilateral_filtered, 'Билатеральный фильтр')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0osd9hgPwgHw" + }, + "source": [ + "LIME" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "output_embedded_package_id": "1YI5PIcRyu6wjNI3yjocXgx-ZtjEr1hlF" + }, + "executionInfo": { + "elapsed": 6877, + "status": "ok", + "timestamp": 1732973578497, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "qQeyiTfCRVQS", + "outputId": "178f3f89-9e4e-44fa-98d3-6410271222a5" + }, + "outputs": [], + "source": [ + "import cv2\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from skimage import restoration\n", + "\n", + "# Загрузка изображения\n", + "image_path = \"/content/3.jpg\" # Укажите путь к вашему изображению\n", + "input_image = cv2.imread(image_path)\n", + "if input_image is None:\n", + " raise FileNotFoundError(f\"Не удалось загрузить изображение по указанному пути: {image_path}\")\n", + "\n", + "# Функция для отображения изображения\n", + "def show_image(image, title):\n", + " plt.figure(figsize=(10, 10))\n", + " plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))\n", + " plt.title(title)\n", + " plt.axis('off')\n", + " plt.show()\n", + "\n", + "# Функция для оценки карты освещенности\n", + "def estimate_illumination_map(image, kernel_size=15):\n", + " epsilon = 1e-11 # Определение epsilon для использования в функции\n", + " illumination_map = np.max(image, axis=2)\n", + " illumination_map = cv2.GaussianBlur(illumination_map, (kernel_size, kernel_size), 0)\n", + "\n", + " return illumination_map\n", + "\n", + "# Функция для применения LIME улучшения\n", + "def lime_enhancement(image, alpha=1, epsilon=1e-11):\n", + " illumination_map = cv2.normalize(estimate_illumination_map(image).astype(np.float32), None, alpha=0.0, beta=255.0, norm_type=cv2.NORM_MINMAX)\n", + " illumination_map = illumination_map[:, :, np.newaxis] # Добавление размерности для каналов\n", + " enhanced_image = np.clip((image.astype(np.float32) / (illumination_map / 255.0)) * alpha, 0, 255)\n", + " enhanced_image = enhanced_image.astype(np.uint8)\n", + " return enhanced_image\n", + "\n", + "# Применение LIME улучшения\n", + "lime_result = lime_enhancement(input_image, alpha=1)\n", + "\n", + "# Отображение исходного и обработанного изображений\n", + "show_image(input_image, 'Input Image')\n", + "show_image(lime_result, 'LIME Enhanced Image')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qk3M1b9FRmnM" + }, + "source": [ + "Преобразование Фурье" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 342 + }, + "executionInfo": { + "elapsed": 4699, + "status": "ok", + "timestamp": 1732973654601, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "FBCiNQELRuRK", + "outputId": "4baa1b06-90e6-42da-d8b2-2dd51f561c39" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import cv2\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Функции для фильтрации изображений\n", + "def gaussian_low_pass_filter(image, D0):\n", + " rows, cols = image.shape\n", + " crow, ccol = rows // 2, cols // 2\n", + "\n", + " # Перевод изображения в частотную область\n", + " dft = cv2.dft(np.float32(image), flags=cv2.DFT_COMPLEX_OUTPUT)\n", + " dft_shift = np.fft.fftshift(dft)\n", + "\n", + " # Создание маски Гауссовского фильтра\n", + " mask = np.zeros((rows, cols, 2), np.float32)\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " D = np.sqrt((i - crow)**2 + (j - ccol)**2)\n", + " mask[i, j] = np.exp(-D**2 / (2 * D0**2))\n", + "\n", + " # Применение маски\n", + " fshift = dft_shift * mask\n", + " f_ishift = np.fft.ifftshift(fshift)\n", + " img_back = cv2.idft(f_ishift)\n", + " img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])\n", + "\n", + " return img_back\n", + "\n", + "def gaussian_high_pass_filter(image, D0):\n", + " rows, cols = image.shape\n", + " crow, ccol = rows // 2, cols // 2\n", + "\n", + " # Перевод изображения в частотную область\n", + " dft = cv2.dft(np.float32(image), flags=cv2.DFT_COMPLEX_OUTPUT)\n", + " dft_shift = np.fft.fftshift(dft)\n", + "\n", + " # Создание маски Гауссовского фильтра\n", + " mask = np.ones((rows, cols, 2), np.float32)\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " D = np.sqrt((i - crow)**2 + (j - ccol)**2)\n", + " mask[i, j] = 1 - np.exp(-D**2 / (2 * D0**2))\n", + "\n", + " # Применение маски\n", + " fshift = dft_shift * mask\n", + " f_ishift = np.fft.ifftshift(fshift)\n", + " img_back = cv2.idft(f_ishift)\n", + " img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])\n", + "\n", + " return img_back\n", + "\n", + "# Загрузка изображения\n", + "def load_image(image_path):\n", + " image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)\n", + " if image is None:\n", + " raise FileNotFoundError(f\"Не удалось загрузить изображение по указанному пути: {image_path}\")\n", + " return image\n", + "\n", + "# Отображение исходного и фильтрованных изображений\n", + "def show_images(original, low_pass, high_pass):\n", + " plt.figure(figsize=(18, 6))\n", + " titles = ['Original Image', 'Low Pass Filtered Image', 'High Pass Filtered Image']\n", + " images = [original, low_pass, high_pass]\n", + "\n", + " for i in range(3):\n", + " plt.subplot(1, 3, i + 1)\n", + " plt.imshow(images[i], cmap='gray')\n", + " plt.title(titles[i])\n", + " plt.axis('off')\n", + " plt.show()\n", + "\n", + "# Основной код\n", + "image_path = \"/content/3.jpg\" # Укажите путь к вашему изображению\n", + "input_image = cv2.imread(image_path)\n", + "if input_image is None:\n", + " raise FileNotFoundError(f\"Не удалось загрузить изображение по указанному пути: {image_path}\")\n", + "\n", + "# Преобразование изображения в градации серого\n", + "input_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY)\n", + "\n", + "# Применение фильтров\n", + "low_pass_result = gaussian_low_pass_filter(input_image, D0=30)\n", + "high_pass_result = gaussian_high_pass_filter(input_image, D0=30)\n", + "\n", + "# Отображение результатов\n", + "show_images(input_image, low_pass_result, high_pass_result)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YAeox9QRbgjt" + }, + "source": [ + "Медианный и адаптивный медианный фильры" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 342 + }, + "executionInfo": { + "elapsed": 53805, + "status": "ok", + "timestamp": 1732976525949, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "SjZXNds2boW9", + "outputId": "579e3438-1517-4a88-86a0-0988ab336fdf" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import cv2\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Загрузка изображения\n", + "image_path = \"/content/3.jpg\" # Укажите путь к вашему изображению\n", + "input_image = cv2.imread(image_path)\n", + "if input_image is None:\n", + " raise FileNotFoundError(f\"Не удалось загрузить изображение по указанному пути: {image_path}\")\n", + "\n", + "# Добавление шума к изображению\n", + "noise = np.random.randint(2, 200, input_image.shape, dtype='uint8')\n", + "noisy_image = cv2.add(input_image, noise)\n", + "\n", + "# Функция для отображения изображения\n", + "def show_image(image, title):\n", + " plt.figure(figsize=(10, 10))\n", + " plt.imshow(image, cmap='gray')\n", + " plt.title(title)\n", + " plt.axis('off')\n", + " plt.show()\n", + "\n", + "# Функция для применения медианного фильтра\n", + "def median_filter(image, kernel_size=3):\n", + " return cv2.medianBlur(image, kernel_size)\n", + "\n", + "# Функция для применения адаптивного медианного фильтра\n", + "def adaptive_median_filter(image, max_kernel_size=7):\n", + " filtered_image = image.copy()\n", + " rows, cols, channels = image.shape\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " k = 3\n", + " while k <= max_kernel_size:\n", + " half_k = k // 2\n", + " # Определение границ окна\n", + " rmin = max(0, i - half_k)\n", + " rmax = min(rows, i + half_k + 1)\n", + " cmin = max(0, j - half_k)\n", + " cmax = min(cols, j + half_k + 1)\n", + " # Извлечение окна\n", + " window = image[rmin:rmax, cmin:cmax, :]\n", + " # Применение медианного фильтра к окну\n", + " z_min = np.min(window, axis=(0, 1))\n", + " z_max = np.max(window, axis=(0, 1))\n", + " z_med = np.median(window, axis=(0, 1))\n", + " z_xy = image[i, j, :]\n", + " if np.all(z_min < z_med) and np.all(z_med < z_max):\n", + " if np.all(z_min < z_xy) and np.all(z_xy < z_max):\n", + " filtered_image[i, j, :] = z_xy\n", + " else:\n", + " filtered_image[i, j, :] = z_med\n", + " break\n", + " else:\n", + " k += 2\n", + " if k > max_kernel_size:\n", + " filtered_image[i, j] = z_med\n", + " return filtered_image\n", + "\n", + "# Применение фильтров\n", + "median_result = median_filter(noisy_image, kernel_size=5)\n", + "adaptive_median_result = adaptive_median_filter(noisy_image, max_kernel_size=7)\n", + "\n", + "# Отображение исходного и фильтрованных изображений\n", + "plt.figure(figsize=(18, 6))\n", + "titles = ['Noisy Image', 'Median Filtered Image', 'Adaptive Median Filtered Image']\n", + "images = [noisy_image, median_result, adaptive_median_result]\n", + "\n", + "for i in range(3):\n", + " plt.subplot(1, 3, i + 1)\n", + " plt.imshow(cv2.cvtColor(images[i], cv2.COLOR_BGR2RGB) if len(images[i].shape) == 3 else images[i], cmap='gray')\n", + " plt.title(titles[i])\n", + " plt.axis('off')\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-KQytGBmdq0y" + }, + "source": [ + "Многоуровневая декомпозиция" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "output_embedded_package_id": "1i9Cm5n7h6VTlYMNGaajM76TMHCqBUzjE" + }, + "executionInfo": { + "elapsed": 4169, + "status": "ok", + "timestamp": 1732993395080, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "5h6qkfj7XvDS", + "outputId": "7a26fcd1-4abf-4dd6-a74f-aa9d8905eea9" + }, + "outputs": [], + "source": [ + "import cv2\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from scipy.special import expit\n", + "\n", + "# Загрузка изображения\n", + "image_path = \"/content/5.png\" # Укажите путь к вашему изображению\n", + "input_image = cv2.imread(image_path)\n", + "if input_image is None:\n", + " raise FileNotFoundError(f\"Не удалось загрузить изображение по указанному пути: {image_path}\")\n", + "\n", + "# Функция для отображения изображения\n", + "def show_image(image, title):\n", + " plt.figure(figsize=(10, 10))\n", + " plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))\n", + " plt.title(title)\n", + " plt.axis('off')\n", + " plt.show()\n", + "\n", + "# Функция для разложения изображения на базовый слой и детали\n", + "def multiscale_decomposition(image, kernel_size=15):\n", + " base_layer = cv2.GaussianBlur(image, (kernel_size, kernel_size), 0)\n", + " detail_layer_1 = cv2.subtract(image, base_layer)\n", + " detail_layer_2 = cv2.subtract(base_layer, cv2.GaussianBlur(base_layer, (kernel_size, kernel_size), 0))\n", + " return base_layer, detail_layer_1, detail_layer_2\n", + "\n", + "# Функция для применения сигмоиды к каждому пикселю\n", + "def sigmoid(x, a=1):\n", + " return expit(a * x)\n", + "\n", + "# Функция для применения тонового улучшения на основе трехуровневой декомпозиции\n", + "def multiscale_tone_enhancement(image, delta_0=1.0, delta_1=1.5, delta_2=1.2, alpha=1.0):\n", + " # Разложение изображения на базовый слой и два слоя детализации\n", + " base_layer, detail_layer_1, detail_layer_2 = multiscale_decomposition(image, kernel_size=15)\n", + "\n", + " # Среднее значение яркости для каждого канала\n", + " mu = np.mean(base_layer, axis=(0, 1))\n", + "\n", + " # Применение S-симгмоиды для управления усилением деталей\n", + " S = lambda x: sigmoid((x - mu) / 255.0, a=alpha)\n", + "\n", + " # Усиление базового слоя и слоев детализации\n", + " enhanced_image = base_layer + delta_0 * (base_layer - mu) + \\\n", + " 3 * S(detail_layer_1) * (delta_1 * detail_layer_1) + \\\n", + " 3 * S(detail_layer_2) * (delta_2 * detail_layer_2)\n", + "\n", + " # Клиппинг результата в допустимый диапазон [0, 255]\n", + " enhanced_image = np.clip(enhanced_image, 0, 255).astype(np.uint8)\n", + " return enhanced_image\n", + "\n", + "# Применение тонового улучшения\n", + "base_layer, detail_layer_1, detail_layer_2 = multiscale_decomposition(input_image, kernel_size=15)\n", + "\n", + "\n", + "# Отображение всех слоев\n", + "show_image(base_layer, 'Base Layer')\n", + "combined_detail_1 = cv2.add(input_image, detail_layer_1)\n", + "show_image(combined_detail_1, 'Detail Layer 1 Combined with Original')\n", + "combined_detail_2 = cv2.add(input_image, detail_layer_2)\n", + "show_image(combined_detail_2, 'Detail Layer 2 Combined with Original')\n", + "\n", + "enhanced_image = multiscale_tone_enhancement(input_image)\n", + "\n", + "# Отображение исходного и улучшенного изображения\n", + "show_image(input_image, 'Original Image')\n", + "show_image(enhanced_image, 'Enhanced Image')\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hnBtqO8gdk5O" + }, + "source": [] + } + ], + "metadata": { + "colab": { + "authorship_tag": "ABX9TyMDq1Nn8Tgi9q1bF37fu8j4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/seminars/Тема 1.1/Resource1.ipynb b/seminars/Тема 1.1/Resource1.ipynb new file mode 100644 index 0000000..294ed07 --- /dev/null +++ b/seminars/Тема 1.1/Resource1.ipynb @@ -0,0 +1,274 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "btIcF4lpu_Xx" + }, + "source": [ + "import requests\n", + "\n", + "# Пример запроса к TCIA API\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "j4rCtvc9u_XD" + }, + "source": [ + "**Шаг 1**. The Cancer Imaging Archive (TCIA) предоставляет доступ к медицинским изображениям через API. Вы можете использовать библиотеку requests для загрузки данных из TCIA:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "uR4WCv5yrzNl", + "outputId": "0a2a3495-5147-4bed-a209-c800c26cda8d" + }, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "# Пример запроса к TCIA API\n", + "url = \"https://services.cancerimagingarchive.net/services/v3/TCIA/query/getSeries?Collection=TCGA-BRCA\"\n", + "response = requests.get(url)\n", + "\n", + "# Проверка ответа\n", + "if response.status_code == 200:\n", + " print(\"Данные получены успешно\")\n", + "else:\n", + " print(\"Ошибка получения данных\")\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AlwoAu-ju7SC" + }, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LuW1z73XvxYF" + }, + "source": [ + "**Шаг 2.** Данные приходят в JSON формате.\n", + "В этом примере запрос отправляется с параметром format=json, чтобы вернуть данные в формате JSON.\n", + "Преобразование ответа в JSON:\n", + "response.json() позволяет конвертировать ответ в словарь Python, с которым будем работать дальше.\n", + "Обработка JSON-ответа:\n", + "Сначала печатаем весь JSON для ознакомления." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "0rA31BsAuGeM", + "outputId": "fac4ab13-ddbb-41fd-ade9-279df305554a" + }, + "outputs": [], + "source": [ + "import requests\n", + "import json\n", + "\n", + "# Пример запроса к TCIA API, предполагающий JSON формат\n", + "url = \"https://services.cancerimagingarchive.net/services/v3/TCIA/query/getSeries?Collection=TCGA-BRCA&format=json\"\n", + "response = requests.get(url)\n", + "\n", + "# Проверка ответа\n", + "if response.status_code == 200:\n", + " print(\"Данные получены успешно\")\n", + "\n", + " # Преобразование ответа в JSON\n", + " data = response.json()\n", + " print(json.dumps(data, indent=4)) # Печать данных в формате JSON с отступами\n", + "\n", + " # Обработка данных\n", + " for item in data:\n", + " series_instance_uid = item.get('SeriesInstanceUID', 'N/A')\n", + " modality = item.get('Modality', 'N/A')\n", + " protocol_name = item.get('ProtocolName', 'N/A')\n", + " body_part_examined = item.get('BodyPartExamined', 'N/A')\n", + "\n", + " # Печать извлеченной информации\n", + " print(f\"SeriesInstanceUID: {series_instance_uid}\")\n", + " print(f\"Modality: {modality}\")\n", + " print(f\"ProtocolName: {protocol_name}\")\n", + " print(f\"BodyPartExamined: {body_part_examined}\")\n", + " print('-' * 30)\n", + "else:\n", + " print(\"Ошибка получения данных\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "G3VSxAgX6RbK" + }, + "source": [ + "**Шаг3**.**Получение идентификаторов для загрузки изображений**\n", + "\n", + "Используя первый запрос к TCIA API, получили метаданные, такие как SeriesInstanceUID для каждой серии изображений.\n", + "Эти идентификаторы нужны для следующего шага — получения ссылок на сами изображения.\n", + "Запрос на загрузку изображений с использованием SeriesInstanceUID\n", + "Чтобы загрузить все изображения в определенной серии, нужно выполнить запрос к TCIA API с использованием SeriesInstanceUID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "AexV0o2gJahr", + "outputId": "06c3167c-931f-4f15-917e-a5fe6d7463d5" + }, + "outputs": [], + "source": [ + "import os\n", + "import requests\n", + "\n", + "# Пример использования SeriesInstanceUID\n", + "series_instance_uid = \"1.3.6.1.4.1.14519.5.2.1.6450.4002.265963154110062013582166845425\" # Пример идентификатора\n", + "\n", + "# URL для загрузки изображений\n", + "url = f\"https://services.cancerimagingarchive.net/services/v3/TCIA/query/getImage?SeriesInstanceUID={series_instance_uid}\"\n", + "response = requests.get(url, stream=True)\n", + "\n", + "# Путь для сохранения ZIP-файла\n", + "output_path = \"/content/images.zip\"\n", + "\n", + "# Создание необходимых папок, если их нет\n", + "folder_path = os.path.dirname(output_path)\n", + "os.makedirs(folder_path, exist_ok=True)\n", + "\n", + "# Проверка ответа и сохранение изображения\n", + "if response.status_code == 200:\n", + " print(f\"Сохранение файла в {output_path}\")\n", + " with open(output_path, \"wb\") as file:\n", + " for chunk in response.iter_content(chunk_size=128):\n", + " file.write(chunk)\n", + " print(f\"Изображения успешно загружены и сохранены как {output_path}\")\n", + "else:\n", + " print(\"Ошибка при загрузке изображений\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "-PwPLc8aKV9h", + "outputId": "821075b5-c777-451e-e84f-6ea3f69c092a" + }, + "outputs": [], + "source": [ + "import zipfile\n", + "\n", + "# Распаковка загруженного ZIP-файла\n", + "with zipfile.ZipFile(\"images.zip\", 'r') as zip_ref:\n", + " zip_ref.extractall(\"images\") # Папка для распакованных изображений\n", + "print(\"Файлы успешно распакованы\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DVVYma3wM4vf" + }, + "source": [ + "**Шаг. 4**# Для чтения DICOM файлов используйте pydicom, а для визуализации изображений — matplotlib." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Tw2PTwQrOlLe", + "outputId": "d21f3b3b-c985-45ad-fdc6-a8c36cd10335" + }, + "outputs": [], + "source": [ + "!pip install pydicom\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 839 + }, + "id": "OLMmOFkCM59-", + "outputId": "f4370e76-ad70-45da-8192-fe2de2232455" + }, + "outputs": [], + "source": [ + "import os\n", + "import pydicom\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Папка с распакованными изображениями\n", + "dicom_folder = \"images\"\n", + "\n", + "# Чтение всех DICOM-файлов в папке и визуализация первого изображения\n", + "for filename in os.listdir(dicom_folder):\n", + " if filename.endswith(\".dcm\"):\n", + " dicom_path = os.path.join(dicom_folder, filename)\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + "\n", + " # Получение массива пикселей изображения\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Визуализация изображения\n", + " plt.imshow(image, cmap=\"gray\")\n", + " plt.title(f\"Пациент ID: {dicom_data.PatientID}\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n", + " break # Визуализируем только первое изображение\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "U-9TfVjKQsFV" + }, + "source": [ + " библиотеки, как OpenCV, scikit-image, и NumPy предоставляют множество инструментов для фильтрации, повышения контраста и удаления шума." + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/seminars/Тема 1.1/resource2.ipynb b/seminars/Тема 1.1/resource2.ipynb new file mode 100644 index 0000000..9901bff --- /dev/null +++ b/seminars/Тема 1.1/resource2.ipynb @@ -0,0 +1,204 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "9B_sc-M_NlTZ" + }, + "source": [ + "Шаг 1. Устанавливаем библиотеку для работы с загрузкой данных по ссылке" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "OsQErPaDDkmK", + "outputId": "49149067-e8b9-42f8-acbe-15b2d80cd79e" + }, + "outputs": [], + "source": [ + "!pip install gdown\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_F6Rmsu4NviH" + }, + "source": [ + "Шаг 3. Скачиваем и распаковываем файлы,з ная ID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "d19wjKXOLIyw", + "outputId": "804d657b-dce7-4822-c631-b6a330801207" + }, + "outputs": [], + "source": [ + "import gdown\n", + "\n", + "# ID файла с Google Диска\n", + "file_id = '1qVrpV7vmhIsUxFiH189LmAn0ALbAPrgS'\n", + "#https://drive.google.com/file/d/1qVrpV7vmhIsUxFiH189LmAn0ALbAPrgS/view?usp=drive_link\n", + "# Формируем URL для скачивания\n", + "url = f'https://drive.google.com/uc?id={file_id}'\n", + "\n", + "# Скачиваем файл в директорию /content/\n", + "file_path = '/content/my_file_name.tar'\n", + "gdown.download(url, file_path, quiet=False)\n", + "\n", + "# Распаковываем архив .tar\n", + "import tarfile\n", + "with tarfile.open(file_path, 'r') as tar:\n", + " tar.extractall(path='/content/')\n", + " print(\"Архив распакован в директорию /content/\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YNJiS-a6N4vg" + }, + "source": [ + "Шаг 4. Просматриваем структуру и описание данных в JSON" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "_G-7skCHPJZ2", + "outputId": "f0d8712c-1805-4a5d-c857-3c6b6659fc71" + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "# Путь к файлу JSON\n", + "json_file_path = '/content/Task08_HepaticVessel/dataset.json'\n", + "\n", + "# Загружаем содержимое JSON файла\n", + "with open(json_file_path, 'r') as f:\n", + " dataset_info = json.load(f)\n", + "\n", + "# Просмотрим содержимое JSON\n", + "print(json.dumps(dataset_info, indent=4)) # Красиво выводим содержимое\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4GOfTVYdODLL" + }, + "source": [ + "Шаг 5. Выводим для просмотра 20 случайных объектов с метками из обучающего набора." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "CTMaymbpP1IQ", + "outputId": "e6ce5b6a-b949-4499-93e5-a7f8816bc888" + }, + "outputs": [], + "source": [ + "import random\n", + "import nibabel as nib\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import os\n", + "\n", + "# Папки с изображениями и метками\n", + "images_dir = '/content/Task08_HepaticVessel/imagesTr'\n", + "labels_dir = '/content/Task08_HepaticVessel/labelsTr'\n", + "\n", + "# Фильтруем доступные файлы\n", + "available_files = sorted([f for f in os.listdir(images_dir) if not f.startswith('._') and f.endswith('.nii.gz')])\n", + "images = [os.path.join(images_dir, file) for file in available_files]\n", + "labels = [os.path.join(labels_dir, file.replace('imagesTr', 'labelsTr')) for file in available_files]\n", + "\n", + "# Проверяем количество файлов\n", + "print(f\"Обнаружено {len(images)} изображений и {len(labels)} меток.\")\n", + "\n", + "# Случайно выбираем 20 изображений\n", + "random_indices = random.sample(range(len(images)), min(20, len(images)))\n", + "\n", + "# Цветовая карта для меток\n", + "cmap = plt.cm.colors.ListedColormap(['black', 'green', 'red']) # Цвета: фон, сосуды, опухоли\n", + "bounds = [0, 1, 2, 3]\n", + "norm = plt.cm.colors.BoundaryNorm(bounds, cmap.N)\n", + "\n", + "# Визуализация изображений и меток\n", + "for idx in random_indices:\n", + " try:\n", + " # Загружаем изображение и метку\n", + " image = nib.load(images[idx]).get_fdata()\n", + " label = nib.load(labels[idx]).get_fdata()\n", + "\n", + " # Поиск срезов с ненулевыми метками\n", + " non_empty_slices = [z for z in range(label.shape[2]) if np.unique(label[:, :, z]).size > 1]\n", + "\n", + " if not non_empty_slices:\n", + " print(f\"Все срезы в {images[idx]} содержат только фон.\")\n", + " continue\n", + "\n", + " # Выбираем случайный срез с ненулевыми метками\n", + " slice_index = random.choice(non_empty_slices)\n", + " print(f\"Выбран срез {slice_index} для изображения {images[idx]}\")\n", + "\n", + " plt.figure(figsize=(10, 5))\n", + "\n", + " # Отображаем изображение\n", + " plt.subplot(1, 2, 1)\n", + " plt.imshow(image[:, :, slice_index], cmap='gray')\n", + " plt.title(f'Изображение (Slice: {slice_index})')\n", + " plt.axis('off')\n", + "\n", + " # Отображаем метки\n", + " plt.subplot(1, 2, 2)\n", + " plt.imshow(label[:, :, slice_index], cmap=cmap, norm=norm)\n", + " plt.title('Метки')\n", + " plt.axis('off')\n", + "\n", + " plt.show()\n", + "\n", + " except Exception as e:\n", + " print(f\"Ошибка при обработке файла {images[idx]}: {e}\")\n" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/seminars/Тема 1.1/segm2.ipynb b/seminars/Тема 1.1/segm2.ipynb new file mode 100644 index 0000000..8c114a3 --- /dev/null +++ b/seminars/Тема 1.1/segm2.ipynb @@ -0,0 +1,1684 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 10197, + "status": "ok", + "timestamp": 1733327907192, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "s5gldFUP1Ta9", + "outputId": "0f3abd7a-48e9-4166-9d9f-9ee6ff65c32e" + }, + "outputs": [], + "source": [ + "import requests\n", + "import json\n", + "\n", + "# Пример запроса к TCIA API, предполагающий JSON формат\n", + "url = \"https://services.cancerimagingarchive.net/services/v3/TCIA/query/getSeries?Collection=TCGA-BRCA&format=json\"\n", + "response = requests.get(url)\n", + "\n", + "# Проверка ответа\n", + "if response.status_code == 200:\n", + " print(\"Данные получены успешно\")\n", + "\n", + " # Преобразование ответа в JSON\n", + " try:\n", + " data = response.json()\n", + " if data:\n", + " print(json.dumps(data, indent=4)) # Печать данных в формате JSON с отступами\n", + "\n", + " # Обработка данных\n", + " for item in data:\n", + " series_instance_uid = item.get('SeriesInstanceUID', 'N/A')\n", + " modality = item.get('Modality', 'N/A')\n", + " protocol_name = item.get('ProtocolName', 'N/A')\n", + " body_part_examined = item.get('BodyPartExamined', 'N/A')\n", + "\n", + " # Печать извлеченной информации\n", + " print(f\"SeriesInstanceUID: {series_instance_uid}\")\n", + " print(f\"Modality: {modality}\")\n", + " print(f\"ProtocolName: {protocol_name}\")\n", + " print(f\"BodyPartExamined: {body_part_examined}\")\n", + " print('-' * 30)\n", + " else:\n", + " print(\"Нет данных для отображения.\")\n", + " except json.JSONDecodeError:\n", + " print(\"Ошибка преобразования ответа в JSON.\")\n", + "else:\n", + " print(f\"Ошибка получения данных. Статус код: {response.status_code}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1EMLEteH2PP8" + }, + "source": [ + "получение изображений" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 8511, + "status": "ok", + "timestamp": 1733327923136, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "_jqz40802ScS", + "outputId": "f36da207-d5e2-4753-ca64-3c38c52bbc61" + }, + "outputs": [], + "source": [ + "import os\n", + "import requests\n", + "\n", + "# Пример использования SeriesInstanceUID\n", + "series_instance_uid = \"1.3.6.1.4.1.14519.5.2.1.6450.4002.265963154110062013582166845425\" # Пример идентификатора\n", + "\n", + "# URL для загрузки изображений\n", + "url = f\"https://services.cancerimagingarchive.net/services/v3/TCIA/query/getImage?SeriesInstanceUID={series_instance_uid}\"\n", + "response = requests.get(url, stream=True)\n", + "\n", + "# Путь для сохранения ZIP-файла\n", + "output_path = \"/content/images.zip\"\n", + "\n", + "# Создание необходимых папок, если их нет\n", + "folder_path = os.path.dirname(output_path)\n", + "os.makedirs(folder_path, exist_ok=True)\n", + "\n", + "# Проверка ответа и сохранение изображения\n", + "if response.status_code == 200:\n", + " print(f\"Сохранение файла в {output_path}\")\n", + " with open(output_path, \"wb\") as file:\n", + " for chunk in response.iter_content(chunk_size=128):\n", + " file.write(chunk)\n", + " print(f\"Изображения успешно загружены и сохранены как {output_path}\")\n", + "else:\n", + " print(\"Ошибка при загрузке изображений\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 487, + "status": "ok", + "timestamp": 1733327927800, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "5T9WU8e32ZwO", + "outputId": "8607816e-7b09-45e6-f2ee-028195091358" + }, + "outputs": [], + "source": [ + "import zipfile\n", + "\n", + "# Распаковка загруженного ZIP-файла\n", + "with zipfile.ZipFile(\"images.zip\", 'r') as zip_ref:\n", + " zip_ref.extractall(\"images\") # Папка для распакованных изображений\n", + "print(\"Файлы успешно распакованы\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 9944, + "status": "ok", + "timestamp": 1733327954427, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "wDnQciCD2nK7", + "outputId": "114de64d-1dad-4e0f-fc25-6eb2f6f2be3c" + }, + "outputs": [], + "source": [ + "!pip install pydicom" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 5672, + "status": "ok", + "timestamp": 1733327939856, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "fy7iue6r-qLe", + "outputId": "90f7405d-77ba-42cb-bcd1-daba88d8e46b" + }, + "outputs": [], + "source": [ + "pip install scikit-image" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mHUiH_ra73lm" + }, + "source": [ + "Вывод 20 случайных изображений серии" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XLX5F2QA73ku" + }, + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "output_embedded_package_id": "1bVzJQoS5a20xhtxUvdmWXcv0JVIyOj1_" + }, + "executionInfo": { + "elapsed": 6961, + "status": "ok", + "timestamp": 1733161002828, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "ZG1Ab2X-3b3W", + "outputId": "c8912e6e-d792-4147-ef1b-767e8c181ae3" + }, + "outputs": [], + "source": [ + "import os\n", + "import random\n", + "import pydicom\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Папка с распакованными изображениями\n", + "dicom_folder = \"images\"\n", + "\n", + "# Сбор всех DICOM-файлов в папке\n", + "dicom_files = [os.path.join(dicom_folder, filename) for filename in os.listdir(dicom_folder) if filename.endswith(\".dcm\")]\n", + "\n", + "# Проверка, что в папке достаточно файлов для выбора 20\n", + "num_files_to_show = min(20, len(dicom_files))\n", + "\n", + "# Выбор случайных файлов из списка случайным образом\n", + "random_files = random.sample(dicom_files, num_files_to_show)\n", + "\n", + "# Визуализация выбранных изображений\n", + "for dicom_path in random_files:\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + "\n", + " # Получение массива пикселей изображения\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Визуализация изображения\n", + " filename = os.path.basename(dicom_path)\n", + " plt.imshow(image, cmap=\"gray\")\n", + " plt.title(f\"Файл: {filename}\\nПациент ID: {dicom_data.PatientID}\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "S33pbEOx3b2k" + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2tugza_l6CgF" + }, + "source": [ + "После выбора изображениq, применим к нему обработку" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 878 + }, + "executionInfo": { + "elapsed": 23682, + "status": "ok", + "timestamp": 1733136739686, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "u0rx2vLS6oYm", + "outputId": "41fee910-4786-4c12-eeea-3a7b55ba6282" + }, + "outputs": [], + "source": [ + "import os\n", + "import random\n", + "import pydicom\n", + "import matplotlib.pyplot as plt\n", + "import cv2\n", + "import numpy as np\n", + "import skimage.restoration as restoration\n", + "\n", + "# Папка с распакованными изображениями\n", + "dicom_folder = \"images\"\n", + "\n", + "# Сбор всех DICOM-файлов в папке\n", + "dicom_files = [filename for filename in os.listdir(dicom_folder) if filename.endswith(\".dcm\")]\n", + "\n", + "# Запрос у пользователя названий файлов для отображения\n", + "file_names = input(\"Введите названия файлов изображений через запятую: \").split(\",\")\n", + "file_names = [name.strip() for name in file_names]\n", + "\n", + "# Поиск указанных файлов в папке\n", + "selected_files = []\n", + "for filename in file_names:\n", + " found_files = [os.path.join(dicom_folder, f) for f in dicom_files if filename in f]\n", + " selected_files.extend(found_files)\n", + "\n", + "# Визуализация выбранных изображений\n", + "for dicom_path in selected_files:\n", + " if os.path.exists(dicom_path):\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + "\n", + " # Получение массива пикселей изображения\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Визуализация изображения\n", + " filename = os.path.basename(dicom_path)\n", + " plt.imshow(image, cmap=\"gray\")\n", + " plt.title(f\"Файл: {filename}\\nПациент ID: {dicom_data.PatientID}\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n", + "\n", + "# Применение адаптивного медианного фильтра для улучшения качества изображений перед сегментацией\n", + "for dicom_path in selected_files:\n", + " if os.path.exists(dicom_path):\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Преобразование изображения в формат uint8, если это необходимо\n", + " if image.dtype != np.uint8:\n", + " image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n", + "\n", + " # Применение адаптивного медианного фильтра для сглаживания шума\n", + " filtered_image = restoration.denoise_bilateral(image, sigma_color=0.01, sigma_spatial=11, channel_axis=None)\n", + "\n", + " # Преобразование фильтрованного изображения обратно в формат uint8\n", + " filtered_image = (filtered_image * 255).astype(np.uint8)\n", + "\n", + " # Визуализация фильтрованного изображения\n", + " filename = os.path.basename(dicom_path)\n", + " plt.imshow(filtered_image, cmap=\"gray\")\n", + " plt.title(f\"Фильтрованное изображение: {filename}\")\n", + " plt.axis(\"off\")\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LzHtNa0oAWzx" + }, + "source": [ + "**watershed**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "executionInfo": { + "elapsed": 23251, + "status": "ok", + "timestamp": 1733161357153, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "_W5paV7yAaKX", + "outputId": "f9eb183c-24a7-4187-a8ab-7e82f1072fd9" + }, + "outputs": [], + "source": [ + "import os\n", + "import random\n", + "import pydicom\n", + "import matplotlib.pyplot as plt\n", + "import cv2\n", + "import numpy as np\n", + "import skimage.restoration as restoration\n", + "\n", + "# Папка с распакованными изображениями\n", + "dicom_folder = \"images\"\n", + "\n", + "# Сбор всех DICOM-файлов в папке\n", + "dicom_files = [filename for filename in os.listdir(dicom_folder) if filename.endswith(\".dcm\")]\n", + "\n", + "# Запрос у пользователя названий файлов для отображения\n", + "file_names = input(\"Введите названия файлов изображений через запятую: \").split(\",\")\n", + "file_names = [name.strip() for name in file_names]\n", + "\n", + "# Поиск указанных файлов в папке\n", + "selected_files = []\n", + "for filename in file_names:\n", + " found_files = [os.path.join(dicom_folder, f) for f in dicom_files if filename in f]\n", + " selected_files.extend(found_files)\n", + "\n", + "# Визуализация выбранных изображений\n", + "for dicom_path in selected_files:\n", + " if os.path.exists(dicom_path):\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + "\n", + " # Получение массива пикселей изображения\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Обрезка верхней 1/3 изображения\n", + " height = image.shape[0]\n", + " cropped_image = image[height // 3:, :]\n", + "\n", + " # Применение адаптивного выравнивания гистограммы (CLAHE)\n", + " clahe = cv2.createCLAHE(clipLimit=0.5, tileGridSize=(3, 3))\n", + " cropped_image = clahe.apply(cropped_image.astype(np.uint8))\n", + "\n", + " # Визуализация обрезанного изображения\n", + " filename = os.path.basename(dicom_path)\n", + " plt.imshow(cropped_image, cmap=\"gray\")\n", + " plt.title(f\"Обрезанное изображение: {filename}\\nПациент ID: {dicom_data.PatientID}\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n", + "\n", + "# Применение адаптивного медианного фильтра для улучшения качества изображений перед сегментацией\n", + "for dicom_path in selected_files:\n", + " if os.path.exists(dicom_path):\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Обрезка верхней 1/3 изображения\n", + " height = image.shape[0]\n", + " cropped_image = image[height // 3:, :]\n", + "\n", + " # Преобразование изображения в формат uint8, если это необходимо\n", + " if cropped_image.dtype != np.uint8:\n", + " cropped_image = cv2.normalize(cropped_image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n", + "\n", + " # Применение адаптивного медианного фильтра для сглаживания шума\n", + " filtered_image = restoration.denoise_bilateral(cropped_image, sigma_color=0.05, sigma_spatial=5, channel_axis=None)\n", + "\n", + " # Преобразование фильтрованного изображения обратно в формат uint8\n", + " filtered_image = (filtered_image * 255).astype(np.uint8)\n", + "\n", + " # Применение алгоритма водораздела для сегментации изображения\n", + " # Создание маски для водораздела\n", + " _, binary_image = cv2.threshold(filtered_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n", + " distance_transform = cv2.distanceTransform(binary_image, cv2.DIST_L2, 5)\n", + " _, sure_fg = cv2.threshold(distance_transform, 0.9 * distance_transform.max(), 255, 0)\n", + " sure_fg = np.uint8(sure_fg)\n", + " unknown = cv2.subtract(binary_image, sure_fg)\n", + "\n", + " # Маркировка объектов\n", + " num_markers, markers = cv2.connectedComponents(sure_fg)\n", + " markers = markers + 1\n", + " markers[unknown == 255] = 0\n", + "\n", + " # Применение водораздела\n", + " markers = cv2.watershed(cv2.cvtColor(filtered_image, cv2.COLOR_GRAY2BGR), markers)\n", + " segmented_image = np.zeros_like(filtered_image)\n", + " segmented_image[markers == -1] = 255\n", + "\n", + " # Наложение сегментированного изображения на исходное с зелеными границами\n", + " original_image_bgr = cv2.cvtColor(cropped_image, cv2.COLOR_GRAY2BGR)\n", + " original_image_bgr[markers == -1] = [0, 255, 0] # Установка зеленого цвета для границ\n", + "\n", + " # Визуализация наложенного изображения\n", + " filename = os.path.basename(dicom_path)\n", + " plt.imshow(cv2.cvtColor(original_image_bgr, cv2.COLOR_BGR2RGB))\n", + " plt.title(f\"Сегментированное изображение с наложением (водораздел): {filename}\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bbDf5ciOYHfS" + }, + "source": [ + "thresholding" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "executionInfo": { + "elapsed": 8619, + "status": "ok", + "timestamp": 1733161943856, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "7YbBnF0fYLNw", + "outputId": "29ed2b7a-1627-456a-dd32-86d0c4b06538" + }, + "outputs": [], + "source": [ + "import os\n", + "import random\n", + "import pydicom\n", + "import matplotlib.pyplot as plt\n", + "import cv2\n", + "import numpy as np\n", + "\n", + "# Папка с распакованными изображениями\n", + "dicom_folder = \"images\"\n", + "\n", + "# Сбор всех DICOM-файлов в папке\n", + "dicom_files = [filename for filename in os.listdir(dicom_folder) if filename.endswith(\".dcm\")]\n", + "\n", + "# Запрос у пользователя названий файлов для отображения\n", + "file_names = input(\"Введите названия файлов изображений через запятую: \").split(\",\")\n", + "file_names = [name.strip() for name in file_names]\n", + "\n", + "# Поиск указанных файлов в папке\n", + "selected_files = []\n", + "for filename in file_names:\n", + " found_files = [os.path.join(dicom_folder, f) for f in dicom_files if filename in f]\n", + " selected_files.extend(found_files)\n", + "\n", + "# Визуализация выбранных изображений\n", + "for dicom_path in selected_files:\n", + " if os.path.exists(dicom_path):\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + "\n", + " # Получение массива пикселей изображения\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Визуализация изображения\n", + " filename = os.path.basename(dicom_path)\n", + " plt.imshow(image, cmap=\"gray\")\n", + " plt.title(f\"Файл: {filename}\\nПациент ID: {dicom_data.PatientID}\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n", + "\n", + "# Применение обработки изображений перед сегментацией\n", + "for dicom_path in selected_files:\n", + " if os.path.exists(dicom_path):\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Обрезка верхней трети изображения\n", + " height, width = image.shape\n", + " cropped_image = image[height // 3: , :]\n", + "\n", + " # Преобразование изображения в формат uint8, если это необходимо\n", + " if cropped_image.dtype != np.uint8:\n", + " cropped_image = cv2.normalize(cropped_image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n", + "\n", + " # Применение адаптивного выравнивания гистограммы для повышения контрастности\n", + " clahe = cv2.createCLAHE(clipLimit=1.5, tileGridSize=(8, 8))\n", + " enhanced_image = clahe.apply(cropped_image)\n", + "\n", + " # Визуализация улучшенного изображения\n", + " filename = os.path.basename(dicom_path)\n", + " plt.imshow(enhanced_image, cmap=\"gray\")\n", + " plt.title(f\"Улучшенное изображение: {filename}\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n", + "\n", + " # Применение пороговой сегментации\n", + " threshold_value = 100 # Порог можно изменять в зависимости от задачи\n", + " _, thresholded_image = cv2.threshold(enhanced_image, threshold_value, 255, cv2.THRESH_BINARY)\n", + "\n", + " # Визуализация пороговой сегментации\n", + " plt.imshow(thresholded_image, cmap=\"gray\")\n", + " plt.title(f\"Пороговая сегментация: {filename}\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n", + "\n", + " # Наложение сегментированного изображения на исходное с зеленой линией\n", + " contours, _ = cv2.findContours(thresholded_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n", + " overlay_image = cv2.cvtColor(cropped_image, cv2.COLOR_GRAY2BGR)\n", + " cv2.drawContours(overlay_image, contours, -1, (0, 255, 0), 1)\n", + "\n", + " # Визуализация наложенного изображения\n", + " plt.imshow(cv2.cvtColor(overlay_image, cv2.COLOR_BGR2RGB))\n", + " plt.title(f\"Наложенное изображение: {filename}\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "F5PIzY6Y7fXU" + }, + "source": [ + "FuzzyCmeans" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 3985, + "status": "ok", + "timestamp": 1733236827403, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "DPAimyHw8dSq", + "outputId": "c8edd6bd-9fe7-4595-b7b6-2152cfeda19c" + }, + "outputs": [], + "source": [ + "pip install fuzzy-c-means" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8RAcDcRi-ZlV" + }, + "outputs": [], + "source": [ + "pip install tabulate>=0.9.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ak9OshsrOLrl" + }, + "source": [ + "Обработка изображения - многослойная декомпозиция. Сегментация с Fuzzy C means" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "executionInfo": { + "elapsed": 5336, + "status": "ok", + "timestamp": 1733162965578, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "IhGYJoriAXP_", + "outputId": "0ac944eb-efe4-4516-e084-7a8adfccc256" + }, + "outputs": [], + "source": [ + "import cv2\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from scipy.special import expit\n", + "from fcmeans import FCM # Для Fuzzy C-means\n", + "\n", + "# Загрузка изображения\n", + "image_path = \"/content/8.png\" # Укажите путь к вашему изображению\n", + "input_image = cv2.imread(image_path)\n", + "if input_image is None:\n", + " raise FileNotFoundError(f\"Не удалось загрузить изображение по указанному пути: {image_path}\")\n", + "\n", + "# Функция для отображения изображения\n", + "def show_image(image, title):\n", + " plt.figure(figsize=(10, 10))\n", + " plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))\n", + " plt.title(title)\n", + " plt.axis('off')\n", + " plt.show()\n", + "\n", + "# Функция для разложения изображения на базовый слой и детали\n", + "def multiscale_decomposition(image, kernel_size=5):\n", + " base_layer = cv2.GaussianBlur(image, (kernel_size, kernel_size), 0)\n", + " detail_layer_1 = cv2.subtract(image, base_layer)\n", + " detail_layer_2 = cv2.subtract(base_layer, cv2.GaussianBlur(base_layer, (kernel_size, kernel_size), 0))\n", + " return base_layer, detail_layer_1, detail_layer_2\n", + "\n", + "# Функция для применения сигмоиды к каждому пикселю\n", + "def sigmoid(x, a=1):\n", + " return expit(a * x)\n", + "\n", + "# Функция для применения тонового улучшения на основе трехуровневой декомпозиции\n", + "def multiscale_tone_enhancement(image, delta_0=1.1, delta_1=2.5, delta_2=2.2, alpha=0.5):\n", + " # Разложение изображения на базовый слой и два слоя детализации\n", + " base_layer, detail_layer_1, detail_layer_2 = multiscale_decomposition(image, kernel_size=5)\n", + "\n", + " # Среднее значение яркости для каждого канала\n", + " mu = np.mean(base_layer, axis=(0, 1))\n", + "\n", + " # Применение S-симгмоиды для управления усилением деталей\n", + " S = lambda x: sigmoid((x - mu) / 255.0, a=alpha)\n", + "\n", + " # Усиление базового слоя и слоев детализации\n", + " enhanced_image = base_layer + delta_0 * (base_layer - mu) + \\\n", + " 0.9 * S(detail_layer_1) * (delta_1 * detail_layer_1) + \\\n", + " 0.9 * S(detail_layer_2) * (delta_2 * detail_layer_2)\n", + "\n", + " # Клиппинг результата в допустимый диапазон [0, 255]\n", + " enhanced_image = np.clip(enhanced_image, 0, 255).astype(np.uint8)\n", + " return enhanced_image\n", + "\n", + "# Применение тонового улучшения\n", + "enhanced_image = multiscale_tone_enhancement(input_image)\n", + "\n", + "# Преобразование изображения в градации серого для FCM\n", + "gray_image = cv2.cvtColor(enhanced_image, cv2.COLOR_BGR2GRAY)\n", + "\n", + "# Преобразование изображения в формат, подходящий для FCM\n", + "reshaped_image = gray_image.flatten().astype(np.float64) / 255.0\n", + "\n", + "# Применение Fuzzy C-means\n", + "num_clusters = 5 # Задайте количество кластеров\n", + "fcm = FCM(n_clusters=num_clusters)\n", + "fcm.fit(reshaped_image.reshape(-1, 1))\n", + "cluster_labels = fcm.u.argmax(axis=1) # Метки кластеров\n", + "\n", + "# Преобразование кластеров в форму изображения\n", + "segmented_image = cluster_labels.reshape(gray_image.shape)\n", + "\n", + "# Отображение результатов\n", + "show_image(input_image, 'Original Image')\n", + "show_image(enhanced_image, 'Enhanced Image')\n", + "plt.figure(figsize=(10, 10))\n", + "plt.imshow(segmented_image, cmap=\"jet\")\n", + "plt.title(\"Fuzzy C-means Segmentation\")\n", + "plt.axis('off')\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v30wSTf2z2KX" + }, + "source": [ + "Метод активных контуров - змеек" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 3699, + "status": "ok", + "timestamp": 1733327984318, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "8w6lib4r14jZ", + "outputId": "8e5c5470-d875-4f42-8d53-2751fb295d8e" + }, + "outputs": [], + "source": [ + "pip install scipy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "executionInfo": { + "elapsed": 5058, + "status": "ok", + "timestamp": 1733330809629, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "8JoBvPgwdlM1", + "outputId": "1fa592ef-8270-4b25-c99f-2fe69110a350" + }, + "outputs": [], + "source": [ + "import os\n", + "import matplotlib.pyplot as plt\n", + "import cv2\n", + "import numpy as np\n", + "from skimage.segmentation import active_contour\n", + "from scipy.spatial import distance\n", + "\n", + "# Функция для применения адаптивного медианного фильтра\n", + "def adaptive_median_filter(image, max_kernel_size=5):\n", + " filtered_image = image.copy()\n", + " height, width = image.shape\n", + "\n", + " for i in range(height):\n", + " for j in range(width):\n", + " kernel_size = 3 # Начальный размер ядра\n", + " while kernel_size <= max_kernel_size:\n", + " # Получаем область с заданным размером ядра\n", + " half_size = kernel_size // 2\n", + " x_start = max(j - half_size, 0)\n", + " x_end = min(j + half_size + 1, width)\n", + " y_start = max(i - half_size, 0)\n", + " y_end = min(i + half_size + 1, height)\n", + "\n", + " region = image[y_start:y_end, x_start:x_end]\n", + " median_value = np.median(region)\n", + "\n", + " # Если медианное значение близко к текущему пикселю, увеличиваем размер ядра\n", + " if abs(median_value - image[i, j]) < 10: # Порог можно настроить\n", + " break\n", + " kernel_size += 2 # Увеличиваем размер ядра, проверяя на большее\n", + " filtered_image[i, j] = median_value\n", + "\n", + " return filtered_image\n", + "\n", + "# Функция для объединения пересекающихся контуров\n", + "def merge_contours(contours, min_distance=10):\n", + " merged_contours = []\n", + "\n", + " while contours:\n", + " contour = contours.pop(0)\n", + " merged = False\n", + " for i, existing_contour in enumerate(merged_contours):\n", + " # Проверяем, пересекаются ли контуры (минимальное расстояние между точками)\n", + " if any(distance.euclidean(point, existing_point) < min_distance for point in contour for existing_point in existing_contour):\n", + " # Если контуры пересекаются, объединяем их\n", + " merged_contours[i] = np.vstack((existing_contour, contour))\n", + " merged = True\n", + " break\n", + " if not merged:\n", + " merged_contours.append(contour)\n", + "\n", + " return merged_contours\n", + "\n", + "# Загрузка изображения (например, PNG)\n", + "image_path = \"/content/9.png\" # Укажите путь к вашему изображению\n", + "input_image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)\n", + "\n", + "if input_image is None:\n", + " raise FileNotFoundError(f\"Не удалось загрузить изображение по указанному пути: {image_path}\")\n", + "\n", + "# Обрезка верхней трети изображения (три восьмых сверху)\n", + "cropped_image = input_image\n", + "\n", + "# Преобразование изображения в формат uint8, если это необходимо\n", + "if cropped_image.dtype != np.uint8:\n", + " cropped_image = cv2.normalize(cropped_image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n", + "\n", + "# Применение адаптивного медианного фильтра\n", + "adaptive_filtered = adaptive_median_filter(cropped_image)\n", + "\n", + "# Визуализация результата CLAHE\n", + "plt.figure(figsize=(6, 6))\n", + "plt.imshow(adaptive_filtered, cmap=\"gray\")\n", + "plt.axis(\"off\")\n", + "plt.show()\n", + "\n", + "# Инвертирование изображения перед сегментацией\n", + "inverted_image = np.invert(adaptive_filtered)\n", + "\n", + "# Применение пороговой сегментации для выделения светлых участков на инвертированном изображении\n", + "gray_threshold = np.percentile(inverted_image, 90) # Порог для серых участков, 70-й перцентиль\n", + "binary_image = inverted_image > gray_threshold # Выделяем светлые участки\n", + "\n", + "# Визуализация результата пороговой сегментации\n", + "plt.figure(figsize=(6, 6))\n", + "plt.imshow(binary_image, cmap=\"gray\")\n", + "plt.title(\"Результат пороговой сегментации (светлые участки на инвертированном изображении)\")\n", + "plt.axis(\"off\")\n", + "plt.show()\n", + "\n", + "# Найдем контуры светлых областей на бинаризованном изображении\n", + "contours, _ = cv2.findContours(binary_image.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n", + "\n", + "# Отсортируем контуры по площади и выберем 2 самых крупных\n", + "contours = sorted(contours, key=cv2.contourArea, reverse=True)[:1] # Выбираем только 2 контуры\n", + "\n", + "# Генерация начальных точек для активных контуров\n", + "init_points = []\n", + "for contour in contours:\n", + " # Найдем центры контуров\n", + " M = cv2.moments(contour)\n", + " if M['m00'] != 0:\n", + " cx = int(M['m10'] / M['m00'])\n", + " cy = int(M['m01'] / M['m00'])\n", + " init_points.append((cy, cx))\n", + "\n", + "# Инициализация контуров с центрами\n", + "init_contours = []\n", + "for point in init_points:\n", + " y, x = point\n", + " s = np.linspace(0, 2 * np.pi, 400)\n", + " r = y + 90 * np.sin(s)\n", + " c = x + 90 * np.cos(s)\n", + " init = np.array([r, c]).T\n", + " init_contours.append(init)\n", + "\n", + "# Применение метода активных контуров\n", + "fig, ax = plt.subplots()\n", + "ax.imshow(inverted_image, cmap=\"gray\") # Выводим контуры на исходном изображении\n", + "\n", + "# Объединяем пересекающиеся контуры\n", + "merged_contours = merge_contours(init_contours, min_distance=1)\n", + "\n", + "# Отображаем объединенные контуры\n", + "for init in merged_contours:\n", + " snake = active_contour(inverted_image, init, alpha=0.05, beta=0.1, gamma=0.001) # Параметры активного контура\n", + " snake[:, 0] = np.clip(snake[:, 0], 0, inverted_image.shape[0] - 1) # ограничиваем по оси Y\n", + " snake[:, 1] = np.clip(snake[:, 1], 0, inverted_image.shape[1] - 1) # ограничиваем по оси X\n", + "\n", + " # Проверяем, чтобы контуры не были в темных областях\n", + " contour_values = inverted_image[tuple(snake.astype(int).T)]\n", + "\n", + " # Если средняя яркость вдоль контура больше порога, рисуем контур\n", + " if np.mean(contour_values) >105: # Порог для яркости, можно настроить\n", + " ax.plot(snake[:, 1], snake[:, 0], '-r', lw=2)\n", + "\n", + "ax.set_title(f\"Объединенные активные контуры: {os.path.basename(image_path)}\")\n", + "ax.axis(\"off\")\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "background_save": true + }, + "id": "eKwxXTinna1u" + }, + "outputs": [], + "source": [ + "import os\n", + "import pydicom\n", + "import matplotlib.pyplot as plt\n", + "import cv2\n", + "import numpy as np\n", + "from skimage.segmentation import active_contour\n", + "from scipy.spatial import distance\n", + "from skimage.morphology import disk\n", + "\n", + "# Папка с распакованными изображениями\n", + "dicom_folder = \"images\"\n", + "\n", + "# Сбор всех DICOM-файлов в папке\n", + "dicom_files = [filename for filename in os.listdir(dicom_folder) if filename.endswith(\".dcm\")]\n", + "\n", + "# Запрос у пользователя названий файлов для отображения\n", + "file_names = input(\"Введите названия файлов изображений через запятую: \").split(\",\")\n", + "file_names = [name.strip() for name in file_names]\n", + "\n", + "# Поиск указанных файлов в папке\n", + "selected_files = []\n", + "for filename in file_names:\n", + " found_files = [os.path.join(dicom_folder, f) for f in dicom_files if filename in f]\n", + " selected_files.extend(found_files)\n", + "\n", + "# Функция для применения адаптивного медианного фильтра\n", + "def adaptive_median_filter(image, max_kernel_size=15):\n", + " filtered_image = image.copy()\n", + " height, width = image.shape\n", + "\n", + " for i in range(height):\n", + " for j in range(width):\n", + " kernel_size = 3 # Начальный размер ядра\n", + " while kernel_size <= max_kernel_size:\n", + " # Получаем область с заданным размером ядра\n", + " half_size = kernel_size // 2\n", + " x_start = max(j - half_size, 0)\n", + " x_end = min(j + half_size + 1, width)\n", + " y_start = max(i - half_size, 0)\n", + " y_end = min(i + half_size + 1, height)\n", + "\n", + " region = image[y_start:y_end, x_start:x_end]\n", + " median_value = np.median(region)\n", + "\n", + " # Если медианное значение близко к текущему пикселю, увеличиваем размер ядра\n", + " if abs(median_value - image[i, j]) < 10: # Порог можно настроить\n", + " break\n", + " kernel_size += 2 # Увеличиваем размер ядра, проверяя на большее\n", + " filtered_image[i, j] = median_value\n", + "\n", + " return filtered_image\n", + "\n", + "# Функция для объединения пересекающихся контуров\n", + "def merge_contours(contours, min_distance=10):\n", + " merged_contours = []\n", + "\n", + " while contours:\n", + " contour = contours.pop(0)\n", + " merged = False\n", + " for i, existing_contour in enumerate(merged_contours):\n", + " # Проверяем, пересекаются ли контуры (минимальное расстояние между точками)\n", + " if any(distance.euclidean(point, existing_point) < min_distance for point in contour for existing_point in existing_contour):\n", + " # Если контуры пересекаются, объединяем их\n", + " merged_contours[i] = np.vstack((existing_contour, contour))\n", + " merged = True\n", + " break\n", + " if not merged:\n", + " merged_contours.append(contour)\n", + "\n", + " return merged_contours\n", + "\n", + "# Обработка каждого выбранного DICOM файла\n", + "for dicom_path in selected_files:\n", + " if os.path.exists(dicom_path):\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Обрезка верхней трети изображения (три восьмых сверху)\n", + " height, width = image.shape\n", + " cropped_image = image[int(height * 3 / 8):, :]\n", + "\n", + " # Преобразование изображения в формат uint8, если это необходимо\n", + " if cropped_image.dtype != np.uint8:\n", + " cropped_image = cv2.normalize(cropped_image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n", + "\n", + " # Применение фильтра для увеличения яркости (CLAHE)\n", + " #clahe = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(1, 1))\n", + " enhanced_image = cropped_image\n", + "\n", + " # Визуализация результата CLAHE\n", + " plt.figure(figsize=(6, 6))\n", + " plt.imshow(enhanced_image, cmap=\"gray\")\n", + " # plt.title(\"После CLAHE (выравнивание гистограммы)\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n", + "\n", + " # Применение пороговой сегментации для выделения светлых участков\n", + " gray_threshold = np.percentile(enhanced_image, 90) # Порог для серых участков, 75-й перцентиль\n", + " binary_image = enhanced_image > gray_threshold # Выделяем светлые участки\n", + "\n", + " # Инвертируем бинарное изображение, чтобы контуры были на светлых областях\n", + " # binary_image = np.invert(binary_image)\n", + "\n", + " # Визуализация результата пороговой сегментации\n", + " plt.figure(figsize=(6, 6))\n", + " plt.imshow(binary_image, cmap=\"gray\")\n", + " plt.title(\"Результат пороговой сегментации (светлые участки)\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n", + "\n", + " # Найдем контуры светлых областей\n", + " contours, _ = cv2.findContours(binary_image.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n", + "\n", + " # Отсортируем контуры по площади и выберем 3 самых больших\n", + " contours = sorted(contours, key=cv2.contourArea, reverse=True)[:2]\n", + "\n", + " # Генерация начальных точек для активных контуров\n", + " init_points = []\n", + " for contour in contours:\n", + " # Найдем центры контуров\n", + " M = cv2.moments(contour)\n", + " if M['m00'] != 0:\n", + " cx = int(M['m10'] / M['m00'])\n", + " cy = int(M['m01'] / M['m00'])\n", + " init_points.append((cy, cx))\n", + "\n", + " # Инициализация контуров с центрами\n", + " init_contours = []\n", + " for point in init_points:\n", + " y, x = point\n", + " s = np.linspace(0, 2 * np.pi, 400)\n", + " r = y + 90 * np.sin(s)\n", + " c = x + 90 * np.cos(s)\n", + " init = np.array([r, c]).T\n", + " init_contours.append(init)\n", + "\n", + " # Применение метода активных контуров\n", + " fig, ax = plt.subplots()\n", + " ax.imshow(enhanced_image, cmap=\"gray\")\n", + "\n", + " # Объединяем пересекающиеся контуры\n", + " merged_contours = merge_contours(init_contours, min_distance=10)\n", + "\n", + " # Отображаем объединенные контуры\n", + " for init in merged_contours:\n", + " snake = active_contour(enhanced_image, init, alpha=0.1, beta=0.002, gamma=0.001)\n", + " snake[:, 0] = np.clip(snake[:, 0], 0, enhanced_image.shape[0] - 1) # ограничиваем по оси Y\n", + " snake[:, 1] = np.clip(snake[:, 1], 0, enhanced_image.shape[1] - 1) # ограничиваем по оси X\n", + "\n", + " # Теперь проверяем, чтобы контуры не были в темных областях\n", + " contour_values = enhanced_image[tuple(snake.astype(int).T)]\n", + "\n", + " # Если средняя яркость вдоль контура меньше порога, не рисуем контур\n", + " if np.mean(contour_values) > 0: # Порог для яркости, можно настроить\n", + " ax.plot(snake[:, 1], snake[:, 0], '-r', lw=2)\n", + "\n", + " ax.set_title(f\"Объединенные активные контуры: {os.path.basename(dicom_path)}\")\n", + " ax.axis(\"off\")\n", + " plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5GL9iwgn-4pr" + }, + "source": [ + "Графы\n", + "Графическая визуализация" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "executionInfo": { + "elapsed": 854, + "status": "ok", + "timestamp": 1733306605637, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "fN7ANg0jHxHX", + "outputId": "88a89cb6-2da4-4ea2-c4e5-25f1f07e28f5" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import networkx as nx\n", + "\n", + "# Создаем простое изображение как матрицу интенсивностей\n", + "image = np.array([\n", + " [10, 10, 20, 20],\n", + " [10, 10, 20, 20],\n", + " [30, 30, 40, 40],\n", + " [30, 30, 40, 40]\n", + "])\n", + "\n", + "# Визуализируем изображение и матрицу интенсивностей\n", + "fig, ax = plt.subplots(1, 2, figsize=(10, 5))\n", + "ax[0].imshow(image, cmap='gray')\n", + "ax[0].set_title('Исходное изображение')\n", + "ax[0].axis('off')\n", + "\n", + "# Отображаем матрицу интенсивностей на втором графике\n", + "for i in range(image.shape[0]):\n", + " for j in range(image.shape[1]):\n", + " ax[1].text(j, i, str(image[i, j]), ha='center', va='center', color='black')\n", + "ax[1].imshow(image, cmap='gray', alpha=0.5)\n", + "ax[1].set_title('Матрица интенсивностей')\n", + "ax[1].axis('off')\n", + "\n", + "plt.show()\n", + "\n", + "# Создаем граф из изображения\n", + "rows, cols = image.shape\n", + "graph = nx.grid_2d_graph(rows, cols)\n", + "\n", + "# Добавляем веса ребрам графа на основе разницы интенсивностей\n", + "for i in range(rows):\n", + " for j in range(cols):\n", + " if i < rows - 1:\n", + " weight = np.abs(image[i, j] - image[i + 1, j])\n", + " graph.add_edge((i, j), (i + 1, j), weight=weight)\n", + " if j < cols - 1:\n", + " weight = np.abs(image[i, j] - image[i, j + 1])\n", + " graph.add_edge((i, j), (i, j + 1), weight=weight)\n", + "\n", + "# Визуализируем граф\n", + "pos = {(i, j): (j, -i) for i, j in graph.nodes()}\n", + "weights = [graph[u][v]['weight'] for u, v in graph.edges()]\n", + "\n", + "plt.figure(figsize=(8, 8))\n", + "nx.draw(graph, pos, with_labels=True, node_size=500, font_size=8, width=weights, edge_color=weights, edge_cmap=plt.cm.Blues)\n", + "plt.title('Графовое представление изображения')\n", + "plt.show()\n", + "\n", + "# Простая сегментация на основе порога веса\n", + "threshold = 15\n", + "segments = []\n", + "for component in nx.connected_components(graph):\n", + " subgraph = graph.subgraph(component)\n", + " if all(graph[u][v]['weight'] <= threshold for u, v in subgraph.edges()):\n", + " segments.append(component)\n", + "\n", + "# Визуализируем сегменты\n", + "segmented_image = np.zeros_like(image)\n", + "for idx, segment in enumerate(segments, start=1):\n", + " for (i, j) in segment:\n", + " segmented_image[i, j] = 255 - (idx * 50)\n", + "\n", + "plt.figure(figsize=(5, 5))\n", + "plt.imshow(segmented_image, cmap='gray')\n", + "plt.title('Сегментированное изображение')\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KAHyPKy5I6Bi" + }, + "source": [ + "Сегментация графовым методом минимального разреза" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 10, + "status": "ok", + "timestamp": 1733324307973, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "sc91Ebh4U63z", + "outputId": "1da952f0-a375-4dec-e59e-7c61d8f6063a" + }, + "outputs": [], + "source": [ + "pip install pygraph\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 445 + }, + "executionInfo": { + "elapsed": 15633, + "status": "ok", + "timestamp": 1733312122850, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "o5E7GVLNbUwn", + "outputId": "a44d3093-4f09-44a2-b462-130d1a5c283c" + }, + "outputs": [], + "source": [ + "import os\n", + "import pydicom\n", + "import matplotlib.pyplot as plt\n", + "import cv2\n", + "import numpy as np\n", + "import networkx as nx\n", + "\n", + "# Пример графовой сегментации (графовый минимум разреза)\n", + "def graph_cut_segmentation(image, threshold=100, max_weight=100):\n", + " # Преобразуем изображение в бинарное\n", + " ret, binary_image = cv2.threshold(image, threshold, 255, cv2.THRESH_BINARY)\n", + "\n", + " # Получаем размеры изображения\n", + " rows, cols = binary_image.shape\n", + "\n", + " # Создаем граф\n", + " G = nx.grid_2d_graph(rows, cols)\n", + "\n", + " # Присваиваем веса рёбрам, которые отражают разницу в яркости пикселей\n", + " for i in range(rows):\n", + " for j in range(cols):\n", + " if i < rows - 1: # Вертикальные рёбра\n", + " weight = np.abs(int(binary_image[i, j]) - int(binary_image[i + 1, j]))\n", + " weight = max(1, min(weight, max_weight)) # Ограничиваем минимальный вес на 1, максимальный на max_weight\n", + " G.add_edge((i, j), (i + 1, j), weight=weight)\n", + "\n", + " if j < cols - 1: # Горизонтальные рёбра\n", + " weight = np.abs(int(binary_image[i, j]) - int(binary_image[i, j + 1]))\n", + " weight = max(1, min(weight, max_weight)) # Ограничиваем минимальный вес на 1, максимальный на max_weight\n", + " G.add_edge((i, j), (i, j + 1), weight=weight)\n", + "\n", + " # Определяем источник и сток (например, верхний левый и нижний правый углы)\n", + " source = (0, 0)\n", + " sink = (rows - 1, cols - 1)\n", + "\n", + " try:\n", + " # Выполняем минимальный разрез\n", + " cut_value, partition = nx.minimum_cut(G, source, sink)\n", + " reachable, non_reachable = partition\n", + "\n", + " # Сегментированное изображение\n", + " segmented_image = np.zeros_like(binary_image)\n", + "\n", + " for (i, j) in reachable:\n", + " segmented_image[i, j] = 255\n", + "\n", + " for (i, j) in non_reachable:\n", + " segmented_image[i, j] = 0\n", + "\n", + " return segmented_image\n", + "\n", + " except nx.NetworkXUnbounded:\n", + " print(\"Ошибка: Найден путь с бесконечной пропускной способностью. Проверьте веса рёбер.\")\n", + " return None\n", + "\n", + "\n", + "# Папка с распакованными изображениями\n", + "dicom_folder = \"images\"\n", + "\n", + "# Сбор всех DICOM-файлов в папке\n", + "dicom_files = [filename for filename in os.listdir(dicom_folder) if filename.endswith(\".dcm\")]\n", + "\n", + "# Запрос у пользователя названий файлов для отображения\n", + "file_names = input(\"Введите названия файлов изображений через запятую: \").split(\",\")\n", + "file_names = [name.strip() for name in file_names]\n", + "\n", + "# Поиск указанных файлов в папке\n", + "selected_files = []\n", + "for filename in file_names:\n", + " found_files = [os.path.join(dicom_folder, f) for f in dicom_files if filename in f]\n", + " selected_files.extend(found_files)\n", + "\n", + "# Обработка и сегментация изображений\n", + "for dicom_path in selected_files:\n", + " if os.path.exists(dicom_path):\n", + " dicom_data = pydicom.dcmread(dicom_path)\n", + " image = dicom_data.pixel_array\n", + "\n", + " # Обрезка верхних 40% изображения (оставляем нижние 60%)\n", + " height = image.shape[0]\n", + " cropped_image = image[height // 3:, :] # Обрезаем верхнюю 40%\n", + "\n", + " # Проверка размера после обрезки\n", + " print(f\"Размер обрезанного изображения: {cropped_image.shape}\")\n", + "\n", + " # Визуализация обрезанного изображения\n", + " plt.imshow(cropped_image, cmap=\"gray\")\n", + " plt.title(f\"Обрезанное изображение: {os.path.basename(dicom_path)}\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n", + "\n", + " # Преобразование изображения в формат uint8, если это необходимо\n", + " if cropped_image.dtype != np.uint8:\n", + " cropped_image = cv2.normalize(cropped_image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n", + "\n", + " # Применение адаптивного медианного фильтра для сглаживания шума\n", + " filtered_image = cv2.fastNlMeansDenoising(cropped_image, None, 10, 7, 21)\n", + "\n", + " # Преобразование фильтрованного изображения обратно в формат uint8\n", + " filtered_image = (filtered_image * 255).astype(np.uint8)\n", + "\n", + " # Применение графового метода сегментации\n", + " segmented_image = graph_cut_segmentation(filtered_image, threshold=70, max_weight=50)\n", + "\n", + " # Если сегментация прошла успешно, выводим результат\n", + " if segmented_image is not None:\n", + " plt.imshow(segmented_image, cmap=\"gray\")\n", + " plt.title(f\"Графовая сегментация (минимальный разрез): {os.path.basename(dicom_path)}\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 6224, + "status": "ok", + "timestamp": 1733315226216, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "pGwk8fcqlD3r", + "outputId": "b3708797-66c3-4e59-a308-1e4a88384f43" + }, + "outputs": [], + "source": [ + "!pip install cupy-cuda11x\n", + "!pip install scikit-learn\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "executionInfo": { + "elapsed": 1006, + "status": "ok", + "timestamp": 1733332484067, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "C74LMrzI8UqN", + "outputId": "d3199a01-db0b-4b30-bca3-901671ffe662" + }, + "outputs": [], + "source": [ + "\n", + "import cv2\n", + "import matplotlib.pyplot as plt\n", + "from skimage import segmentation, color\n", + "import numpy as np\n", + "\n", + "# Путь к изображению на Google Диске\n", + "image_path = '/content/7.png'\n", + "\n", + "# Загружаем изображение с помощью OpenCV\n", + "image = cv2.imread(image_path)\n", + "\n", + "# OpenCV загружает изображение в формате BGR, но для отображения используем RGB\n", + "image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n", + "\n", + "# Отображаем оригинальное изображение\n", + "plt.imshow(image_rgb)\n", + "plt.axis('off')\n", + "plt.title('Original Image')\n", + "plt.show()\n", + "\n", + "# Преобразуем изображение в формат для сегментации (float)\n", + "image_float = np.float32(image_rgb) / 255.0\n", + "\n", + "# Применяем алгоритм Felzenszwalb-Huttenlocher для сегментации\n", + "segments = segmentation.felzenszwalb(image_float, scale=500, sigma=0.05, min_size=50)\n", + "\n", + "# Отображаем результаты сегментации\n", + "fig, ax = plt.subplots(1, 2, figsize=(12, 6))\n", + "\n", + "# Оригинальное изображение\n", + "ax[0].imshow(image_rgb)\n", + "ax[0].set_title('Original Image')\n", + "ax[0].axis('off')\n", + "\n", + "# Сегментированное изображение\n", + "ax[1].imshow(segments)\n", + "ax[1].set_title('Segmented Image (Felzenszwalb-Huttenlocher)')\n", + "ax[1].axis('off')\n", + "\n", + "plt.show()\n", + "\n", + "# Применяем цветовую карту для сегментации\n", + "segmented_image = color.label2rgb(segments, image_rgb, kind='avg')\n", + "\n", + "# Отображаем цветную сегментацию\n", + "plt.imshow(segmented_image)\n", + "plt.title('Segmented Image with Colors')\n", + "plt.axis('off')\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 17015, + "status": "ok", + "timestamp": 1733324373410, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "T3cG-ndWKv5Y", + "outputId": "383d61ee-48ff-45c6-fba8-a3e0bcc791be" + }, + "outputs": [], + "source": [ + "pip install scikit-learn" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "executionInfo": { + "elapsed": 91492, + "status": "ok", + "timestamp": 1733332390246, + "user": { + "displayName": "E Shuvalova", + "userId": "09074408506229732964" + }, + "user_tz": -180 + }, + "id": "UzQwZH60LtnN", + "outputId": "2f4f9891-7041-4fe2-ef50-25716eca6724" + }, + "outputs": [], + "source": [ + "import cv2\n", + "import matplotlib.pyplot as plt\n", + "from skimage import segmentation, color\n", + "import numpy as np\n", + "from sklearn.cluster import SpectralClustering\n", + "from skimage import img_as_float\n", + "from sklearn.neighbors import kneighbors_graph\n", + "\n", + "# Путь к изображению на Google Диске\n", + "image_path = '/content/7.png'\n", + "\n", + "# Загружаем изображение с помощью OpenCV\n", + "image = cv2.imread(image_path)\n", + "\n", + "# OpenCV загружает изображение в формате BGR, но для отображения используем RGB\n", + "image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n", + "\n", + "# Отображаем оригинальное изображение\n", + "plt.imshow(image_rgb)\n", + "plt.axis('off')\n", + "plt.title('Original Image')\n", + "plt.show()\n", + "\n", + "# Преобразуем изображение в формат для сегментации (float)\n", + "image_float = np.float32(image_rgb) / 255.0\n", + "\n", + "# **Метод 1: Felzenszwalb-Huttenlocher**\n", + "segments_fz = segmentation.felzenszwalb(image_float, scale=200, sigma=0.05, min_size=150)\n", + "\n", + "# **Метод 2: Graph Cut (Quickshift)**\n", + "segments_quickshift = segmentation.quickshift(image_float, kernel_size=3, max_dist=6, ratio=0.9)\n", + "\n", + "# **Метод 3: Spectral Clustering**\n", + "# Преобразуем изображение в граф с соседями\n", + "image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n", + "graph = kneighbors_graph(image_gray.reshape(-1, 1), n_neighbors=10, mode='distance', include_self=False)\n", + "graph = 0.5 * (graph + graph.T) # Симметризация графа\n", + "\n", + "# Применяем спектральную кластеризацию\n", + "sc = SpectralClustering(n_clusters=5, affinity='precomputed', n_init=10)\n", + "segments_spectral = sc.fit_predict(graph.toarray()).reshape(image_gray.shape)\n", + "\n", + "# Отображаем результаты сегментации\n", + "fig, ax = plt.subplots(1, 4, figsize=(16, 6))\n", + "\n", + "# Оригинальное изображение\n", + "ax[0].imshow(image_rgb)\n", + "ax[0].set_title('Original Image')\n", + "ax[0].axis('off')\n", + "\n", + "# Felzenszwalb-Huttenlocher\n", + "ax[1].imshow(segments_fz)\n", + "ax[1].set_title('Felzenszwalb-Huttenlocher')\n", + "ax[1].axis('off')\n", + "\n", + "# Graph Cut (Quickshift)\n", + "ax[2].imshow(segments_quickshift)\n", + "ax[2].set_title('Graph Cut (Quickshift)')\n", + "ax[2].axis('off')\n", + "\n", + "# Spectral Clustering\n", + "ax[3].imshow(segments_spectral)\n", + "ax[3].set_title('Spectral Clustering')\n", + "ax[3].axis('off')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# Применяем цветовую карту для сегментации\n", + "segmented_image_fz = color.label2rgb(segments_fz, image_rgb, kind='avg')\n", + "segmented_image_quickshift = color.label2rgb(segments_quickshift, image_rgb, kind='avg')\n", + "segmented_image_spectral = color.label2rgb(segments_spectral, image_rgb, kind='avg')\n", + "\n", + "# Отображаем цветные сегментации\n", + "fig, ax = plt.subplots(1, 3, figsize=(18, 6))\n", + "\n", + "# Felzenszwalb-Huttenlocher\n", + "ax[0].imshow(segmented_image_fz)\n", + "ax[0].set_title('Segmented Image (Felzenszwalb-Huttenlocher)')\n", + "ax[0].axis('off')\n", + "\n", + "# Graph Cut (Quickshift)\n", + "ax[1].imshow(segmented_image_quickshift)\n", + "ax[1].set_title('Segmented Image (Graph Cut - Quickshift)')\n", + "ax[1].axis('off')\n", + "\n", + "# Spectral Clustering\n", + "ax[2].imshow(segmented_image_spectral)\n", + "ax[2].set_title('Segmented Image (Spectral Clustering)')\n", + "ax[2].axis('off')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + } + ], + "metadata": { + "colab": { + "authorship_tag": "ABX9TyNqotXgUNNGNY+IQjZNQBRs", + "name": "", + "version": "" + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/seminars/Тема 1.2/.gitkeep b/seminars/Тема 1.2/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/seminars/Тема 1.2/Практикум1.ipynb b/seminars/Тема 1.2/Практикум1.ipynb new file mode 100644 index 0000000..4c948d8 --- /dev/null +++ b/seminars/Тема 1.2/Практикум1.ipynb @@ -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 +} diff --git a/seminars/Тема 1.2/Т1.ipynb b/seminars/Тема 1.2/Т1.ipynb new file mode 100644 index 0000000..708ff78 --- /dev/null +++ b/seminars/Тема 1.2/Т1.ipynb @@ -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 +} diff --git a/seminars/Тема 1.2/Т2.ipynb b/seminars/Тема 1.2/Т2.ipynb new file mode 100644 index 0000000..17f1286 --- /dev/null +++ b/seminars/Тема 1.2/Т2.ipynb @@ -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 +} diff --git a/seminars/Тема 2.1/.gitkeep b/seminars/Тема 2.1/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/seminars/Тема 2.1/radiomics_report.html b/seminars/Тема 2.1/radiomics_report.html new file mode 100644 index 0000000..8bc6526 --- /dev/null +++ b/seminars/Тема 2.1/radiomics_report.html @@ -0,0 +1,237 @@ + + + + + + + Радиомический анализ опухолей мозга - Отчет + + + +
+
+

🧠 Радиомический анализ опухолей мозга

+
Многоканальный анализ МРТ данных (T1, T1c, T2, FLAIR)
+
Дата: 17.06.2026 13:25  |  Пациенты: BRATS_001, BRATS_002  |  Признаков: 5320
+
+ +
+
2
Пациентов
+
5320
Признаков
+
4
Модальности МРТ
+
7
Графиков в отчете
+
+ +
+
1. Сравнение модальностей МРТ
+
+ 1. Сравнение модальностей МРТ + +
+
+
🔍 ВЫВОД
+
T1c (контрастная) модальность показывает наибольшую гетерогенность опухоли — высокие значения энтропии и контраста указывают на активное накопление контрастного препарата, что характерно для агрессивных опухолей. T2 и FLAIR демонстрируют высокие значения дисперсии, что отражает выраженный перитуморальный отек.
+
+
+ +
+
2. Распределение текстурных признаков
+
+ 2. Распределение текстурных признаков + +
+
+
🔍 ВЫВОД
+
Текстурный анализ показывает, что T1c имеет наибольшие значения энтропии и контраста, что подтверждает высокую гетерогенность опухолевой ткани. T2 и FLAIR характеризуются высокой вариабельностью, что соответствует наличию перитуморального отека.
+
+
+ +
+
4. Сравнение профилей пациентов (радарная диаграмма)
+
+ 4. Сравнение профилей пациентов (радарная диаграмма) + +
+
+
🔍 ВЫВОД
+
BRATS_001 имеет более высокие значения энтропии и контраста во всех модальностях → опухоль более гетерогенная, что может указывать на более агрессивное течение. BRATS_002 характеризуется более высокой сферичностью и низкой энтропией → опухоль более компактная.
+
+
+ +
+
5. PCA анализ радиомических признаков
+
+ 5. PCA анализ радиомических признаков + +
+
+
🔍 ВЫВОД
+
PC1 объясняет значительную часть вариативности данных и четко разделяет пациентов по степени гетерогенности ткани. Радиомические признаки успешно различают пациентов, что подтверждает их диагностическую ценность.
+
+
+ +
+
6. Важность признаков для дифференцировки
+
+ 6. Важность признаков для дифференцировки + +
+
+
🔍 ВЫВОД
+
GLCM_Contrast и GLCM_Entropy являются наиболее важными признаками для дифференцировки пациентов → текстурный анализ наиболее информативен. Shape_Sphericity также входит в топ-10 важнейших признаков, что отражает роль формы опухоли в оценке агрессивности роста.
+
+
+ +
+
7. Комплексный анализ признаков (тепловая карта)
+
+ 7. Комплексный анализ признаков (тепловая карта) + +
+
+
🔍 ВЫВОД
+
BRATS_001 демонстрирует более высокие значения энтропии и контраста во всех модальностях, что подтверждает его более гетерогенную структуру. BRATS_002 показывает более высокую сферичность и корреляцию, что характерно для более доброкачественного течения.
+
+
+ +
+
8. Статистика по ключевым признакам
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ПризнакПациентT1T1cT2FLAIRСреднее
Средняя интенсивностьBRATS_001911.16900.67772.78711.42824.01
Средняя интенсивностьBRATS_0021029.92787.48705.27755.53819.55
ЭнтропияBRATS_0014.113.483.974.343.98
ЭнтропияBRATS_0024.483.643.994.554.16
КонтрастBRATS_0015.571.234.395.604.20
КонтрастBRATS_0026.471.374.336.464.66
КорреляцияBRATS_0010.850.910.850.880.87
КорреляцияBRATS_0020.890.920.850.900.89
+ + +
+

🔑 Ключевые выводы

+
    +
  • T1c показывает наибольшую гетерогенность → активное контрастное усиление
  • +
  • T2 и FLAIR отражают перитуморальный отек → высокая дисперсия
  • +
  • GLCM_Contrast и GLCM_Entropy — наиболее информативные признаки
  • +
  • Shape_Sphericity отражает агрессивность роста → низкая сферичность = инфильтрация
  • +
  • BRATS_001 имеет более гетерогенную опухоль → более агрессивный фенотип
  • +
  • BRATS_002 имеет более компактную опухоль → менее агрессивный фенотип
  • +
+
+ + + + + diff --git a/seminars/Тема 2.1/requirements_3d.txt b/seminars/Тема 2.1/requirements_3d.txt new file mode 100644 index 0000000..9e108a2 --- /dev/null +++ b/seminars/Тема 2.1/requirements_3d.txt @@ -0,0 +1,23 @@ +# ============================================================================= +# Зависимости для 3D радиомики +# ============================================================================= + +# Основные +numpy>=1.23.0 +pandas>=1.5.0 +scipy>=1.10.0 + +# Работа с медицинскими изображениями +nibabel>=5.2.0 +SimpleITK>=2.3.0 +pyradiomics>=3.0.0 + +# Визуализация и анализ +matplotlib>=3.6.0 +seaborn>=0.12.0 +scikit-learn>=1.2.0 +openpyxl>=3.1.0 + +# Утилиты +tqdm>=4.66.0 +joblib>=1.4.0 \ No newline at end of file diff --git a/seminars/Тема 2.1/segmentation_full_report.html b/seminars/Тема 2.1/segmentation_full_report.html new file mode 100644 index 0000000..26d949e --- /dev/null +++ b/seminars/Тема 2.1/segmentation_full_report.html @@ -0,0 +1,291 @@ + + + + + + + Полный отчет по сегментации опухолей BRATS + + + +
+

🧠 Полный отчет по сегментации опухолей головного мозга

+
+ Дата: 17.06.2026 16:04  |  + Пациентов: 2  |  + Модальности: T1, T1c, T2, FLAIR  |  + Методов сегментации: 4 +
+ +
+
2
Пациентов
+
4
Модальности МРТ
+
5
Графиков
+
3
Методов сегментации
+
+ +

1. Введение

+

Данный отчет представляет результаты сегментации опухолей головного мозга на основе + данных BRATS (Brain Tumor Segmentation). Анализ выполнен для пациентов + с использованием четырех модальностей МРТ: + T1 (анатомия), T1c (контрастное усиление), T2 (отек + опухоль) и + FLAIR (подавление сигнала от жидкости, наилучшая визуализация отека).

+ +
+
+
+ Опухоль (сегментация) +
+
+
+ Здоровые ткани +
+
+ +

2. Методы сегментации

+
+
+
Порог Оцу
+
★★★☆☆
+
Автоматический порог по гистограмме
Быстрый, но чувствителен к шуму
+
+
+
Среднее + Std
+
★★☆☆☆
+
Порог = среднее + 0.5×std
Простой, но не всегда оптимален
+
+
+
Градиентный
+
★★★☆☆
+
Выделение границ по градиенту
Хорошо выделяет края
+
+
+
⭐ Комбинированный
+
★★★★★
+
Пересечение порогового и градиентного
Наиболее точный метод
+
+
+ +

3. Результаты сегментации

+ +

3.1 Сравнение пациентов

+
+ Сравнение методов +
+ +

3.2 Пациент: BRATS_001

+
+
+ BRATS_001 методы +
+
+ BRATS_001 модальности +
+
+ +

3.2 Пациент: BRATS_002

+
+
+ BRATS_002 методы +
+
+ BRATS_002 модальности +
+
+ +

4. Интерпретация результатов

+ +
+

🧠 Пациент: BRATS_001

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ПараметрЗначениеИнтерпретация
Максимальная площадьFLAIR (9,178 пикселей)Наиболее полное отображение опухоли и перифокальных изменений
Минимальная площадьT1 (7,305 пикселей)Отражает только активную опухолевую ткань без отека
Общая площадь поражения32,724 пикселейСуммарная оценка распространенности процесса
Соотношение FLAIR/T1c1.17🟡 Умеренный отек, границы опухоли нечеткие
Характер роста🔴 Крупная опухоль, обширное поражениеТребуется оценка с учетом клинических данных
+ +
+
📊 КЛИНИЧЕСКАЯ ИНТЕРПРЕТАЦИЯ
+
+

У пациента BRATS_001 выявлено новообразование головного мозга с + максимальной площадью поражения в режиме FLAIR (9,178 пикселей).

+

Активная опухолевая ткань (T1c) составляет 7,847 пикселей, + что отражает нарушение гематоэнцефалического барьера.

+

Перитуморальный отек (FLAIR) составляет 9,178 пикселей. + Умеренный отек.

+

Рекомендации: Для точной оценки рекомендуется использование комбинированного + метода сегментации с учетом всех модальностей МРТ.

+
+
+
+ + +
+

🧠 Пациент: BRATS_002

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ПараметрЗначениеИнтерпретация
Максимальная площадьFLAIR (9,689 пикселей)Наиболее полное отображение опухоли и перифокальных изменений
Минимальная площадьT1 (8,929 пикселей)Отражает только активную опухолевую ткань без отека
Общая площадь поражения36,845 пикселейСуммарная оценка распространенности процесса
Соотношение FLAIR/T1c1.07🟡 Умеренный отек, границы опухоли нечеткие
Характер роста🔴 Крупная опухоль, обширное поражениеТребуется оценка с учетом клинических данных
+ +
+
📊 КЛИНИЧЕСКАЯ ИНТЕРПРЕТАЦИЯ
+
+

У пациента BRATS_002 выявлено новообразование головного мозга с + максимальной площадью поражения в режиме FLAIR (9,689 пикселей).

+

Активная опухолевая ткань (T1c) составляет 9,091 пикселей, + что отражает нарушение гематоэнцефалического барьера.

+

Перитуморальный отек (FLAIR) составляет 9,689 пикселей. + Умеренный отек.

+

Рекомендации: Для точной оценки рекомендуется использование комбинированного + метода сегментации с учетом всех модальностей МРТ.

+
+
+
+ + +

5. Ключевые выводы

+
+

🔑 Основные результаты

+
    +
  • FLAIR модальность показывает наибольшую площадь поражения → наилучшая визуализация перитуморального отека
  • +
  • T1c модальность показывает активную опухолевую ткань → оценка агрессивности
  • +
  • Комбинированный метод дает наиболее точные результаты → рекомендуется для клинического использования
  • +
  • Соотношение FLAIR/T1c отражает степень инфильтративного роста опухоли
  • +
  • Разница между T1c и FLAIR отражает зону отека без активного контрастного усиления
  • +
+
+ +

6. Рекомендации

+
+
+

Для клинической практики

+
    +
  • • Использовать T1c для оценки активности опухоли
  • +
  • • Использовать FLAIR для оценки отека
  • +
  • • Оценивать соотношение T1c/FLAIR
  • +
  • • Применять комбинированный метод сегментации
  • +
+
+
+

Для научных исследований

+
    +
  • • Использовать все 4 модальности
  • +
  • • Применять комбинированный метод сегментации
  • +
  • • Анализировать текстурные признаки
  • +
  • • Сравнивать разные методы сегментации
  • +
+
+
+ + +
+ + diff --git a/seminars/Тема 2.1/Лабораторная работа.docx b/seminars/Тема 2.1/Лабораторная работа.docx new file mode 100644 index 0000000..0445a71 --- /dev/null +++ b/seminars/Тема 2.1/Лабораторная работа.docx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce36cc34480a00a3f6a80117ba5a6b84e9468bd21e12b4ccbffec9a16c7fdb78 +size 45348 diff --git a/seminars/Тема 2.2/.gitkeep b/seminars/Тема 2.2/.gitkeep new file mode 100644 index 0000000..e69de29