# ============================================================================= # РАДИОМИЧЕСКИЙ АНАЛИЗ ОПУХОЛЕЙ МОЗГА # ГЕНЕРАЦИЯ 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"""
| Признак | Пациент | T1 | T1c | T2 | FLAIR | Среднее |
|---|---|---|---|---|---|---|
| {feat_label} | {patient} | {t1_str} | {t1c_str} | {t2_str} | {flair_str} | {avg:.2f} |