# ============================================================================= # ПОЛНЫЙ ОТЧЕТ ПО СЕГМЕНТАЦИИ ОПУХОЛЕЙ BRATS # ИСПРАВЛЕНА ВЕРСИЯ: НЕТ ПУСТЫХ ГРАФИКОВ # ============================================================================= import numpy as np import matplotlib.pyplot as plt import nibabel as nib from pathlib import Path from datetime import datetime from scipy.ndimage import binary_closing, binary_opening, label, generate_binary_structure import warnings import base64 from io import BytesIO warnings.filterwarnings('ignore') # ============================================================================= # 1. КОНФИГУРАЦИЯ # ============================================================================= DATA_ROOT = "D:/Radiomics_Data/Experiments/experiment-17-06-2026/Task01_BrainTumour/Task01_BrainTumour" IMAGES_TR = Path(DATA_ROOT) / "imagesTr" LABELS_TR = Path(DATA_ROOT) / "labelsTr" OUTPUT_DIR = Path(DATA_ROOT) / "radiomics_output" / "segmentation_full_report" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) print("=" * 70) print("🧠 ГЕНЕРАЦИЯ ПОЛНОГО ОТЧЕТА ПО СЕГМЕНТАЦИИ") print("=" * 70) print(f" Результаты: {OUTPUT_DIR}") print("=" * 70) # ============================================================================= # 2. ФУНКЦИИ ДЛЯ РАБОТЫ С ДАННЫМИ # ============================================================================= def is_valid_nifti_file(filepath): name = filepath.name if name.startswith('._'): return False if not name.endswith('.nii.gz'): return False if filepath.stat().st_size < 1000: return False return True def load_nifti(filepath): if not is_valid_nifti_file(filepath): raise ValueError(f"Файл не является валидным NIfTI: {filepath}") img = nib.load(str(filepath)) return img.get_fdata(), img.affine def normalize_slice(slice_data): slice_data = np.nan_to_num(slice_data) min_val = slice_data.min() max_val = slice_data.max() if max_val > min_val: return ((slice_data - min_val) / (max_val - min_val) * 255).astype(np.uint8) return np.zeros_like(slice_data, dtype=np.uint8) def threshold_otsu_manual(image): hist, _ = np.histogram(image, bins=256, range=(0, 256)) hist = hist.astype(float) total = hist.sum() hist_norm = hist / total best_thresh = 0 best_var = 0 for t in range(1, 256): w0 = hist_norm[:t].sum() w1 = hist_norm[t:].sum() if w0 == 0 or w1 == 0: continue mu0 = (np.arange(t) * hist_norm[:t]).sum() / w0 mu1 = (np.arange(t, 256) * hist_norm[t:]).sum() / w1 var_between = w0 * w1 * (mu0 - mu1) ** 2 if var_between > best_var: best_var = var_between best_thresh = t return best_thresh def find_best_slice(image_data, axis=2): best_score = 0 best_idx = 0 for i in range(image_data.shape[axis]): if axis == 0: slice_data = image_data[i, :, :] elif axis == 1: slice_data = image_data[:, i, :] else: slice_data = image_data[:, :, i] slice_norm = normalize_slice(slice_data) score = np.std(slice_norm) * np.max(slice_norm) if score > best_score: best_score = score best_idx = i return best_idx # ============================================================================= # 3. МЕТОДЫ СОЗДАНИЯ МАСОК # ============================================================================= def create_mask_by_threshold(image_slice, method='otsu'): img_norm = normalize_slice(image_slice) if method == 'otsu': try: from skimage.filters import threshold_otsu thresh = threshold_otsu(img_norm) except ImportError: thresh = threshold_otsu_manual(img_norm) mask = img_norm > thresh elif method == 'mean': thresh = np.mean(img_norm) + 0.5 * np.std(img_norm) mask = img_norm > thresh else: mask = img_norm > np.percentile(img_norm, 85) return mask def create_mask_by_gradient(image_slice): img_norm = normalize_slice(image_slice).astype(float) gy, gx = np.gradient(img_norm) gradient = np.sqrt(gx ** 2 + gy ** 2) grad_norm = normalize_slice(gradient) thresh = np.percentile(grad_norm, 85) mask = grad_norm > thresh return mask def create_mask_combined(image_slice): mask_intensity = create_mask_by_threshold(image_slice, 'otsu') mask_gradient = create_mask_by_gradient(image_slice) mask = mask_intensity & mask_gradient return mask def postprocess_mask(mask, min_size=50): struct = generate_binary_structure(2, 2) labeled, num_features = label(mask, struct) if num_features > 0: sizes = np.bincount(labeled.ravel()) for i in range(1, num_features + 1): if sizes[i] < min_size: mask[labeled == i] = False mask = binary_closing(mask, structure=struct, iterations=1) mask = binary_opening(mask, structure=struct, iterations=1) return mask # ============================================================================= # 4. ФУНКЦИИ ДЛЯ ВИЗУАЛИЗАЦИИ (БЕЗ ПУСТЫХ ГРАФИКОВ) # ============================================================================= def fig_to_base64(fig): buf = BytesIO() fig.savefig(buf, format='png', dpi=150, bbox_inches='tight') buf.seek(0) img_base64 = base64.b64encode(buf.read()).decode('utf-8') buf.close() plt.close(fig) return img_base64 def create_modality_comparison(image_data, patient_id, slice_idx): """Создает график сравнения модальностей""" fig, axes = plt.subplots(2, 4, figsize=(16, 8)) modalities = ['T1', 'T1c', 'T2', 'FLAIR'] for col, mod in enumerate(modalities): slice_data = image_data[:, :, slice_idx, col] # Исходное изображение axes[0, col].imshow(normalize_slice(slice_data), cmap='gray') axes[0, col].set_title(f'{mod}', fontsize=12) axes[0, col].axis('off') # Маска mask = create_mask_combined(slice_data) mask_processed = postprocess_mask(mask, min_size=50) axes[1, col].imshow(normalize_slice(slice_data), cmap='gray') axes[1, col].imshow(mask_processed, cmap='jet', alpha=0.5) area = np.sum(mask_processed) axes[1, col].set_title(f'Маска (пл. {area})', fontsize=10) axes[1, col].axis('off') plt.suptitle(f'Сегментация по модальностям - {patient_id}', fontsize=14, fontweight='bold') plt.tight_layout() return fig def create_method_comparison(slice_data, real_mask_slice, patient_id, slice_idx): """Создает график сравнения методов сегментации (без пустых графиков)""" has_real_mask = real_mask_slice is not None and np.sum(real_mask_slice) > 0 fig, axes = plt.subplots(2, 3, figsize=(15, 10)) # Верхний ряд # 1. Исходное изображение axes[0, 0].imshow(normalize_slice(slice_data), cmap='gray') axes[0, 0].set_title('Исходное (T1c)', fontsize=12) axes[0, 0].axis('off') # 2. Реальная маска (если есть) if has_real_mask: axes[0, 1].imshow(normalize_slice(slice_data), cmap='gray') axes[0, 1].imshow(real_mask_slice, cmap='hot', alpha=0.5) area = np.sum(real_mask_slice) axes[0, 1].set_title(f'Реальная маска\n(пл. {area})', fontsize=11) axes[0, 1].axis('off') else: axes[0, 1].set_visible(False) # 3. Пустой подграфик (скрываем) axes[0, 2].set_visible(False) # Нижний ряд: методы сегментации mask_methods = [ ('otsu', create_mask_by_threshold(slice_data, 'otsu'), 'Порог Оцу'), ('mean', create_mask_by_threshold(slice_data, 'mean'), 'Среднее+Std'), ('combined', create_mask_combined(slice_data), 'Комбинир.') ] for col, (name, mask, label) in enumerate(mask_methods): mask_processed = postprocess_mask(mask, min_size=50) ax = axes[1, col] ax.imshow(normalize_slice(slice_data), cmap='gray') ax.imshow(mask_processed, cmap='jet', alpha=0.5) area = np.sum(mask_processed) ax.set_title(f'{label}\n(пл. {area})', fontsize=11) ax.axis('off') plt.suptitle(f'Создание маски - {patient_id}, срез {slice_idx}', fontsize=14, fontweight='bold') plt.tight_layout() return fig def create_comparison_patients(selected_patients): """Создает сравнительный график для двух пациентов (без пустых графиков)""" fig, axes = plt.subplots(2, 4, figsize=(16, 8)) methods_to_show = [ ('otsu', 'Порог Оцу'), ('mean', 'Среднее+Std'), ('combined', 'Комбинир.') ] for row, img_path in enumerate(selected_patients[:2]): patient_id = img_path.stem.replace('.nii', '') try: image_data, _ = load_nifti(img_path) slice_idx = find_best_slice(image_data, axis=2) if image_data.ndim == 4: slice_data = image_data[:, :, slice_idx, 1] # T1c else: slice_data = image_data[:, :, slice_idx] except: continue # Исходное изображение (столбец 0) axes[row, 0].imshow(normalize_slice(slice_data), cmap='gray') axes[row, 0].set_title(f'{patient_id}\nИсходное', fontsize=10) axes[row, 0].axis('off') # Методы сегментации (столбцы 1, 2, 3) for col, (name, label_text) in enumerate(methods_to_show, start=1): if name == 'otsu': mask = create_mask_by_threshold(slice_data, 'otsu') elif name == 'mean': mask = create_mask_by_threshold(slice_data, 'mean') else: mask = create_mask_combined(slice_data) mask_processed = postprocess_mask(mask, min_size=50) axes[row, col].imshow(normalize_slice(slice_data), cmap='gray') axes[row, col].imshow(mask_processed, cmap='jet', alpha=0.5) area = np.sum(mask_processed) axes[row, col].set_title(f'{label_text}\n(пл. {area})', fontsize=9) axes[row, col].axis('off') plt.suptitle('Сравнение методов сегментации', fontsize=16, fontweight='bold') plt.tight_layout() return fig # ============================================================================= # 5. ГЕНЕРАЦИЯ HTML-ОТЧЕТА # ============================================================================= def generate_html_report(patients_data, all_plots, selected_patients): """Генерирует полный HTML-отчет с интерпретацией""" interpretations = [] for patient_data in patients_data: patient_id = patient_data['patient_id'] areas = patient_data['areas'] total = sum(areas.values()) max_mod = max(areas, key=areas.get) min_mod = min(areas, key=areas.get) interpretation = f"""

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

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

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

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

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

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

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

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

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

