| |
| """ |
| 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() |
|
|
|
|