#!/usr/bin/env python3 """ 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}") # 加载 NIfTI 文件 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) # 设置阈值 (30 HU 为显著变化) threshold = 30 significant_mask = abs_diff > threshold # 计算统计量 voxel_volume = np.prod(spacing) # mm³ 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(" - 形态学特征") # 找到变化最显著的区域作为 ROI abs_diff = np.abs(diff_map) threshold = np.percentile(abs_diff, 99) # 取变化最大的 1% 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 标准") # 尝试使用 LLM 生成报告 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"""
纵向对比诊断报告
{recist['recist_text']}
右肺上叶后段见一结节灶,与 2025-10-01 基线对比:
根据 RECIST 1.1 标准评估为 {recist['recist_text']},建议:
报告生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
本报告由 NeuroScan AI 自动生成,仅供参考,最终诊断请以医师意见为准