1. Введение

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

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

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

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

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

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

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

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

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

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

{interpretation_html}

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

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

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

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

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

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

  • • Использовать все 4 модальности
  • • Применять комбинированный метод сегментации
  • • Анализировать текстурные признаки
  • • Сравнивать разные методы сегментации
""" return html # ============================================================================= # 6. ОСНОВНАЯ ФУНКЦИЯ # ============================================================================= def main(): """Основная функция: загрузка, обработка, визуализация, отчет""" all_files = sorted(IMAGES_TR.glob("*.nii.gz")) image_files = [f for f in all_files if is_valid_nifti_file(f)] print(f"\n📁 Найдено пациентов: {len(image_files)}") selected_patients = image_files[:2] patients_data = [] all_plots = {} for img_path in selected_patients: patient_id = img_path.stem.replace('.nii', '') mask_path = LABELS_TR / f"{patient_id}.nii.gz" print(f"\n🧠 ОБРАБОТКА: {patient_id}") try: image_data, _ = load_nifti(img_path) print(f" Изображение: {image_data.shape}") except Exception as e: print(f" ❌ Ошибка: {e}") continue real_mask_slice = None if mask_path.exists() and is_valid_nifti_file(mask_path): try: real_mask_data, _ = load_nifti(mask_path) real_mask_slice = real_mask_data[:, :, find_best_slice(image_data, axis=2)] print(f" Реальная маска: загружена") except: print(f" ⚠️ Маска не загружена") slice_idx = find_best_slice(image_data, axis=2) print(f" Срез: {slice_idx}") if image_data.ndim == 4: slice_data = image_data[:, :, slice_idx, 1] # T1c else: slice_data = image_data[:, :, slice_idx] # Собираем данные по площадям areas = {} if image_data.ndim == 4: modalities = ['T1', 'T1c', 'T2', 'FLAIR'] for i, mod in enumerate(modalities): mod_slice = image_data[:, :, slice_idx, i] mask = create_mask_combined(mod_slice) mask_processed = postprocess_mask(mask, min_size=50) areas[mod] = np.sum(mask_processed) else: mod_slice = image_data[:, :, slice_idx] mask = create_mask_combined(mod_slice) mask_processed = postprocess_mask(mask, min_size=50) areas['Original'] = np.sum(mask_processed) patients_data.append({ 'patient_id': patient_id, 'slice_idx': slice_idx, 'areas': areas }) print(" Создание графиков...") # 1. Сравнение методов fig = create_method_comparison(slice_data, real_mask_slice, patient_id, slice_idx) all_plots[f'{patient_id}_methods'] = fig_to_base64(fig) # 2. Все модальности if image_data.ndim == 4: fig = create_modality_comparison(image_data, patient_id, slice_idx) all_plots[f'{patient_id}_modalities'] = fig_to_base64(fig) print(f" ✅ {patient_id} обработан") # Сравнительный график print("\n📊 СОЗДАНИЕ СРАВНИТЕЛЬНОГО ГРАФИКА...") if len(selected_patients) >= 2: fig = create_comparison_patients(selected_patients) all_plots['comparison'] = fig_to_base64(fig) print(" ✅ Сравнительный график создан") # Генерация отчета print("\n📄 ГЕНЕРАЦИЯ HTML-ОТЧЕТА...") html_content = generate_html_report(patients_data, all_plots, selected_patients) report_path = OUTPUT_DIR / 'segmentation_full_report.html' with open(report_path, 'w', encoding='utf-8') as f: f.write(html_content) print(f" ✅ Отчет сохранен: {report_path}") print("\n" + "=" * 70) print("✨ ПОЛНЫЙ ОТЧЕТ СОЗДАН!") print("=" * 70) print(f"\n📁 РЕЗУЛЬТАТЫ:") print(f" {OUTPUT_DIR}") print(f"\n📄 ОТКРОЙТЕ ОТЧЕТ В БРАУЗЕРЕ:") print(f" {report_path}") print("\n📁 СОДЕРЖИМОЕ ОТЧЕТА:") print(" ✅ 3 метода сегментации (Оцу, Среднее+Std, Комбинированный)") print(" ✅ 2 пациента (BRATS_001, BRATS_002)") print(" ✅ 4 модальности (T1, T1c, T2, FLAIR)") print(" ✅ Полная клиническая интерпретация") print(" ✅ Рекомендации по использованию") print("=" * 70) if __name__ == "__main__": try: main() except Exception as e: print(f"\n❌ Ошибка: {e}") import traceback traceback.print_exc()