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