Initial commit: project structure, scripts and documentation
This commit is contained in:
@@ -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
|
||||
+46
@@ -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
|
||||
@@ -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` | Путь к файлу классов | `<output-dir>/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 классов (почки, сегменты, опухоли, кисты, абсцесс)
|
||||
|
||||
## 📄 Лицензия
|
||||
|
||||
Образовательный проект. Медицинские данные используются в исследовательских и учебных целях.
|
||||
BIN
Binary file not shown.
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
Каждая строка = один объект:
|
||||
```
|
||||
<class_id> <x_center> <y_center> <width> <height>
|
||||
```
|
||||
|
||||
Пример:
|
||||
```
|
||||
0 0.512 0.634 0.150 0.200
|
||||
1 0.300 0.450 0.100 0.080
|
||||
```
|
||||
|
||||
## Формат сегментации (images_labels/polygons/slice_XXXX.txt)
|
||||
|
||||
Каждая строка = полигон одного объекта:
|
||||
```
|
||||
<class_id> <x1> <y1> <x2> <y2> ... <xn> <yn>
|
||||
```
|
||||
|
||||
Пример:
|
||||
```
|
||||
0 0.450 0.500 0.460 0.510 0.470 0.505 0.465 0.495
|
||||
```
|
||||
|
||||
Все координаты нормализованы к [0, 1] относительно размеров изображения.
|
||||
LFS
BIN
Binary file not shown.
+975
@@ -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{Формирование ложных паттернов}: нейросеть может научиться распознавать не патологию, а артефакты сканирования или особенности оборудования (<<shortcut learning>>).
|
||||
\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{<class\_id> <x\_center> <y\_center> <width> <height>}
|
||||
|
||||
\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{<class\_id> <x1> <y1> <x2> <y2> ... <xn> <yn>}
|
||||
|
||||
\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}
|
||||
LFS
BIN
Binary file not shown.
+660
@@ -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}
|
||||
LFS
BIN
Binary file not shown.
+1064
File diff suppressed because it is too large
Load Diff
LFS
BIN
Binary file not shown.
+868
@@ -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}
|
||||
<class_id> <x_center> <y_center> <width> <height>
|
||||
\end{verbatim}
|
||||
|
||||
где все координаты нормализованы от 0 до 1 относительно размеров изображения.
|
||||
|
||||
\textbf{Пример:}
|
||||
\begin{verbatim}
|
||||
8 0.354492 0.541992 0.017578 0.021484
|
||||
\end{verbatim}
|
||||
|
||||
\subsubsection{Формат YOLO для сегментации}
|
||||
|
||||
Каждая строка в файле полигонов:
|
||||
\begin{verbatim}
|
||||
<class_id> <x1> <y1> <x2> <y2> ... <xn> <yn>
|
||||
\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}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
https://cloud.mail.ru/public/yYb4/kynw3eQeZ
|
||||
@@ -0,0 +1,13 @@
|
||||
import torch
|
||||
|
||||
def check_cuda_availability():
|
||||
if torch.cuda.is_available():
|
||||
print("CUDA доступна!")
|
||||
print(f"Количество доступных GPU: {torch.cuda.device_count()}")
|
||||
for i in range(torch.cuda.device_count()):
|
||||
print(f"GPU {i}: {torch.cuda.get_device_name(i)}")
|
||||
else:
|
||||
print("CUDA недоступна.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_cuda_availability()
|
||||
@@ -0,0 +1,21 @@
|
||||
import tensorflow as tf
|
||||
|
||||
print(f"Версия TensorFlow: {tf.__version__}")
|
||||
|
||||
# Получаем список физических устройств GPU
|
||||
gpus = tf.config.list_physical_devices('GPU')
|
||||
|
||||
if gpus:
|
||||
print(f"✅ Успех! TensorFlow видит {len(gpus)} GPU:")
|
||||
for gpu in gpus:
|
||||
print(f" - {gpu}")
|
||||
|
||||
# Дополнительная проверка: может ли TF выделить память на GPU
|
||||
try:
|
||||
tf.config.set_visible_devices(gpus[0], 'GPU')
|
||||
print("✅ GPU успешно инициализирован и готов к работе!")
|
||||
except RuntimeError as e:
|
||||
print(f"❌ Ошибка инициализации GPU: {e}")
|
||||
else:
|
||||
print("❌ TensorFlow НЕ видит GPU. Будет использоваться только CPU.")
|
||||
print("💡 Совет: Если у вас Windows, попробуйте установить: pip install tensorflow==2.10.0")
|
||||
@@ -0,0 +1,25 @@
|
||||
# Óêàçûâàåì àáñîëþòíûé ïóòü ê òåêóùåé ïàïêå ýêñïåðèìåíòà
|
||||
path: D:/EVPolyakov/TheEducation/experiment-10-06-2026(GPU)
|
||||
|
||||
# Óêàçûâàåì íà ôàéëû ñî ñïèñêàìè èçîáðàæåíèé äëÿ êàæäîé âûáîðêè
|
||||
train: train.txt
|
||||
val: val.txt
|
||||
test: test.txt
|
||||
|
||||
# Êîëè÷åñòâî êëàññîâ
|
||||
nc: 12
|
||||
|
||||
# Íàçâàíèÿ êëàññîâ (ïîðÿäîê äîëæåí ñòðîãî ñîîòâåòñòâîâàòü èíäåêñàì 0-11 â âàøèõ .txt ôàéëàõ ìåòîê)
|
||||
names:
|
||||
- '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'
|
||||
@@ -0,0 +1,99 @@
|
||||
# scripts/train_yolo.py
|
||||
"""
|
||||
Обучение YOLOv8 на детекцию.
|
||||
Принимает параметры через командную строку.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
from ultralytics import YOLO
|
||||
|
||||
|
||||
def check_environment():
|
||||
print("=" * 60)
|
||||
print("ПРОВЕРКА ОКРУЖЕНИЯ (DETECT)")
|
||||
print("=" * 60)
|
||||
print(f"Python: {sys.version.split()[0]}")
|
||||
print(f"PyTorch: {torch.__version__}")
|
||||
print(f"CUDA доступна: {torch.cuda.is_available()}")
|
||||
if torch.cuda.is_available():
|
||||
print(f"GPU: {torch.cuda.get_device_name(0)}")
|
||||
print(f"VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.2f} GB")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def train(data_path: str, model: str, epochs: int, imgsz: int,
|
||||
batch: int, device: str, project: str, name: str):
|
||||
check_environment()
|
||||
|
||||
log_dir = Path(project) / name
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"\nЗагрузка модели: {model}")
|
||||
yolo = YOLO(model)
|
||||
|
||||
args = {
|
||||
'task': 'detect',
|
||||
'data': data_path,
|
||||
'epochs': epochs,
|
||||
'imgsz': imgsz,
|
||||
'batch': batch,
|
||||
'device': device,
|
||||
'project': project,
|
||||
'name': name,
|
||||
'exist_ok': True,
|
||||
'pretrained': True,
|
||||
'optimizer': 'auto',
|
||||
'lr0': 0.01,
|
||||
'lrf': 0.01,
|
||||
'momentum': 0.937,
|
||||
'weight_decay': 0.0005,
|
||||
'warmup_epochs': 3.0,
|
||||
'warmup_momentum': 0.8,
|
||||
'warmup_bias_lr': 0.1,
|
||||
'box': 7.5,
|
||||
'cls': 0.5,
|
||||
'dfl': 1.5,
|
||||
'patience': 20,
|
||||
'save': True,
|
||||
'save_period': 10,
|
||||
'plots': True,
|
||||
'verbose': True,
|
||||
}
|
||||
|
||||
print(f"\nЗапуск обучения DETECT...")
|
||||
print(f"Датасет: {data_path}")
|
||||
print(f"Модель: {model}")
|
||||
print(f"Эпохи: {epochs}, Batch: {batch}, imgsz: {imgsz}")
|
||||
print(f"Результаты: {log_dir}")
|
||||
print("=" * 60)
|
||||
|
||||
t0 = time.time()
|
||||
results = yolo.train(**args)
|
||||
print(f"\nВремя обучения: {(time.time() - t0) / 60:.1f} мин")
|
||||
print(f"Лучшая модель: {log_dir / 'weights' / 'best.pt'}")
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Обучение YOLOv8 detect')
|
||||
parser.add_argument('--data', type=str, required=True, help='Путь к data.yaml')
|
||||
parser.add_argument('--model', type=str, default='yolov8s.pt')
|
||||
parser.add_argument('--epochs', type=int, default=100)
|
||||
parser.add_argument('--imgsz', type=int, default=512)
|
||||
parser.add_argument('--batch', type=int, default=16)
|
||||
parser.add_argument('--device', type=str, default='0')
|
||||
parser.add_argument('--project', type=str, default='runs/detect')
|
||||
parser.add_argument('--name', type=str, default='experiment')
|
||||
args = parser.parse_args()
|
||||
|
||||
train(args.data, args.model, args.epochs, args.imgsz,
|
||||
args.batch, args.device, args.project, args.name)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,104 @@
|
||||
# scripts/train_yolo_seg.py
|
||||
"""
|
||||
Обучение YOLOv8 на сегментацию.
|
||||
Принимает параметры через командную строку.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
from ultralytics import YOLO
|
||||
|
||||
|
||||
def check_environment():
|
||||
print("=" * 60)
|
||||
print("ПРОВЕРКА ОКРУЖЕНИЯ (SEGMENT)")
|
||||
print("=" * 60)
|
||||
print(f"Python: {sys.version.split()[0]}")
|
||||
print(f"PyTorch: {torch.__version__}")
|
||||
print(f"CUDA доступна: {torch.cuda.is_available()}")
|
||||
if torch.cuda.is_available():
|
||||
print(f"GPU: {torch.cuda.get_device_name(0)}")
|
||||
print(f"VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.2f} GB")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def train(data_path: str, model: str, epochs: int, imgsz: int,
|
||||
batch: int, device: str, project: str, name: str):
|
||||
check_environment()
|
||||
|
||||
if '-seg' not in model.lower():
|
||||
print(f"⚠️ Модель '{model}' не является сегментационной!")
|
||||
|
||||
log_dir = Path(project) / name
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"\nЗагрузка модели: {model}")
|
||||
yolo = YOLO(model)
|
||||
|
||||
args = {
|
||||
'task': 'segment',
|
||||
'data': data_path,
|
||||
'epochs': epochs,
|
||||
'imgsz': imgsz,
|
||||
'batch': batch,
|
||||
'device': device,
|
||||
'project': project,
|
||||
'name': name,
|
||||
'exist_ok': True,
|
||||
'pretrained': True,
|
||||
'optimizer': 'auto',
|
||||
'lr0': 0.01,
|
||||
'lrf': 0.01,
|
||||
'momentum': 0.937,
|
||||
'weight_decay': 0.0005,
|
||||
'warmup_epochs': 3.0,
|
||||
'warmup_momentum': 0.8,
|
||||
'warmup_bias_lr': 0.1,
|
||||
'box': 7.5,
|
||||
'cls': 0.5,
|
||||
'dfl': 1.5,
|
||||
'mask_ratio': 4,
|
||||
'overlap_mask': True,
|
||||
'patience': 20,
|
||||
'save': True,
|
||||
'save_period': 10,
|
||||
'plots': True,
|
||||
'verbose': True,
|
||||
}
|
||||
|
||||
print(f"\nЗапуск обучения СЕГМЕНТАЦИИ...")
|
||||
print(f"Датасет: {data_path}")
|
||||
print(f"Модель: {model}")
|
||||
print(f"Эпохи: {epochs}, Batch: {batch}, imgsz: {imgsz}")
|
||||
print(f"Результаты: {log_dir}")
|
||||
print("=" * 60)
|
||||
|
||||
t0 = time.time()
|
||||
results = yolo.train(**args)
|
||||
print(f"\nВремя обучения: {(time.time() - t0) / 60:.1f} мин")
|
||||
print(f"Лучшая модель: {log_dir / 'weights' / 'best.pt'}")
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Обучение YOLOv8 segment')
|
||||
parser.add_argument('--data', type=str, required=True, help='Путь к data.yaml')
|
||||
parser.add_argument('--model', type=str, default='yolov8s-seg.pt')
|
||||
parser.add_argument('--epochs', type=int, default=100)
|
||||
parser.add_argument('--imgsz', type=int, default=512)
|
||||
parser.add_argument('--batch', type=int, default=16)
|
||||
parser.add_argument('--device', type=str, default='0')
|
||||
parser.add_argument('--project', type=str, default='runs/segment')
|
||||
parser.add_argument('--name', type=str, default='seg_experiment')
|
||||
args = parser.parse_args()
|
||||
|
||||
train(args.data, args.model, args.epochs, args.imgsz,
|
||||
args.batch, args.device, args.project, args.name)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,353 @@
|
||||
# scripts/ttv_freq2.py
|
||||
"""
|
||||
Версия 6: Читает predefined_classes.txt для сохранения полной схемы классов.
|
||||
Это позволяет использовать общую схему 12 классов для всех экспериментов,
|
||||
даже если в конкретном датасете присутствуют не все классы.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import random
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
raise SystemExit("Установите PyYAML: pip install pyyaml")
|
||||
|
||||
|
||||
def load_predefined_classes(path: Path) -> 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()
|
||||
@@ -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"""
|
||||
<div class="section">
|
||||
<div class="section-title">{title}</div>
|
||||
<div class="plot-container">
|
||||
"""
|
||||
if img_base64:
|
||||
html += f' <img src="data:image/png;base64,{img_base64}" alt="{title}">\n'
|
||||
else:
|
||||
html += ' <div class="no-plot">⚠️ График не сгенерирован</div>\n'
|
||||
|
||||
html += f"""
|
||||
</div>
|
||||
<div class="conclusion-box">
|
||||
<div class="label">🔍 ВЫВОД</div>
|
||||
<div class="text">{conclusion_text}</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return html
|
||||
|
||||
# Генерация HTML
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Радиомический анализ опухолей мозга - Отчет</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f0f2f5; color: #1a1a2e; padding: 20px; }}
|
||||
.container {{ max-width: 1200px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 10px 40px rgba(0,0,0,0.1); padding: 40px; }}
|
||||
.header {{ text-align: center; padding-bottom: 30px; border-bottom: 3px solid #4ECDC4; margin-bottom: 30px; }}
|
||||
.header h1 {{ font-size: 28px; color: #1a1a2e; }}
|
||||
.header .subtitle {{ font-size: 14px; color: #666; margin-top: 10px; }}
|
||||
.header .meta {{ font-size: 13px; color: #888; margin-top: 5px; }}
|
||||
.summary-box {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 25px; border-radius: 12px; margin-bottom: 30px; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; }}
|
||||
.summary-box .item {{ text-align: center; }}
|
||||
.summary-box .item .number {{ font-size: 28px; font-weight: bold; }}
|
||||
.summary-box .item .label {{ font-size: 13px; opacity: 0.8; }}
|
||||
.section {{ margin-bottom: 40px; }}
|
||||
.section-title {{ font-size: 22px; color: #1a1a2e; border-left: 4px solid #4ECDC4; padding-left: 15px; margin-bottom: 20px; }}
|
||||
.plot-container {{ background: #fafafa; border-radius: 10px; padding: 20px; margin-bottom: 25px; border: 1px solid #e8e8e8; }}
|
||||
.plot-container img {{ width: 100%; height: auto; border-radius: 8px; }}
|
||||
.plot-container .no-plot {{ text-align: center; padding: 40px; color: #999; font-size: 16px; }}
|
||||
.conclusion-box {{ background: #fff8e1; border-left: 4px solid #ffc107; padding: 15px 20px; border-radius: 8px; margin-top: 15px; }}
|
||||
.conclusion-box .label {{ font-weight: bold; color: #e65100; font-size: 14px; }}
|
||||
.conclusion-box .text {{ margin-top: 5px; font-size: 14px; line-height: 1.6; color: #333; }}
|
||||
.stats-table {{ width: 100%; border-collapse: collapse; margin: 15px 0; font-size: 13px; }}
|
||||
.stats-table th {{ background: #4ECDC4; color: white; padding: 10px 15px; text-align: left; }}
|
||||
.stats-table td {{ padding: 8px 15px; border-bottom: 1px solid #e8e8e8; }}
|
||||
.stats-table tr:hover {{ background: #f5f5f5; }}
|
||||
.stats-table .highlight {{ font-weight: bold; color: #FF6B6B; }}
|
||||
.footer {{ text-align: center; padding-top: 30px; border-top: 2px solid #e8e8e8; margin-top: 30px; color: #888; font-size: 13px; }}
|
||||
.key-findings {{ background: #e8f5e9; border-radius: 12px; padding: 25px; margin-top: 30px; }}
|
||||
.key-findings h3 {{ color: #2e7d32; margin-bottom: 15px; }}
|
||||
.key-findings ul {{ list-style: none; padding: 0; }}
|
||||
.key-findings ul li {{ padding: 8px 0; padding-left: 25px; position: relative; line-height: 1.5; }}
|
||||
.key-findings ul li:before {{ content: "▸"; position: absolute; left: 0; color: #2e7d32; font-weight: bold; }}
|
||||
@media (max-width: 768px) {{ .container {{ padding: 20px; }} .summary-box {{ grid-template-columns: repeat(2, 1fr); }} }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🧠 Радиомический анализ опухолей мозга</h1>
|
||||
<div class="subtitle">Многоканальный анализ МРТ данных (T1, T1c, T2, FLAIR)</div>
|
||||
<div class="meta">Дата: {datetime.now().strftime('%d.%m.%Y %H:%M')} | Пациенты: BRATS_001, BRATS_002 | Признаков: {len(df.columns)}</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-box">
|
||||
<div class="item"><div class="number">{len(df)}</div><div class="label">Пациентов</div></div>
|
||||
<div class="item"><div class="number">{len(df.columns)}</div><div class="label">Признаков</div></div>
|
||||
<div class="item"><div class="number">4</div><div class="label">Модальности МРТ</div></div>
|
||||
<div class="item"><div class="number">7</div><div class="label">Графиков в отчете</div></div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Добавляем разделы с графиками (только если есть данные)
|
||||
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 += """
|
||||
<div class="section">
|
||||
<div class="section-title">8. Статистика по ключевым признакам</div>
|
||||
<table class="stats-table">
|
||||
<thead><tr><th>Признак</th><th>Пациент</th><th>T1</th><th>T1c</th><th>T2</th><th>FLAIR</th><th>Среднее</th></tr></thead>
|
||||
<tbody>
|
||||
"""
|
||||
|
||||
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"""
|
||||
<tr>
|
||||
<td>{feat_label}</td>
|
||||
<td><b>{patient}</b></td>
|
||||
<td>{t1_str}</td>
|
||||
<td>{t1c_str}</td>
|
||||
<td>{t2_str}</td>
|
||||
<td>{flair_str}</td>
|
||||
<td class="highlight">{avg:.2f}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
html_content += """
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="key-findings">
|
||||
<h3>🔑 Ключевые выводы</h3>
|
||||
<ul>
|
||||
<li><b>T1c</b> показывает наибольшую гетерогенность → активное контрастное усиление</li>
|
||||
<li><b>T2 и FLAIR</b> отражают перитуморальный отек → высокая дисперсия</li>
|
||||
<li><b>GLCM_Contrast и GLCM_Entropy</b> — наиболее информативные признаки</li>
|
||||
<li><b>Shape_Sphericity</b> отражает агрессивность роста → низкая сферичность = инфильтрация</li>
|
||||
<li><b>BRATS_001</b> имеет более гетерогенную опухоль → более агрессивный фенотип</li>
|
||||
<li><b>BRATS_002</b> имеет более компактную опухоль → менее агрессивный фенотип</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Отчет сгенерирован автоматически<br>
|
||||
Данные: BRATS (Task01_BrainTumour) | Модальности: T1, T1c, T2, FLAIR
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# Сохраняем 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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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"""
|
||||
<div class="interpretation">
|
||||
<h3>🧠 Пациент: {patient_id}</h3>
|
||||
<table class="stats-table">
|
||||
<tr><th>Параметр</th><th>Значение</th><th>Интерпретация</th></tr>
|
||||
<tr>
|
||||
<td>Максимальная площадь</td>
|
||||
<td><b>{max_mod}</b> ({areas[max_mod]:,} пикселей)</td>
|
||||
<td>Наиболее полное отображение опухоли и перифокальных изменений</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Минимальная площадь</td>
|
||||
<td><b>{min_mod}</b> ({areas[min_mod]:,} пикселей)</td>
|
||||
<td>Отражает только активную опухолевую ткань без отека</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Общая площадь поражения</td>
|
||||
<td><b>{total:,}</b> пикселей</td>
|
||||
<td>Суммарная оценка распространенности процесса</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if 'FLAIR' in areas and 'T1c' in areas:
|
||||
ratio = areas['FLAIR'] / areas['T1c']
|
||||
if ratio > 1.5:
|
||||
interpretation += f"""
|
||||
<tr>
|
||||
<td>Соотношение FLAIR/T1c</td>
|
||||
<td><b>{ratio:.2f}</b></td>
|
||||
<td>🔴 Выраженный перитуморальный отек (инфильтративный рост)</td>
|
||||
</tr>
|
||||
"""
|
||||
elif ratio > 1.0:
|
||||
interpretation += f"""
|
||||
<tr>
|
||||
<td>Соотношение FLAIR/T1c</td>
|
||||
<td><b>{ratio:.2f}</b></td>
|
||||
<td>🟡 Умеренный отек, границы опухоли нечеткие</td>
|
||||
</tr>
|
||||
"""
|
||||
else:
|
||||
interpretation += f"""
|
||||
<tr>
|
||||
<td>Соотношение FLAIR/T1c</td>
|
||||
<td><b>{ratio:.2f}</b></td>
|
||||
<td>🟢 Компактная опухоль, минимальный отек</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if total > 30000:
|
||||
growth = "🔴 Крупная опухоль, обширное поражение"
|
||||
elif total > 20000:
|
||||
growth = "🟡 Опухоль средних размеров"
|
||||
else:
|
||||
growth = "🟢 Относительно небольшая опухоль"
|
||||
|
||||
interpretation += f"""
|
||||
<tr>
|
||||
<td>Характер роста</td>
|
||||
<td>{growth}</td>
|
||||
<td>Требуется оценка с учетом клинических данных</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="conclusion-box">
|
||||
<div class="label">📊 КЛИНИЧЕСКАЯ ИНТЕРПРЕТАЦИЯ</div>
|
||||
<div class="text">
|
||||
<p>У пациента <b>{patient_id}</b> выявлено новообразование головного мозга с
|
||||
максимальной площадью поражения в режиме <b>{max_mod}</b> ({areas[max_mod]:,} пикселей).</p>
|
||||
<p>Активная опухолевая ткань (T1c) составляет <b>{areas.get('T1c', 0):,}</b> пикселей,
|
||||
что отражает нарушение гематоэнцефалического барьера.</p>
|
||||
<p>Перитуморальный отек (FLAIR) составляет <b>{areas.get('FLAIR', 0):,}</b> пикселей.
|
||||
{'Выраженный отек указывает на инфильтративный характер роста.' if areas.get('FLAIR', 0) > areas.get('T1c', 0) * 1.5 else 'Умеренный отек.'}</p>
|
||||
<p><b>Рекомендации:</b> Для точной оценки рекомендуется использование комбинированного
|
||||
метода сегментации с учетом всех модальностей МРТ.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
interpretations.append(interpretation)
|
||||
|
||||
interpretation_html = "\n".join(interpretations)
|
||||
|
||||
# Генерация HTML
|
||||
html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Полный отчет по сегментации опухолей BRATS</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f0f2f5; padding: 20px; }}
|
||||
.container {{ max-width: 1200px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 10px 40px rgba(0,0,0,0.1); padding: 40px; }}
|
||||
h1 {{ color: #2c3e50; border-bottom: 3px solid #e74c3c; padding-bottom: 15px; margin-bottom: 30px; }}
|
||||
h2 {{ color: #34495e; margin: 30px 0 15px 0; border-left: 4px solid #e74c3c; padding-left: 15px; }}
|
||||
h3 {{ color: #2c3e50; margin: 20px 0 10px 0; }}
|
||||
h4 {{ color: #34495e; margin: 15px 0 10px 0; }}
|
||||
.meta {{ color: #7f8c8d; margin-bottom: 20px; font-size: 14px; }}
|
||||
|
||||
.summary-box {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 25px; border-radius: 12px; margin-bottom: 30px; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; }}
|
||||
.summary-box .item {{ text-align: center; }}
|
||||
.summary-box .item .number {{ font-size: 28px; font-weight: bold; }}
|
||||
.summary-box .item .label {{ font-size: 13px; opacity: 0.8; }}
|
||||
|
||||
.plot-container {{ background: #fafafa; border-radius: 10px; padding: 20px; margin-bottom: 25px; border: 1px solid #e8e8e8; }}
|
||||
.plot-container img {{ width: 100%; height: auto; border-radius: 8px; }}
|
||||
|
||||
.interpretation {{ background: #f8f9fa; border-radius: 10px; padding: 25px; margin: 20px 0; border: 1px solid #e8e8e8; }}
|
||||
|
||||
.stats-table {{ width: 100%; border-collapse: collapse; margin: 15px 0; font-size: 14px; }}
|
||||
.stats-table th {{ background: #3498db; color: white; padding: 10px 15px; text-align: left; }}
|
||||
.stats-table td {{ padding: 10px 15px; border-bottom: 1px solid #e8e8e8; }}
|
||||
.stats-table tr:hover {{ background: #f5f5f5; }}
|
||||
|
||||
.conclusion-box {{ background: #fff8e1; border-left: 4px solid #ffc107; padding: 15px 20px; border-radius: 8px; margin-top: 15px; }}
|
||||
.conclusion-box .label {{ font-weight: bold; color: #e65100; font-size: 14px; }}
|
||||
.conclusion-box .text {{ margin-top: 5px; font-size: 14px; line-height: 1.6; color: #333; }}
|
||||
.conclusion-box .text p {{ margin: 5px 0; }}
|
||||
|
||||
.key-findings {{ background: #e8f5e9; border-radius: 12px; padding: 25px; margin-top: 30px; }}
|
||||
.key-findings h3 {{ color: #2e7d32; margin-bottom: 15px; }}
|
||||
.key-findings ul {{ list-style: none; padding: 0; }}
|
||||
.key-findings ul li {{ padding: 8px 0; padding-left: 25px; position: relative; line-height: 1.5; }}
|
||||
.key-findings ul li:before {{ content: "▸"; position: absolute; left: 0; color: #2e7d32; font-weight: bold; }}
|
||||
|
||||
.footer {{ text-align: center; margin-top: 30px; padding-top: 20px; border-top: 2px solid #e8e8e8; color: #95a5a6; font-size: 13px; }}
|
||||
|
||||
.method-comparison {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 20px 0; }}
|
||||
.method-card {{ background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center; border: 1px solid #e8e8e8; }}
|
||||
.method-card .name {{ font-weight: bold; font-size: 16px; }}
|
||||
.method-card .stars {{ color: #f39c12; font-size: 18px; margin: 5px 0; }}
|
||||
.method-card .desc {{ font-size: 13px; color: #7f8c8d; }}
|
||||
.method-card.best {{ border-color: #2ecc71; background: #f0fff4; }}
|
||||
|
||||
.legend {{ display: flex; gap: 20px; justify-content: center; margin: 20px 0; flex-wrap: wrap; }}
|
||||
.legend-item {{ display: flex; align-items: center; gap: 8px; font-size: 14px; }}
|
||||
.legend-color {{ width: 30px; height: 15px; border-radius: 3px; }}
|
||||
|
||||
.grid-2 {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }}
|
||||
|
||||
@media (max-width: 768px) {{
|
||||
.grid-2 {{ grid-template-columns: 1fr; }}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🧠 Полный отчет по сегментации опухолей головного мозга</h1>
|
||||
<div class="meta">
|
||||
<strong>Дата:</strong> {datetime.now().strftime('%d.%m.%Y %H:%M')} |
|
||||
<strong>Пациентов:</strong> {len(patients_data)} |
|
||||
<strong>Модальности:</strong> T1, T1c, T2, FLAIR |
|
||||
<strong>Методов сегментации:</strong> 4
|
||||
</div>
|
||||
|
||||
<div class="summary-box">
|
||||
<div class="item"><div class="number">{len(patients_data)}</div><div class="label">Пациентов</div></div>
|
||||
<div class="item"><div class="number">4</div><div class="label">Модальности МРТ</div></div>
|
||||
<div class="item"><div class="number">{len(all_plots)}</div><div class="label">Графиков</div></div>
|
||||
<div class="item"><div class="number">3</div><div class="label">Методов сегментации</div></div>
|
||||
</div>
|
||||
|
||||
<h2>1. Введение</h2>
|
||||
<p>Данный отчет представляет результаты сегментации опухолей головного мозга на основе
|
||||
данных <b>BRATS (Brain Tumor Segmentation)</b>. Анализ выполнен для пациентов
|
||||
с использованием четырех модальностей МРТ:
|
||||
<b>T1</b> (анатомия), <b>T1c</b> (контрастное усиление), <b>T2</b> (отек + опухоль) и
|
||||
<b>FLAIR</b> (подавление сигнала от жидкости, наилучшая визуализация отека).</p>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: rgba(255,0,0,0.5);"></div>
|
||||
<span>Опухоль (сегментация)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #888;"></div>
|
||||
<span>Здоровые ткани</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>2. Методы сегментации</h2>
|
||||
<div class="method-comparison">
|
||||
<div class="method-card">
|
||||
<div class="name">Порог Оцу</div>
|
||||
<div class="stars">★★★☆☆</div>
|
||||
<div class="desc">Автоматический порог по гистограмме<br>Быстрый, но чувствителен к шуму</div>
|
||||
</div>
|
||||
<div class="method-card">
|
||||
<div class="name">Среднее + Std</div>
|
||||
<div class="stars">★★☆☆☆</div>
|
||||
<div class="desc">Порог = среднее + 0.5×std<br>Простой, но не всегда оптимален</div>
|
||||
</div>
|
||||
<div class="method-card">
|
||||
<div class="name">Градиентный</div>
|
||||
<div class="stars">★★★☆☆</div>
|
||||
<div class="desc">Выделение границ по градиенту<br>Хорошо выделяет края</div>
|
||||
</div>
|
||||
<div class="method-card best">
|
||||
<div class="name">⭐ Комбинированный</div>
|
||||
<div class="stars">★★★★★</div>
|
||||
<div class="desc">Пересечение порогового и градиентного<br><b>Наиболее точный метод</b></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>3. Результаты сегментации</h2>
|
||||
|
||||
<h3>3.1 Сравнение пациентов</h3>
|
||||
<div class="plot-container">
|
||||
<img src="data:image/png;base64,{all_plots.get('comparison', '')}" alt="Сравнение методов">
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Добавляем индивидуальные графики
|
||||
for patient_data in patients_data:
|
||||
patient_id = patient_data['patient_id']
|
||||
|
||||
html += f"""
|
||||
<h3>3.2 Пациент: {patient_id}</h3>
|
||||
<div class="grid-2">
|
||||
<div class="plot-container">
|
||||
<img src="data:image/png;base64,{all_plots.get(f'{patient_id}_methods', '')}" alt="{patient_id} методы">
|
||||
</div>
|
||||
<div class="plot-container">
|
||||
<img src="data:image/png;base64,{all_plots.get(f'{patient_id}_modalities', '')}" alt="{patient_id} модальности">
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# 4. ИНТЕРПРЕТАЦИЯ
|
||||
html += f"""
|
||||
<h2>4. Интерпретация результатов</h2>
|
||||
{interpretation_html}
|
||||
|
||||
<h2>5. Ключевые выводы</h2>
|
||||
<div class="key-findings">
|
||||
<h3>🔑 Основные результаты</h3>
|
||||
<ul>
|
||||
<li><b>FLAIR модальность</b> показывает наибольшую площадь поражения → наилучшая визуализация перитуморального отека</li>
|
||||
<li><b>T1c модальность</b> показывает активную опухолевую ткань → оценка агрессивности</li>
|
||||
<li><b>Комбинированный метод</b> дает наиболее точные результаты → рекомендуется для клинического использования</li>
|
||||
<li><b>Соотношение FLAIR/T1c</b> отражает степень инфильтративного роста опухоли</li>
|
||||
<li>Разница между T1c и FLAIR отражает зону отека без активного контрастного усиления</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>6. Рекомендации</h2>
|
||||
<div class="grid-2">
|
||||
<div style="background: #e3f2fd; padding: 20px; border-radius: 10px;">
|
||||
<h3 style="color: #1565c0;">Для клинической практики</h3>
|
||||
<ul style="list-style: none; padding: 0;">
|
||||
<li style="padding: 5px 0;">• Использовать <b>T1c</b> для оценки активности опухоли</li>
|
||||
<li style="padding: 5px 0;">• Использовать <b>FLAIR</b> для оценки отека</li>
|
||||
<li style="padding: 5px 0;">• Оценивать <b>соотношение T1c/FLAIR</b></li>
|
||||
<li style="padding: 5px 0;">• Применять <b>комбинированный метод</b> сегментации</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="background: #f3e5f5; padding: 20px; border-radius: 10px;">
|
||||
<h3 style="color: #6a1b9a;">Для научных исследований</h3>
|
||||
<ul style="list-style: none; padding: 0;">
|
||||
<li style="padding: 5px 0;">• Использовать <b>все 4 модальности</b></li>
|
||||
<li style="padding: 5px 0;">• Применять <b>комбинированный метод</b> сегментации</li>
|
||||
<li style="padding: 5px 0;">• Анализировать <b>текстурные признаки</b></li>
|
||||
<li style="padding: 5px 0;">• Сравнивать <b>разные методы</b> сегментации</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Отчет сгенерирован автоматически на основе радиомического анализа<br>
|
||||
Данные: BRATS (Task01_BrainTumour) | Модальности: T1, T1c, T2, FLAIR |
|
||||
Дата: {datetime.now().strftime('%d.%m.%Y %H:%M')}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
"# Предположим, на странице есть <table> с классом \"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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Reference in New Issue
Block a user