Initial commit: project structure, scripts and documentation
This commit is contained in:
@@ -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')} | Пациенты: BRATS_001, BRATS_002 | Признаков: {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) | Модальности: 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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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')} |
|
||||
<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()
|
||||
Reference in New Issue
Block a user