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
+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()