| | |
| | """ |
| | NeuroScan AI 完整工作流演示脚本 |
| | |
| | 这个脚本展示了从输入到输出的完整流程: |
| | 1. 加载数据 |
| | 2. 图像配准 |
| | 3. 变化检测 |
| | 4. 特征提取 |
| | 5. RECIST 评估 |
| | 6. LLM 报告生成 |
| | 7. 可视化输出 |
| | |
| | 使用方法: |
| | python scripts/workflow_demo.py |
| | """ |
| |
|
| | import os |
| | import sys |
| |
|
| | |
| | PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| | sys.path.insert(0, PROJECT_ROOT) |
| |
|
| | import numpy as np |
| | import nibabel as nib |
| | import matplotlib.pyplot as plt |
| | from datetime import datetime |
| | import json |
| |
|
| | |
| | plt.rcParams['font.sans-serif'] = ['DejaVu Sans', 'SimHei', 'Arial Unicode MS'] |
| | plt.rcParams['axes.unicode_minus'] = False |
| |
|
| |
|
| | def print_header(title): |
| | """打印格式化标题""" |
| | print("\n" + "=" * 70) |
| | print(f" {title}") |
| | print("=" * 70) |
| |
|
| |
|
| | def print_step(step_num, title): |
| | """打印步骤标题""" |
| | print(f"\n{'─' * 70}") |
| | print(f" 📌 步骤 {step_num}: {title}") |
| | print(f"{'─' * 70}") |
| |
|
| |
|
| | def load_data(baseline_path, followup_path): |
| | """ |
| | 阶段 1: 加载数据 |
| | |
| | 输入: NIfTI 文件路径 |
| | 输出: numpy 数组和元数据 |
| | """ |
| | print_step(1, "加载医学影像数据") |
| | |
| | print(f"\n 📂 基线扫描: {baseline_path}") |
| | print(f" 📂 随访扫描: {followup_path}") |
| | |
| | |
| | baseline_nii = nib.load(baseline_path) |
| | followup_nii = nib.load(followup_path) |
| | |
| | |
| | baseline_data = baseline_nii.get_fdata().astype(np.float32) |
| | followup_data = followup_nii.get_fdata().astype(np.float32) |
| | |
| | |
| | spacing = baseline_nii.header.get_zooms()[:3] |
| | affine = baseline_nii.affine |
| | |
| | print(f"\n ✅ 数据加载完成:") |
| | print(f" - 基线尺寸: {baseline_data.shape}") |
| | print(f" - 随访尺寸: {followup_data.shape}") |
| | print(f" - 体素间距: {spacing} mm") |
| | print(f" - 基线 HU 范围: [{baseline_data.min():.0f}, {baseline_data.max():.0f}]") |
| | print(f" - 随访 HU 范围: [{followup_data.min():.0f}, {followup_data.max():.0f}]") |
| | |
| | return { |
| | 'baseline': baseline_data, |
| | 'followup': followup_data, |
| | 'spacing': spacing, |
| | 'affine': affine |
| | } |
| |
|
| |
|
| | def perform_registration(baseline, followup, spacing): |
| | """ |
| | 阶段 2: 图像配准 |
| | |
| | 将随访扫描对齐到基线扫描 |
| | """ |
| | print_step(2, "图像配准 (Registration)") |
| | |
| | print("\n 🔧 配准策略:") |
| | print(" 1. 刚性配准 (Rigid): 校正体位差异") |
| | print(" 2. 非刚性配准 (Deformable): 校正呼吸运动") |
| | |
| | try: |
| | from app.services.registration import ImageRegistrator |
| | |
| | registrator = ImageRegistrator() |
| | |
| | |
| | print("\n ⏳ 执行刚性配准...") |
| | rigid_result = registrator.rigid_registration( |
| | fixed_image=baseline, |
| | moving_image=followup, |
| | spacing=spacing |
| | ) |
| | print(" ✅ 刚性配准完成") |
| | |
| | |
| | print(" ⏳ 执行非刚性配准...") |
| | deformable_result = registrator.deformable_registration( |
| | fixed_image=baseline, |
| | moving_image=rigid_result['registered_image'], |
| | spacing=spacing |
| | ) |
| | print(" ✅ 非刚性配准完成") |
| | |
| | registered = deformable_result['registered_image'] |
| | |
| | except Exception as e: |
| | print(f"\n ⚠️ 配准服务不可用: {e}") |
| | print(" 使用原始图像继续...") |
| | registered = followup |
| | |
| | print(f"\n ✅ 配准完成:") |
| | print(f" - 输出尺寸: {registered.shape}") |
| | |
| | return registered |
| |
|
| |
|
| | def detect_changes(baseline, registered, spacing): |
| | """ |
| | 阶段 3: 变化检测 |
| | |
| | 计算两次扫描之间的差异 |
| | """ |
| | print_step(3, "变化检测 (Change Detection)") |
| | |
| | print("\n 🔍 分析内容:") |
| | print(" - 体素级差异计算") |
| | print(" - 变化区域识别") |
| | print(" - 量化指标提取") |
| | |
| | |
| | diff_map = registered - baseline |
| | abs_diff = np.abs(diff_map) |
| | |
| | |
| | threshold = 30 |
| | significant_mask = abs_diff > threshold |
| | |
| | |
| | voxel_volume = np.prod(spacing) |
| | changed_voxels = significant_mask.sum() |
| | changed_volume = changed_voxels * voxel_volume |
| | |
| | |
| | if changed_voxels > 0: |
| | mean_change = diff_map[significant_mask].mean() |
| | max_increase = diff_map.max() |
| | max_decrease = diff_map.min() |
| | else: |
| | mean_change = 0 |
| | max_increase = 0 |
| | max_decrease = 0 |
| | |
| | print(f"\n ✅ 变化检测完成:") |
| | print(f" - 显著变化阈值: {threshold} HU") |
| | print(f" - 变化体素数: {changed_voxels:,}") |
| | print(f" - 变化体积: {changed_volume/1000:.2f} cm³") |
| | print(f" - 平均变化: {mean_change:+.1f} HU") |
| | print(f" - 最大增加: {max_increase:+.1f} HU") |
| | print(f" - 最大减少: {max_decrease:+.1f} HU") |
| | |
| | return { |
| | 'diff_map': diff_map, |
| | 'significant_mask': significant_mask, |
| | 'changed_volume_mm3': changed_volume, |
| | 'mean_change': mean_change, |
| | 'max_increase': max_increase, |
| | 'max_decrease': max_decrease |
| | } |
| |
|
| |
|
| | def extract_features(baseline, registered, diff_map, spacing): |
| | """ |
| | 阶段 4: 特征提取 |
| | |
| | 提取病灶的量化特征 |
| | """ |
| | print_step(4, "特征提取 (Feature Extraction)") |
| | |
| | print("\n 📊 提取特征:") |
| | print(" - 体积测量") |
| | print(" - 密度分析") |
| | print(" - 形态学特征") |
| | |
| | |
| | abs_diff = np.abs(diff_map) |
| | threshold = np.percentile(abs_diff, 99) |
| | roi_mask = abs_diff > threshold |
| | |
| | if roi_mask.sum() == 0: |
| | roi_mask = abs_diff > 30 |
| | |
| | voxel_volume = np.prod(spacing) |
| | |
| | |
| | baseline_roi = baseline[roi_mask] if roi_mask.sum() > 0 else baseline.flatten() |
| | baseline_features = { |
| | 'volume_mm3': roi_mask.sum() * voxel_volume, |
| | 'mean_hu': float(baseline_roi.mean()), |
| | 'std_hu': float(baseline_roi.std()), |
| | 'min_hu': float(baseline_roi.min()), |
| | 'max_hu': float(baseline_roi.max()) |
| | } |
| | |
| | |
| | followup_roi = registered[roi_mask] if roi_mask.sum() > 0 else registered.flatten() |
| | followup_features = { |
| | 'volume_mm3': roi_mask.sum() * voxel_volume, |
| | 'mean_hu': float(followup_roi.mean()), |
| | 'std_hu': float(followup_roi.std()), |
| | 'min_hu': float(followup_roi.min()), |
| | 'max_hu': float(followup_roi.max()) |
| | } |
| | |
| | |
| | density_change = followup_features['mean_hu'] - baseline_features['mean_hu'] |
| | |
| | print(f"\n ✅ 特征提取完成:") |
| | print(f" 基线特征:") |
| | print(f" - ROI 体积: {baseline_features['volume_mm3']/1000:.2f} cm³") |
| | print(f" - 平均密度: {baseline_features['mean_hu']:.1f} HU") |
| | print(f" 随访特征:") |
| | print(f" - ROI 体积: {followup_features['volume_mm3']/1000:.2f} cm³") |
| | print(f" - 平均密度: {followup_features['mean_hu']:.1f} HU") |
| | print(f" 变化:") |
| | print(f" - 密度变化: {density_change:+.1f} HU") |
| | |
| | return { |
| | 'baseline': baseline_features, |
| | 'followup': followup_features, |
| | 'density_change': density_change |
| | } |
| |
|
| |
|
| | def evaluate_recist(baseline_diameter=10.0, followup_diameter=12.5): |
| | """ |
| | 阶段 5: RECIST 1.1 评估 |
| | |
| | 根据病灶直径变化评估疗效 |
| | """ |
| | print_step(5, "RECIST 1.1 评估") |
| | |
| | print("\n 📋 RECIST 1.1 标准:") |
| | print(" - CR (完全缓解): 所有靶病灶消失") |
| | print(" - PR (部分缓解): 直径总和减少 ≥30%") |
| | print(" - SD (疾病稳定): 介于 PR 和 PD 之间") |
| | print(" - PD (疾病进展): 直径总和增加 ≥20%") |
| | |
| | |
| | change_percent = (followup_diameter - baseline_diameter) / baseline_diameter * 100 |
| | |
| | |
| | if followup_diameter == 0: |
| | recist_code = "CR" |
| | recist_text = "完全缓解 (Complete Response)" |
| | recist_color = "green" |
| | elif change_percent <= -30: |
| | recist_code = "PR" |
| | recist_text = "部分缓解 (Partial Response)" |
| | recist_color = "blue" |
| | elif change_percent >= 20: |
| | recist_code = "PD" |
| | recist_text = "疾病进展 (Progressive Disease)" |
| | recist_color = "red" |
| | else: |
| | recist_code = "SD" |
| | recist_text = "疾病稳定 (Stable Disease)" |
| | recist_color = "orange" |
| | |
| | print(f"\n ✅ RECIST 评估完成:") |
| | print(f" - 基线最长径: {baseline_diameter:.1f} mm") |
| | print(f" - 随访最长径: {followup_diameter:.1f} mm") |
| | print(f" - 变化百分比: {change_percent:+.1f}%") |
| | print(f" - 评估结果: {recist_code} - {recist_text}") |
| | |
| | return { |
| | 'baseline_diameter': baseline_diameter, |
| | 'followup_diameter': followup_diameter, |
| | 'change_percent': change_percent, |
| | 'recist_code': recist_code, |
| | 'recist_text': recist_text, |
| | 'recist_color': recist_color |
| | } |
| |
|
| |
|
| | def generate_report(data, changes, features, recist, output_dir): |
| | """ |
| | 阶段 6: LLM 智能报告生成 |
| | """ |
| | print_step(6, "LLM 智能报告生成") |
| | |
| | print("\n 🤖 报告生成配置:") |
| | print(" - LLM 后端: Ollama (本地)") |
| | print(" - 模型: llama3.1:8b / meditron:7b") |
| | print(" - 报告格式: ACR 标准") |
| | |
| | |
| | try: |
| | from app.services.report import ReportGenerator |
| | |
| | generator = ReportGenerator(llm_backend="ollama") |
| | |
| | |
| | report_data = { |
| | 'patient_id': 'WORKFLOW_DEMO', |
| | 'baseline_date': '2025-10-01', |
| | 'followup_date': datetime.now().strftime('%Y-%m-%d'), |
| | 'baseline_findings': { |
| | 'description': '右肺上叶后段见一结节灶,边界清晰', |
| | 'size_mm': recist['baseline_diameter'], |
| | 'density_hu': features['baseline']['mean_hu'] |
| | }, |
| | 'followup_findings': { |
| | 'description': '右肺上叶后段结节', |
| | 'size_mm': recist['followup_diameter'], |
| | 'density_hu': features['followup']['mean_hu'] |
| | }, |
| | 'changes': { |
| | 'size_change_percent': recist['change_percent'], |
| | 'density_change': features['density_change'] |
| | }, |
| | 'recist_evaluation': recist['recist_text'] |
| | } |
| | |
| | print("\n ⏳ 正在调用 LLM 生成报告...") |
| | report_content = generator.generate_longitudinal_report(**report_data) |
| | |
| | |
| | os.makedirs(output_dir, exist_ok=True) |
| | report_path = os.path.join(output_dir, 'ai_report.html') |
| | generator.save_report(report_content, report_path.replace('.html', ''), format='html') |
| | |
| | print(f" ✅ LLM 报告已生成: {report_path}") |
| | llm_success = True |
| | |
| | except Exception as e: |
| | print(f"\n ⚠️ LLM 报告生成失败: {e}") |
| | print(" 使用模板生成报告...") |
| | llm_success = False |
| | report_content = None |
| | |
| | |
| | template_report = generate_template_report(features, recist, changes) |
| | |
| | os.makedirs(output_dir, exist_ok=True) |
| | template_path = os.path.join(output_dir, 'template_report.html') |
| | with open(template_path, 'w', encoding='utf-8') as f: |
| | f.write(template_report) |
| | |
| | print(f" ✅ 模板报告已生成: {template_path}") |
| | |
| | return { |
| | 'llm_success': llm_success, |
| | 'report_content': report_content, |
| | 'template_path': template_path |
| | } |
| |
|
| |
|
| | def generate_template_report(features, recist, changes): |
| | """生成 HTML 模板报告""" |
| | |
| | html = f"""<!DOCTYPE html> |
| | <html lang="zh-CN"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <title>NeuroScan AI - 纵向对比诊断报告</title> |
| | <style> |
| | * {{ margin: 0; padding: 0; box-sizing: border-box; }} |
| | body {{ |
| | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| | background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); |
| | color: #e0e0e0; |
| | min-height: 100vh; |
| | padding: 40px; |
| | }} |
| | .container {{ max-width: 900px; margin: 0 auto; }} |
| | .header {{ |
| | text-align: center; |
| | padding: 30px; |
| | background: rgba(255,255,255,0.05); |
| | border-radius: 20px; |
| | margin-bottom: 30px; |
| | }} |
| | .header h1 {{ |
| | font-size: 2.5em; |
| | background: linear-gradient(90deg, #00d9ff, #00ff88); |
| | -webkit-background-clip: text; |
| | -webkit-text-fill-color: transparent; |
| | }} |
| | .section {{ |
| | background: rgba(255,255,255,0.05); |
| | border-radius: 15px; |
| | padding: 25px; |
| | margin-bottom: 20px; |
| | }} |
| | .section h2 {{ |
| | color: #00d9ff; |
| | margin-bottom: 15px; |
| | padding-bottom: 10px; |
| | border-bottom: 1px solid rgba(0,217,255,0.3); |
| | }} |
| | .info-grid {{ |
| | display: grid; |
| | grid-template-columns: repeat(2, 1fr); |
| | gap: 15px; |
| | }} |
| | .info-item {{ |
| | background: rgba(0,0,0,0.2); |
| | padding: 15px; |
| | border-radius: 10px; |
| | }} |
| | .info-label {{ color: #888; font-size: 0.9em; }} |
| | .info-value {{ font-size: 1.3em; color: #fff; margin-top: 5px; }} |
| | .recist-badge {{ |
| | display: inline-block; |
| | padding: 10px 25px; |
| | border-radius: 25px; |
| | font-weight: bold; |
| | font-size: 1.2em; |
| | background: {'#ff4444' if recist['recist_code'] == 'PD' else '#44ff44' if recist['recist_code'] == 'CR' else '#4488ff' if recist['recist_code'] == 'PR' else '#ffaa44'}; |
| | color: #000; |
| | }} |
| | .findings {{ line-height: 1.8; }} |
| | .highlight {{ color: #00ff88; font-weight: bold; }} |
| | .warning {{ color: #ff6b6b; }} |
| | </style> |
| | </head> |
| | <body> |
| | <div class="container"> |
| | <div class="header"> |
| | <h1>🏥 NeuroScan AI</h1> |
| | <p style="margin-top: 10px; color: #888;">纵向对比诊断报告</p> |
| | </div> |
| | |
| | <div class="section"> |
| | <h2>📋 患者信息</h2> |
| | <div class="info-grid"> |
| | <div class="info-item"> |
| | <div class="info-label">患者 ID</div> |
| | <div class="info-value">WORKFLOW_DEMO</div> |
| | </div> |
| | <div class="info-item"> |
| | <div class="info-label">检查类型</div> |
| | <div class="info-value">胸部 CT 纵向对比</div> |
| | </div> |
| | <div class="info-item"> |
| | <div class="info-label">基线日期</div> |
| | <div class="info-value">2025-10-01</div> |
| | </div> |
| | <div class="info-item"> |
| | <div class="info-label">随访日期</div> |
| | <div class="info-value">{datetime.now().strftime('%Y-%m-%d')}</div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div class="section"> |
| | <h2>📊 量化分析</h2> |
| | <div class="info-grid"> |
| | <div class="info-item"> |
| | <div class="info-label">基线最长径</div> |
| | <div class="info-value">{recist['baseline_diameter']:.1f} mm</div> |
| | </div> |
| | <div class="info-item"> |
| | <div class="info-label">随访最长径</div> |
| | <div class="info-value">{recist['followup_diameter']:.1f} mm</div> |
| | </div> |
| | <div class="info-item"> |
| | <div class="info-label">直径变化</div> |
| | <div class="info-value {'warning' if recist['change_percent'] > 0 else 'highlight'}">{recist['change_percent']:+.1f}%</div> |
| | </div> |
| | <div class="info-item"> |
| | <div class="info-label">密度变化</div> |
| | <div class="info-value">{features['density_change']:+.1f} HU</div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div class="section"> |
| | <h2>📋 RECIST 1.1 评估</h2> |
| | <div style="text-align: center; padding: 20px;"> |
| | <span class="recist-badge">{recist['recist_code']}</span> |
| | <p style="margin-top: 15px; font-size: 1.2em;">{recist['recist_text']}</p> |
| | </div> |
| | </div> |
| | |
| | <div class="section"> |
| | <h2>🔍 影像所见</h2> |
| | <div class="findings"> |
| | <p>右肺上叶后段见一结节灶,与 <span class="highlight">2025-10-01</span> 基线对比:</p> |
| | <ul style="margin: 15px 0 15px 20px;"> |
| | <li>病灶由 <span class="highlight">{recist['baseline_diameter']:.1f}mm</span> 增大至 <span class="warning">{recist['followup_diameter']:.1f}mm</span></li> |
| | <li>增大约 <span class="warning">{recist['change_percent']:+.1f}%</span></li> |
| | <li>密度变化 <span class="highlight">{features['density_change']:+.1f} HU</span></li> |
| | </ul> |
| | </div> |
| | </div> |
| | |
| | <div class="section"> |
| | <h2>💡 诊断建议</h2> |
| | <div class="findings"> |
| | <p>根据 RECIST 1.1 标准评估为 <span class="warning">{recist['recist_text']}</span>,建议:</p> |
| | <ol style="margin: 15px 0 15px 20px;"> |
| | <li>立即安排胸部专家会诊</li> |
| | <li>考虑 PET-CT 进一步评估代谢活性</li> |
| | <li>建议进行穿刺活检明确病灶性质</li> |
| | <li>3 个月后复查胸部 CT</li> |
| | </ol> |
| | </div> |
| | </div> |
| | |
| | <div style="text-align: center; padding: 20px; color: #666;"> |
| | <p>报告生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p> |
| | <p>本报告由 NeuroScan AI 自动生成,仅供参考,最终诊断请以医师意见为准</p> |
| | </div> |
| | </div> |
| | </body> |
| | </html>""" |
| | |
| | return html |
| |
|
| |
|
| | def create_visualization(data, registered, changes, recist, output_dir): |
| | """ |
| | 阶段 7: 可视化输出 |
| | """ |
| | print_step(7, "可视化输出") |
| | |
| | print("\n 🎨 生成可视化:") |
| | print(" - 多平面对比图") |
| | print(" - 差异热力图") |
| | print(" - RECIST 评估图") |
| | |
| | os.makedirs(output_dir, exist_ok=True) |
| | |
| | baseline = data['baseline'] |
| | followup = data['followup'] |
| | diff_map = changes['diff_map'] |
| | |
| | |
| | mid_slice = baseline.shape[2] // 2 |
| | |
| | |
| | fig = plt.figure(figsize=(16, 12)) |
| | fig.patch.set_facecolor('#1a1a2e') |
| | |
| | |
| | fig.suptitle('NeuroScan AI - 纵向分析工作流演示', |
| | fontsize=20, color='white', fontweight='bold', y=0.98) |
| | |
| | |
| | ax1 = fig.add_subplot(2, 3, 1) |
| | ax1.imshow(baseline[:, :, mid_slice].T, cmap='gray', origin='lower', |
| | vmin=-1000, vmax=400) |
| | ax1.set_title('Step 1: 基线扫描', color='white', fontsize=12) |
| | ax1.axis('off') |
| | |
| | |
| | ax2 = fig.add_subplot(2, 3, 2) |
| | ax2.imshow(followup[:, :, mid_slice].T, cmap='gray', origin='lower', |
| | vmin=-1000, vmax=400) |
| | ax2.set_title('Step 2: 随访扫描', color='white', fontsize=12) |
| | ax2.axis('off') |
| | |
| | |
| | ax3 = fig.add_subplot(2, 3, 3) |
| | ax3.imshow(registered[:, :, mid_slice].T, cmap='gray', origin='lower', |
| | vmin=-1000, vmax=400) |
| | ax3.set_title('Step 3: 配准后', color='white', fontsize=12) |
| | ax3.axis('off') |
| | |
| | |
| | ax4 = fig.add_subplot(2, 3, 4) |
| | im4 = ax4.imshow(diff_map[:, :, mid_slice].T, cmap='RdBu_r', origin='lower', |
| | vmin=-100, vmax=100) |
| | ax4.set_title('Step 4: 差异图', color='white', fontsize=12) |
| | ax4.axis('off') |
| | plt.colorbar(im4, ax=ax4, label='HU 变化', shrink=0.8) |
| | |
| | |
| | ax5 = fig.add_subplot(2, 3, 5) |
| | ax5.imshow(baseline[:, :, mid_slice].T, cmap='gray', origin='lower', |
| | vmin=-1000, vmax=400) |
| | mask = np.abs(diff_map[:, :, mid_slice].T) > 30 |
| | overlay = np.ma.masked_where(~mask, np.abs(diff_map[:, :, mid_slice].T)) |
| | ax5.imshow(overlay, cmap='hot', origin='lower', alpha=0.7, vmin=0, vmax=100) |
| | ax5.set_title('Step 5: 变化热力图', color='white', fontsize=12) |
| | ax5.axis('off') |
| | |
| | |
| | ax6 = fig.add_subplot(2, 3, 6) |
| | ax6.set_facecolor('#1a1a2e') |
| | |
| | |
| | colors = {'CR': '#00ff00', 'PR': '#00aaff', 'SD': '#ffaa00', 'PD': '#ff4444'} |
| | color = colors.get(recist['recist_code'], '#ffffff') |
| | |
| | ax6.text(0.5, 0.7, 'RECIST 1.1 评估', ha='center', va='center', |
| | fontsize=16, color='white', transform=ax6.transAxes) |
| | ax6.text(0.5, 0.5, recist['recist_code'], ha='center', va='center', |
| | fontsize=48, color=color, fontweight='bold', transform=ax6.transAxes) |
| | ax6.text(0.5, 0.3, recist['recist_text'], ha='center', va='center', |
| | fontsize=12, color='white', transform=ax6.transAxes) |
| | ax6.text(0.5, 0.15, f"变化: {recist['change_percent']:+.1f}%", ha='center', va='center', |
| | fontsize=14, color=color, transform=ax6.transAxes) |
| | ax6.axis('off') |
| | ax6.set_title('Step 6: 评估结果', color='white', fontsize=12) |
| | |
| | plt.tight_layout(rect=[0, 0.02, 1, 0.95]) |
| | |
| | |
| | viz_path = os.path.join(output_dir, 'workflow_visualization.png') |
| | plt.savefig(viz_path, dpi=150, facecolor='#1a1a2e', edgecolor='none', |
| | bbox_inches='tight') |
| | plt.close() |
| | |
| | print(f"\n ✅ 可视化已保存: {viz_path}") |
| | |
| | return viz_path |
| |
|
| |
|
| | def save_results(data, changes, features, recist, report, output_dir): |
| | """保存所有结果""" |
| | print_step(8, "保存结果") |
| | |
| | os.makedirs(output_dir, exist_ok=True) |
| | |
| | |
| | results = { |
| | 'timestamp': datetime.now().isoformat(), |
| | 'patient_id': 'WORKFLOW_DEMO', |
| | 'baseline_date': '2025-10-01', |
| | 'followup_date': datetime.now().strftime('%Y-%m-%d'), |
| | 'changes': { |
| | 'changed_volume_mm3': float(changes['changed_volume_mm3']), |
| | 'mean_change_hu': float(changes['mean_change']), |
| | 'max_increase_hu': float(changes['max_increase']), |
| | 'max_decrease_hu': float(changes['max_decrease']) |
| | }, |
| | 'features': { |
| | 'baseline': features['baseline'], |
| | 'followup': features['followup'], |
| | 'density_change': float(features['density_change']) |
| | }, |
| | 'recist': { |
| | 'baseline_diameter_mm': recist['baseline_diameter'], |
| | 'followup_diameter_mm': recist['followup_diameter'], |
| | 'change_percent': recist['change_percent'], |
| | 'evaluation': recist['recist_code'], |
| | 'description': recist['recist_text'] |
| | } |
| | } |
| | |
| | json_path = os.path.join(output_dir, 'analysis_results.json') |
| | with open(json_path, 'w', encoding='utf-8') as f: |
| | json.dump(results, f, indent=2, ensure_ascii=False) |
| | |
| | print(f"\n ✅ 结果已保存:") |
| | print(f" - JSON 数据: {json_path}") |
| | print(f" - HTML 报告: {report['template_path']}") |
| | |
| | return json_path |
| |
|
| |
|
| | def main(): |
| | """主函数""" |
| | print_header("🏥 NeuroScan AI - 完整工作流演示") |
| | |
| | print("\n" + "─" * 70) |
| | print(" 本演示展示从输入到输出的完整流程:") |
| | print(" 输入 → 预处理 → 配准 → 变化检测 → 特征提取 → RECIST → 报告") |
| | print("─" * 70) |
| | |
| | |
| | data_dir = os.path.join(PROJECT_ROOT, 'data', 'processed') |
| | output_dir = os.path.join(PROJECT_ROOT, 'output', 'workflow_demo') |
| | |
| | |
| | baseline_path = None |
| | followup_path = None |
| | |
| | |
| | for folder in ['real_lung_001', 'real_lung_002', 'real_lung_003']: |
| | folder_path = os.path.join(data_dir, folder) |
| | if os.path.exists(folder_path): |
| | b = os.path.join(folder_path, 'baseline.nii.gz') |
| | f = os.path.join(folder_path, 'followup.nii.gz') |
| | if os.path.exists(b) and os.path.exists(f): |
| | baseline_path = b |
| | followup_path = f |
| | break |
| | |
| | if not baseline_path: |
| | print("\n ❌ 未找到测试数据!") |
| | print(" 请先运行: python scripts/download_real_data.py") |
| | return |
| | |
| | |
| | try: |
| | |
| | data = load_data(baseline_path, followup_path) |
| | |
| | |
| | registered = perform_registration( |
| | data['baseline'], |
| | data['followup'], |
| | data['spacing'] |
| | ) |
| | |
| | |
| | changes = detect_changes( |
| | data['baseline'], |
| | registered, |
| | data['spacing'] |
| | ) |
| | |
| | |
| | features = extract_features( |
| | data['baseline'], |
| | registered, |
| | changes['diff_map'], |
| | data['spacing'] |
| | ) |
| | |
| | |
| | recist = evaluate_recist() |
| | |
| | |
| | report = generate_report(data, changes, features, recist, output_dir) |
| | |
| | |
| | viz_path = create_visualization( |
| | data, registered, changes, recist, output_dir |
| | ) |
| | |
| | |
| | json_path = save_results(data, changes, features, recist, report, output_dir) |
| | |
| | |
| | print_header("✅ 工作流演示完成!") |
| | |
| | print(f""" |
| | 📁 输出目录: {output_dir} |
| | |
| | 📄 生成的文件: |
| | 1. analysis_results.json - 量化分析数据 |
| | 2. template_report.html - 诊断报告 |
| | 3. workflow_visualization.png - 可视化图 |
| | {'4. ai_report.html - LLM 智能报告' if report['llm_success'] else ''} |
| | |
| | 🌐 查看报告: |
| | cd {output_dir} && python -m http.server 8899 |
| | 然后访问 http://localhost:8899/template_report.html |
| | """) |
| | |
| | except Exception as e: |
| | print(f"\n ❌ 工作流执行失败: {e}") |
| | import traceback |
| | traceback.print_exc() |
| |
|
| |
|
| | if __name__ == "__main__": |
| | main() |
| |
|
| |
|