Initial commit: project structure, scripts and documentation

This commit is contained in:
2026-08-22 13:17:29 +03:00
commit 5042305df0
54 changed files with 14166 additions and 0 deletions
+13
View File
@@ -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()
+21
View File
@@ -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")
+25
View File
@@ -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'
+99
View File
@@ -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()
+104
View File
@@ -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()
+353
View File
@@ -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()
+991
View File
@@ -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')} &nbsp;|&nbsp; Пациенты: BRATS_001, BRATS_002 &nbsp;|&nbsp; Признаков: {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) &nbsp;|&nbsp; Модальности: 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()
+150
View File
@@ -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()
+593
View File
@@ -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()
+734
View File
@@ -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')} &nbsp;|&nbsp;
<strong>Пациентов:</strong> {len(patients_data)} &nbsp;|&nbsp;
<strong>Модальности:</strong> T1, T1c, T2, FLAIR &nbsp;|&nbsp;
<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) &nbsp;|&nbsp; Модальности: T1, T1c, T2, FLAIR &nbsp;|&nbsp;
Дата: {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()
+246
View File
@@ -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()