Initial commit: project structure, scripts and documentation
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user