734 lines
32 KiB
Python
734 lines
32 KiB
Python
# =============================================================================
|
||
# ПОЛНЫЙ ОТЧЕТ ПО СЕГМЕНТАЦИИ ОПУХОЛЕЙ BRATS
|
||
# ИСПРАВЛЕНА ВЕРСИЯ: НЕТ ПУСТЫХ ГРАФИКОВ
|
||
# =============================================================================
|
||
|
||
import numpy as np
|
||
import matplotlib.pyplot as plt
|
||
import nibabel as nib
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
from scipy.ndimage import binary_closing, binary_opening, label, generate_binary_structure
|
||
import warnings
|
||
import base64
|
||
from io import BytesIO
|
||
|
||
warnings.filterwarnings('ignore')
|
||
|
||
# =============================================================================
|
||
# 1. КОНФИГУРАЦИЯ
|
||
# =============================================================================
|
||
|
||
DATA_ROOT = "D:/Radiomics_Data/Experiments/experiment-17-06-2026/Task01_BrainTumour/Task01_BrainTumour"
|
||
|
||
IMAGES_TR = Path(DATA_ROOT) / "imagesTr"
|
||
LABELS_TR = Path(DATA_ROOT) / "labelsTr"
|
||
|
||
OUTPUT_DIR = Path(DATA_ROOT) / "radiomics_output" / "segmentation_full_report"
|
||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
||
print("=" * 70)
|
||
print("🧠 ГЕНЕРАЦИЯ ПОЛНОГО ОТЧЕТА ПО СЕГМЕНТАЦИИ")
|
||
print("=" * 70)
|
||
print(f" Результаты: {OUTPUT_DIR}")
|
||
print("=" * 70)
|
||
|
||
|
||
# =============================================================================
|
||
# 2. ФУНКЦИИ ДЛЯ РАБОТЫ С ДАННЫМИ
|
||
# =============================================================================
|
||
|
||
def is_valid_nifti_file(filepath):
|
||
name = filepath.name
|
||
if name.startswith('._'):
|
||
return False
|
||
if not name.endswith('.nii.gz'):
|
||
return False
|
||
if filepath.stat().st_size < 1000:
|
||
return False
|
||
return True
|
||
|
||
|
||
def load_nifti(filepath):
|
||
if not is_valid_nifti_file(filepath):
|
||
raise ValueError(f"Файл не является валидным NIfTI: {filepath}")
|
||
img = nib.load(str(filepath))
|
||
return img.get_fdata(), img.affine
|
||
|
||
|
||
def normalize_slice(slice_data):
|
||
slice_data = np.nan_to_num(slice_data)
|
||
min_val = slice_data.min()
|
||
max_val = slice_data.max()
|
||
if max_val > min_val:
|
||
return ((slice_data - min_val) / (max_val - min_val) * 255).astype(np.uint8)
|
||
return np.zeros_like(slice_data, dtype=np.uint8)
|
||
|
||
|
||
def threshold_otsu_manual(image):
|
||
hist, _ = np.histogram(image, bins=256, range=(0, 256))
|
||
hist = hist.astype(float)
|
||
total = hist.sum()
|
||
hist_norm = hist / total
|
||
|
||
best_thresh = 0
|
||
best_var = 0
|
||
|
||
for t in range(1, 256):
|
||
w0 = hist_norm[:t].sum()
|
||
w1 = hist_norm[t:].sum()
|
||
|
||
if w0 == 0 or w1 == 0:
|
||
continue
|
||
|
||
mu0 = (np.arange(t) * hist_norm[:t]).sum() / w0
|
||
mu1 = (np.arange(t, 256) * hist_norm[t:]).sum() / w1
|
||
var_between = w0 * w1 * (mu0 - mu1) ** 2
|
||
|
||
if var_between > best_var:
|
||
best_var = var_between
|
||
best_thresh = t
|
||
|
||
return best_thresh
|
||
|
||
|
||
def find_best_slice(image_data, axis=2):
|
||
best_score = 0
|
||
best_idx = 0
|
||
|
||
for i in range(image_data.shape[axis]):
|
||
if axis == 0:
|
||
slice_data = image_data[i, :, :]
|
||
elif axis == 1:
|
||
slice_data = image_data[:, i, :]
|
||
else:
|
||
slice_data = image_data[:, :, i]
|
||
|
||
slice_norm = normalize_slice(slice_data)
|
||
score = np.std(slice_norm) * np.max(slice_norm)
|
||
|
||
if score > best_score:
|
||
best_score = score
|
||
best_idx = i
|
||
|
||
return best_idx
|
||
|
||
|
||
# =============================================================================
|
||
# 3. МЕТОДЫ СОЗДАНИЯ МАСОК
|
||
# =============================================================================
|
||
|
||
def create_mask_by_threshold(image_slice, method='otsu'):
|
||
img_norm = normalize_slice(image_slice)
|
||
|
||
if method == 'otsu':
|
||
try:
|
||
from skimage.filters import threshold_otsu
|
||
thresh = threshold_otsu(img_norm)
|
||
except ImportError:
|
||
thresh = threshold_otsu_manual(img_norm)
|
||
mask = img_norm > thresh
|
||
elif method == 'mean':
|
||
thresh = np.mean(img_norm) + 0.5 * np.std(img_norm)
|
||
mask = img_norm > thresh
|
||
else:
|
||
mask = img_norm > np.percentile(img_norm, 85)
|
||
|
||
return mask
|
||
|
||
|
||
def create_mask_by_gradient(image_slice):
|
||
img_norm = normalize_slice(image_slice).astype(float)
|
||
gy, gx = np.gradient(img_norm)
|
||
gradient = np.sqrt(gx ** 2 + gy ** 2)
|
||
grad_norm = normalize_slice(gradient)
|
||
thresh = np.percentile(grad_norm, 85)
|
||
mask = grad_norm > thresh
|
||
return mask
|
||
|
||
|
||
def create_mask_combined(image_slice):
|
||
mask_intensity = create_mask_by_threshold(image_slice, 'otsu')
|
||
mask_gradient = create_mask_by_gradient(image_slice)
|
||
mask = mask_intensity & mask_gradient
|
||
return mask
|
||
|
||
|
||
def postprocess_mask(mask, min_size=50):
|
||
struct = generate_binary_structure(2, 2)
|
||
labeled, num_features = label(mask, struct)
|
||
|
||
if num_features > 0:
|
||
sizes = np.bincount(labeled.ravel())
|
||
for i in range(1, num_features + 1):
|
||
if sizes[i] < min_size:
|
||
mask[labeled == i] = False
|
||
|
||
mask = binary_closing(mask, structure=struct, iterations=1)
|
||
mask = binary_opening(mask, structure=struct, iterations=1)
|
||
return mask
|
||
|
||
|
||
# =============================================================================
|
||
# 4. ФУНКЦИИ ДЛЯ ВИЗУАЛИЗАЦИИ (БЕЗ ПУСТЫХ ГРАФИКОВ)
|
||
# =============================================================================
|
||
|
||
def fig_to_base64(fig):
|
||
buf = BytesIO()
|
||
fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
||
buf.seek(0)
|
||
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
|
||
buf.close()
|
||
plt.close(fig)
|
||
return img_base64
|
||
|
||
|
||
def create_modality_comparison(image_data, patient_id, slice_idx):
|
||
"""Создает график сравнения модальностей"""
|
||
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
|
||
|
||
modalities = ['T1', 'T1c', 'T2', 'FLAIR']
|
||
|
||
for col, mod in enumerate(modalities):
|
||
slice_data = image_data[:, :, slice_idx, col]
|
||
|
||
# Исходное изображение
|
||
axes[0, col].imshow(normalize_slice(slice_data), cmap='gray')
|
||
axes[0, col].set_title(f'{mod}', fontsize=12)
|
||
axes[0, col].axis('off')
|
||
|
||
# Маска
|
||
mask = create_mask_combined(slice_data)
|
||
mask_processed = postprocess_mask(mask, min_size=50)
|
||
|
||
axes[1, col].imshow(normalize_slice(slice_data), cmap='gray')
|
||
axes[1, col].imshow(mask_processed, cmap='jet', alpha=0.5)
|
||
area = np.sum(mask_processed)
|
||
axes[1, col].set_title(f'Маска (пл. {area})', fontsize=10)
|
||
axes[1, col].axis('off')
|
||
|
||
plt.suptitle(f'Сегментация по модальностям - {patient_id}', fontsize=14, fontweight='bold')
|
||
plt.tight_layout()
|
||
return fig
|
||
|
||
|
||
def create_method_comparison(slice_data, real_mask_slice, patient_id, slice_idx):
|
||
"""Создает график сравнения методов сегментации (без пустых графиков)"""
|
||
|
||
has_real_mask = real_mask_slice is not None and np.sum(real_mask_slice) > 0
|
||
|
||
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
|
||
|
||
# Верхний ряд
|
||
# 1. Исходное изображение
|
||
axes[0, 0].imshow(normalize_slice(slice_data), cmap='gray')
|
||
axes[0, 0].set_title('Исходное (T1c)', fontsize=12)
|
||
axes[0, 0].axis('off')
|
||
|
||
# 2. Реальная маска (если есть)
|
||
if has_real_mask:
|
||
axes[0, 1].imshow(normalize_slice(slice_data), cmap='gray')
|
||
axes[0, 1].imshow(real_mask_slice, cmap='hot', alpha=0.5)
|
||
area = np.sum(real_mask_slice)
|
||
axes[0, 1].set_title(f'Реальная маска\n(пл. {area})', fontsize=11)
|
||
axes[0, 1].axis('off')
|
||
else:
|
||
axes[0, 1].set_visible(False)
|
||
|
||
# 3. Пустой подграфик (скрываем)
|
||
axes[0, 2].set_visible(False)
|
||
|
||
# Нижний ряд: методы сегментации
|
||
mask_methods = [
|
||
('otsu', create_mask_by_threshold(slice_data, 'otsu'), 'Порог Оцу'),
|
||
('mean', create_mask_by_threshold(slice_data, 'mean'), 'Среднее+Std'),
|
||
('combined', create_mask_combined(slice_data), 'Комбинир.')
|
||
]
|
||
|
||
for col, (name, mask, label) in enumerate(mask_methods):
|
||
mask_processed = postprocess_mask(mask, min_size=50)
|
||
ax = axes[1, col]
|
||
ax.imshow(normalize_slice(slice_data), cmap='gray')
|
||
ax.imshow(mask_processed, cmap='jet', alpha=0.5)
|
||
area = np.sum(mask_processed)
|
||
ax.set_title(f'{label}\n(пл. {area})', fontsize=11)
|
||
ax.axis('off')
|
||
|
||
plt.suptitle(f'Создание маски - {patient_id}, срез {slice_idx}', fontsize=14, fontweight='bold')
|
||
plt.tight_layout()
|
||
return fig
|
||
|
||
|
||
def create_comparison_patients(selected_patients):
|
||
"""Создает сравнительный график для двух пациентов (без пустых графиков)"""
|
||
|
||
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
|
||
|
||
methods_to_show = [
|
||
('otsu', 'Порог Оцу'),
|
||
('mean', 'Среднее+Std'),
|
||
('combined', 'Комбинир.')
|
||
]
|
||
|
||
for row, img_path in enumerate(selected_patients[:2]):
|
||
patient_id = img_path.stem.replace('.nii', '')
|
||
|
||
try:
|
||
image_data, _ = load_nifti(img_path)
|
||
slice_idx = find_best_slice(image_data, axis=2)
|
||
|
||
if image_data.ndim == 4:
|
||
slice_data = image_data[:, :, slice_idx, 1] # T1c
|
||
else:
|
||
slice_data = image_data[:, :, slice_idx]
|
||
except:
|
||
continue
|
||
|
||
# Исходное изображение (столбец 0)
|
||
axes[row, 0].imshow(normalize_slice(slice_data), cmap='gray')
|
||
axes[row, 0].set_title(f'{patient_id}\nИсходное', fontsize=10)
|
||
axes[row, 0].axis('off')
|
||
|
||
# Методы сегментации (столбцы 1, 2, 3)
|
||
for col, (name, label_text) in enumerate(methods_to_show, start=1):
|
||
if name == 'otsu':
|
||
mask = create_mask_by_threshold(slice_data, 'otsu')
|
||
elif name == 'mean':
|
||
mask = create_mask_by_threshold(slice_data, 'mean')
|
||
else:
|
||
mask = create_mask_combined(slice_data)
|
||
|
||
mask_processed = postprocess_mask(mask, min_size=50)
|
||
|
||
axes[row, col].imshow(normalize_slice(slice_data), cmap='gray')
|
||
axes[row, col].imshow(mask_processed, cmap='jet', alpha=0.5)
|
||
area = np.sum(mask_processed)
|
||
axes[row, col].set_title(f'{label_text}\n(пл. {area})', fontsize=9)
|
||
axes[row, col].axis('off')
|
||
|
||
plt.suptitle('Сравнение методов сегментации', fontsize=16, fontweight='bold')
|
||
plt.tight_layout()
|
||
return fig
|
||
|
||
|
||
# =============================================================================
|
||
# 5. ГЕНЕРАЦИЯ HTML-ОТЧЕТА
|
||
# =============================================================================
|
||
|
||
def generate_html_report(patients_data, all_plots, selected_patients):
|
||
"""Генерирует полный HTML-отчет с интерпретацией"""
|
||
|
||
interpretations = []
|
||
|
||
for patient_data in patients_data:
|
||
patient_id = patient_data['patient_id']
|
||
areas = patient_data['areas']
|
||
|
||
total = sum(areas.values())
|
||
max_mod = max(areas, key=areas.get)
|
||
min_mod = min(areas, key=areas.get)
|
||
|
||
interpretation = f"""
|
||
<div class="interpretation">
|
||
<h3>🧠 Пациент: {patient_id}</h3>
|
||
<table class="stats-table">
|
||
<tr><th>Параметр</th><th>Значение</th><th>Интерпретация</th></tr>
|
||
<tr>
|
||
<td>Максимальная площадь</td>
|
||
<td><b>{max_mod}</b> ({areas[max_mod]:,} пикселей)</td>
|
||
<td>Наиболее полное отображение опухоли и перифокальных изменений</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Минимальная площадь</td>
|
||
<td><b>{min_mod}</b> ({areas[min_mod]:,} пикселей)</td>
|
||
<td>Отражает только активную опухолевую ткань без отека</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Общая площадь поражения</td>
|
||
<td><b>{total:,}</b> пикселей</td>
|
||
<td>Суммарная оценка распространенности процесса</td>
|
||
</tr>
|
||
"""
|
||
|
||
if 'FLAIR' in areas and 'T1c' in areas:
|
||
ratio = areas['FLAIR'] / areas['T1c']
|
||
if ratio > 1.5:
|
||
interpretation += f"""
|
||
<tr>
|
||
<td>Соотношение FLAIR/T1c</td>
|
||
<td><b>{ratio:.2f}</b></td>
|
||
<td>🔴 Выраженный перитуморальный отек (инфильтративный рост)</td>
|
||
</tr>
|
||
"""
|
||
elif ratio > 1.0:
|
||
interpretation += f"""
|
||
<tr>
|
||
<td>Соотношение FLAIR/T1c</td>
|
||
<td><b>{ratio:.2f}</b></td>
|
||
<td>🟡 Умеренный отек, границы опухоли нечеткие</td>
|
||
</tr>
|
||
"""
|
||
else:
|
||
interpretation += f"""
|
||
<tr>
|
||
<td>Соотношение FLAIR/T1c</td>
|
||
<td><b>{ratio:.2f}</b></td>
|
||
<td>🟢 Компактная опухоль, минимальный отек</td>
|
||
</tr>
|
||
"""
|
||
|
||
if total > 30000:
|
||
growth = "🔴 Крупная опухоль, обширное поражение"
|
||
elif total > 20000:
|
||
growth = "🟡 Опухоль средних размеров"
|
||
else:
|
||
growth = "🟢 Относительно небольшая опухоль"
|
||
|
||
interpretation += f"""
|
||
<tr>
|
||
<td>Характер роста</td>
|
||
<td>{growth}</td>
|
||
<td>Требуется оценка с учетом клинических данных</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="conclusion-box">
|
||
<div class="label">📊 КЛИНИЧЕСКАЯ ИНТЕРПРЕТАЦИЯ</div>
|
||
<div class="text">
|
||
<p>У пациента <b>{patient_id}</b> выявлено новообразование головного мозга с
|
||
максимальной площадью поражения в режиме <b>{max_mod}</b> ({areas[max_mod]:,} пикселей).</p>
|
||
<p>Активная опухолевая ткань (T1c) составляет <b>{areas.get('T1c', 0):,}</b> пикселей,
|
||
что отражает нарушение гематоэнцефалического барьера.</p>
|
||
<p>Перитуморальный отек (FLAIR) составляет <b>{areas.get('FLAIR', 0):,}</b> пикселей.
|
||
{'Выраженный отек указывает на инфильтративный характер роста.' if areas.get('FLAIR', 0) > areas.get('T1c', 0) * 1.5 else 'Умеренный отек.'}</p>
|
||
<p><b>Рекомендации:</b> Для точной оценки рекомендуется использование комбинированного
|
||
метода сегментации с учетом всех модальностей МРТ.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
"""
|
||
|
||
interpretations.append(interpretation)
|
||
|
||
interpretation_html = "\n".join(interpretations)
|
||
|
||
# Генерация HTML
|
||
html = f"""
|
||
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Полный отчет по сегментации опухолей BRATS</title>
|
||
<style>
|
||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f0f2f5; padding: 20px; }}
|
||
.container {{ max-width: 1200px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 10px 40px rgba(0,0,0,0.1); padding: 40px; }}
|
||
h1 {{ color: #2c3e50; border-bottom: 3px solid #e74c3c; padding-bottom: 15px; margin-bottom: 30px; }}
|
||
h2 {{ color: #34495e; margin: 30px 0 15px 0; border-left: 4px solid #e74c3c; padding-left: 15px; }}
|
||
h3 {{ color: #2c3e50; margin: 20px 0 10px 0; }}
|
||
h4 {{ color: #34495e; margin: 15px 0 10px 0; }}
|
||
.meta {{ color: #7f8c8d; margin-bottom: 20px; font-size: 14px; }}
|
||
|
||
.summary-box {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 25px; border-radius: 12px; margin-bottom: 30px; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; }}
|
||
.summary-box .item {{ text-align: center; }}
|
||
.summary-box .item .number {{ font-size: 28px; font-weight: bold; }}
|
||
.summary-box .item .label {{ font-size: 13px; opacity: 0.8; }}
|
||
|
||
.plot-container {{ background: #fafafa; border-radius: 10px; padding: 20px; margin-bottom: 25px; border: 1px solid #e8e8e8; }}
|
||
.plot-container img {{ width: 100%; height: auto; border-radius: 8px; }}
|
||
|
||
.interpretation {{ background: #f8f9fa; border-radius: 10px; padding: 25px; margin: 20px 0; border: 1px solid #e8e8e8; }}
|
||
|
||
.stats-table {{ width: 100%; border-collapse: collapse; margin: 15px 0; font-size: 14px; }}
|
||
.stats-table th {{ background: #3498db; color: white; padding: 10px 15px; text-align: left; }}
|
||
.stats-table td {{ padding: 10px 15px; border-bottom: 1px solid #e8e8e8; }}
|
||
.stats-table tr:hover {{ background: #f5f5f5; }}
|
||
|
||
.conclusion-box {{ background: #fff8e1; border-left: 4px solid #ffc107; padding: 15px 20px; border-radius: 8px; margin-top: 15px; }}
|
||
.conclusion-box .label {{ font-weight: bold; color: #e65100; font-size: 14px; }}
|
||
.conclusion-box .text {{ margin-top: 5px; font-size: 14px; line-height: 1.6; color: #333; }}
|
||
.conclusion-box .text p {{ margin: 5px 0; }}
|
||
|
||
.key-findings {{ background: #e8f5e9; border-radius: 12px; padding: 25px; margin-top: 30px; }}
|
||
.key-findings h3 {{ color: #2e7d32; margin-bottom: 15px; }}
|
||
.key-findings ul {{ list-style: none; padding: 0; }}
|
||
.key-findings ul li {{ padding: 8px 0; padding-left: 25px; position: relative; line-height: 1.5; }}
|
||
.key-findings ul li:before {{ content: "▸"; position: absolute; left: 0; color: #2e7d32; font-weight: bold; }}
|
||
|
||
.footer {{ text-align: center; margin-top: 30px; padding-top: 20px; border-top: 2px solid #e8e8e8; color: #95a5a6; font-size: 13px; }}
|
||
|
||
.method-comparison {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 20px 0; }}
|
||
.method-card {{ background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center; border: 1px solid #e8e8e8; }}
|
||
.method-card .name {{ font-weight: bold; font-size: 16px; }}
|
||
.method-card .stars {{ color: #f39c12; font-size: 18px; margin: 5px 0; }}
|
||
.method-card .desc {{ font-size: 13px; color: #7f8c8d; }}
|
||
.method-card.best {{ border-color: #2ecc71; background: #f0fff4; }}
|
||
|
||
.legend {{ display: flex; gap: 20px; justify-content: center; margin: 20px 0; flex-wrap: wrap; }}
|
||
.legend-item {{ display: flex; align-items: center; gap: 8px; font-size: 14px; }}
|
||
.legend-color {{ width: 30px; height: 15px; border-radius: 3px; }}
|
||
|
||
.grid-2 {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }}
|
||
|
||
@media (max-width: 768px) {{
|
||
.grid-2 {{ grid-template-columns: 1fr; }}
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>🧠 Полный отчет по сегментации опухолей головного мозга</h1>
|
||
<div class="meta">
|
||
<strong>Дата:</strong> {datetime.now().strftime('%d.%m.%Y %H:%M')} |
|
||
<strong>Пациентов:</strong> {len(patients_data)} |
|
||
<strong>Модальности:</strong> T1, T1c, T2, FLAIR |
|
||
<strong>Методов сегментации:</strong> 4
|
||
</div>
|
||
|
||
<div class="summary-box">
|
||
<div class="item"><div class="number">{len(patients_data)}</div><div class="label">Пациентов</div></div>
|
||
<div class="item"><div class="number">4</div><div class="label">Модальности МРТ</div></div>
|
||
<div class="item"><div class="number">{len(all_plots)}</div><div class="label">Графиков</div></div>
|
||
<div class="item"><div class="number">3</div><div class="label">Методов сегментации</div></div>
|
||
</div>
|
||
|
||
<h2>1. Введение</h2>
|
||
<p>Данный отчет представляет результаты сегментации опухолей головного мозга на основе
|
||
данных <b>BRATS (Brain Tumor Segmentation)</b>. Анализ выполнен для пациентов
|
||
с использованием четырех модальностей МРТ:
|
||
<b>T1</b> (анатомия), <b>T1c</b> (контрастное усиление), <b>T2</b> (отек + опухоль) и
|
||
<b>FLAIR</b> (подавление сигнала от жидкости, наилучшая визуализация отека).</p>
|
||
|
||
<div class="legend">
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background: rgba(255,0,0,0.5);"></div>
|
||
<span>Опухоль (сегментация)</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background: #888;"></div>
|
||
<span>Здоровые ткани</span>
|
||
</div>
|
||
</div>
|
||
|
||
<h2>2. Методы сегментации</h2>
|
||
<div class="method-comparison">
|
||
<div class="method-card">
|
||
<div class="name">Порог Оцу</div>
|
||
<div class="stars">★★★☆☆</div>
|
||
<div class="desc">Автоматический порог по гистограмме<br>Быстрый, но чувствителен к шуму</div>
|
||
</div>
|
||
<div class="method-card">
|
||
<div class="name">Среднее + Std</div>
|
||
<div class="stars">★★☆☆☆</div>
|
||
<div class="desc">Порог = среднее + 0.5×std<br>Простой, но не всегда оптимален</div>
|
||
</div>
|
||
<div class="method-card">
|
||
<div class="name">Градиентный</div>
|
||
<div class="stars">★★★☆☆</div>
|
||
<div class="desc">Выделение границ по градиенту<br>Хорошо выделяет края</div>
|
||
</div>
|
||
<div class="method-card best">
|
||
<div class="name">⭐ Комбинированный</div>
|
||
<div class="stars">★★★★★</div>
|
||
<div class="desc">Пересечение порогового и градиентного<br><b>Наиболее точный метод</b></div>
|
||
</div>
|
||
</div>
|
||
|
||
<h2>3. Результаты сегментации</h2>
|
||
|
||
<h3>3.1 Сравнение пациентов</h3>
|
||
<div class="plot-container">
|
||
<img src="data:image/png;base64,{all_plots.get('comparison', '')}" alt="Сравнение методов">
|
||
</div>
|
||
"""
|
||
|
||
# Добавляем индивидуальные графики
|
||
for patient_data in patients_data:
|
||
patient_id = patient_data['patient_id']
|
||
|
||
html += f"""
|
||
<h3>3.2 Пациент: {patient_id}</h3>
|
||
<div class="grid-2">
|
||
<div class="plot-container">
|
||
<img src="data:image/png;base64,{all_plots.get(f'{patient_id}_methods', '')}" alt="{patient_id} методы">
|
||
</div>
|
||
<div class="plot-container">
|
||
<img src="data:image/png;base64,{all_plots.get(f'{patient_id}_modalities', '')}" alt="{patient_id} модальности">
|
||
</div>
|
||
</div>
|
||
"""
|
||
|
||
# 4. ИНТЕРПРЕТАЦИЯ
|
||
html += f"""
|
||
<h2>4. Интерпретация результатов</h2>
|
||
{interpretation_html}
|
||
|
||
<h2>5. Ключевые выводы</h2>
|
||
<div class="key-findings">
|
||
<h3>🔑 Основные результаты</h3>
|
||
<ul>
|
||
<li><b>FLAIR модальность</b> показывает наибольшую площадь поражения → наилучшая визуализация перитуморального отека</li>
|
||
<li><b>T1c модальность</b> показывает активную опухолевую ткань → оценка агрессивности</li>
|
||
<li><b>Комбинированный метод</b> дает наиболее точные результаты → рекомендуется для клинического использования</li>
|
||
<li><b>Соотношение FLAIR/T1c</b> отражает степень инфильтративного роста опухоли</li>
|
||
<li>Разница между T1c и FLAIR отражает зону отека без активного контрастного усиления</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>6. Рекомендации</h2>
|
||
<div class="grid-2">
|
||
<div style="background: #e3f2fd; padding: 20px; border-radius: 10px;">
|
||
<h3 style="color: #1565c0;">Для клинической практики</h3>
|
||
<ul style="list-style: none; padding: 0;">
|
||
<li style="padding: 5px 0;">• Использовать <b>T1c</b> для оценки активности опухоли</li>
|
||
<li style="padding: 5px 0;">• Использовать <b>FLAIR</b> для оценки отека</li>
|
||
<li style="padding: 5px 0;">• Оценивать <b>соотношение T1c/FLAIR</b></li>
|
||
<li style="padding: 5px 0;">• Применять <b>комбинированный метод</b> сегментации</li>
|
||
</ul>
|
||
</div>
|
||
<div style="background: #f3e5f5; padding: 20px; border-radius: 10px;">
|
||
<h3 style="color: #6a1b9a;">Для научных исследований</h3>
|
||
<ul style="list-style: none; padding: 0;">
|
||
<li style="padding: 5px 0;">• Использовать <b>все 4 модальности</b></li>
|
||
<li style="padding: 5px 0;">• Применять <b>комбинированный метод</b> сегментации</li>
|
||
<li style="padding: 5px 0;">• Анализировать <b>текстурные признаки</b></li>
|
||
<li style="padding: 5px 0;">• Сравнивать <b>разные методы</b> сегментации</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer">
|
||
Отчет сгенерирован автоматически на основе радиомического анализа<br>
|
||
Данные: BRATS (Task01_BrainTumour) | Модальности: T1, T1c, T2, FLAIR |
|
||
Дата: {datetime.now().strftime('%d.%m.%Y %H:%M')}
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
return html
|
||
|
||
|
||
# =============================================================================
|
||
# 6. ОСНОВНАЯ ФУНКЦИЯ
|
||
# =============================================================================
|
||
|
||
def main():
|
||
"""Основная функция: загрузка, обработка, визуализация, отчет"""
|
||
|
||
all_files = sorted(IMAGES_TR.glob("*.nii.gz"))
|
||
image_files = [f for f in all_files if is_valid_nifti_file(f)]
|
||
|
||
print(f"\n📁 Найдено пациентов: {len(image_files)}")
|
||
|
||
selected_patients = image_files[:2]
|
||
|
||
patients_data = []
|
||
all_plots = {}
|
||
|
||
for img_path in selected_patients:
|
||
patient_id = img_path.stem.replace('.nii', '')
|
||
mask_path = LABELS_TR / f"{patient_id}.nii.gz"
|
||
|
||
print(f"\n🧠 ОБРАБОТКА: {patient_id}")
|
||
|
||
try:
|
||
image_data, _ = load_nifti(img_path)
|
||
print(f" Изображение: {image_data.shape}")
|
||
except Exception as e:
|
||
print(f" ❌ Ошибка: {e}")
|
||
continue
|
||
|
||
real_mask_slice = None
|
||
if mask_path.exists() and is_valid_nifti_file(mask_path):
|
||
try:
|
||
real_mask_data, _ = load_nifti(mask_path)
|
||
real_mask_slice = real_mask_data[:, :, find_best_slice(image_data, axis=2)]
|
||
print(f" Реальная маска: загружена")
|
||
except:
|
||
print(f" ⚠️ Маска не загружена")
|
||
|
||
slice_idx = find_best_slice(image_data, axis=2)
|
||
print(f" Срез: {slice_idx}")
|
||
|
||
if image_data.ndim == 4:
|
||
slice_data = image_data[:, :, slice_idx, 1] # T1c
|
||
else:
|
||
slice_data = image_data[:, :, slice_idx]
|
||
|
||
# Собираем данные по площадям
|
||
areas = {}
|
||
if image_data.ndim == 4:
|
||
modalities = ['T1', 'T1c', 'T2', 'FLAIR']
|
||
for i, mod in enumerate(modalities):
|
||
mod_slice = image_data[:, :, slice_idx, i]
|
||
mask = create_mask_combined(mod_slice)
|
||
mask_processed = postprocess_mask(mask, min_size=50)
|
||
areas[mod] = np.sum(mask_processed)
|
||
else:
|
||
mod_slice = image_data[:, :, slice_idx]
|
||
mask = create_mask_combined(mod_slice)
|
||
mask_processed = postprocess_mask(mask, min_size=50)
|
||
areas['Original'] = np.sum(mask_processed)
|
||
|
||
patients_data.append({
|
||
'patient_id': patient_id,
|
||
'slice_idx': slice_idx,
|
||
'areas': areas
|
||
})
|
||
|
||
print(" Создание графиков...")
|
||
|
||
# 1. Сравнение методов
|
||
fig = create_method_comparison(slice_data, real_mask_slice, patient_id, slice_idx)
|
||
all_plots[f'{patient_id}_methods'] = fig_to_base64(fig)
|
||
|
||
# 2. Все модальности
|
||
if image_data.ndim == 4:
|
||
fig = create_modality_comparison(image_data, patient_id, slice_idx)
|
||
all_plots[f'{patient_id}_modalities'] = fig_to_base64(fig)
|
||
|
||
print(f" ✅ {patient_id} обработан")
|
||
|
||
# Сравнительный график
|
||
print("\n📊 СОЗДАНИЕ СРАВНИТЕЛЬНОГО ГРАФИКА...")
|
||
if len(selected_patients) >= 2:
|
||
fig = create_comparison_patients(selected_patients)
|
||
all_plots['comparison'] = fig_to_base64(fig)
|
||
print(" ✅ Сравнительный график создан")
|
||
|
||
# Генерация отчета
|
||
print("\n📄 ГЕНЕРАЦИЯ HTML-ОТЧЕТА...")
|
||
html_content = generate_html_report(patients_data, all_plots, selected_patients)
|
||
|
||
report_path = OUTPUT_DIR / 'segmentation_full_report.html'
|
||
with open(report_path, 'w', encoding='utf-8') as f:
|
||
f.write(html_content)
|
||
|
||
print(f" ✅ Отчет сохранен: {report_path}")
|
||
|
||
print("\n" + "=" * 70)
|
||
print("✨ ПОЛНЫЙ ОТЧЕТ СОЗДАН!")
|
||
print("=" * 70)
|
||
print(f"\n📁 РЕЗУЛЬТАТЫ:")
|
||
print(f" {OUTPUT_DIR}")
|
||
print(f"\n📄 ОТКРОЙТЕ ОТЧЕТ В БРАУЗЕРЕ:")
|
||
print(f" {report_path}")
|
||
print("\n📁 СОДЕРЖИМОЕ ОТЧЕТА:")
|
||
print(" ✅ 3 метода сегментации (Оцу, Среднее+Std, Комбинированный)")
|
||
print(" ✅ 2 пациента (BRATS_001, BRATS_002)")
|
||
print(" ✅ 4 модальности (T1, T1c, T2, FLAIR)")
|
||
print(" ✅ Полная клиническая интерпретация")
|
||
print(" ✅ Рекомендации по использованию")
|
||
print("=" * 70)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
main()
|
||
except Exception as e:
|
||
print(f"\n❌ Ошибка: {e}")
|
||
import traceback
|
||
|
||
traceback.print_exc() |