Files

991 lines
49 KiB
Python

# =============================================================================
# РАДИОМИЧЕСКИЙ АНАЛИЗ ОПУХОЛЕЙ МОЗГА
# ГЕНЕРАЦИЯ 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')} &nbsp;|&nbsp; Пациенты: BRATS_001, BRATS_002 &nbsp;|&nbsp; Признаков: {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) &nbsp;|&nbsp; Модальности: 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()