四角DICOM信息显示功能.md 38 KB

四角DICOM信息显示功能实现文档

📋 功能概述

实现在 Cornerstone3D 医学图像查看器中显示 DICOM 图像四角标签信息的功能。该功能支持:

  • 可配置的 DICOM 标签选择
  • 可配置的显示位置(左上、右上、左下、右下)
  • 可配置的日期/数字格式化
  • 本地和远程配置适配器
  • 实时更新(图像切换、窗宽窗位变化时)

🎯 设计原则

  1. 适配器模式: 使用适配器模式支持本地和远程配置源
  2. 单一职责: 每个组件职责明确,易于维护和测试
  3. 可扩展性: 易于添加新的格式化器和配置源
  4. 一致性: 与现有 tableColumns 配置系统保持一致

👥 1. 参与者列表

1.1 组件层级

🎨 UI 组件

  • MorePanel (src/pages/view/components/MorePanel.tsx)

    • 职责: 提供"tag信息"按钮,触发功能开关
    • 交互: 调用 Redux action 切换 overlay 状态
  • StackViewer (src/pages/view/components/viewers/stack.image.viewer.tsx)

    • 职责: 集成 DicomOverlayTool,响应状态变化
    • 交互: 监听 Redux state,启用/禁用 overlay

🛠️ 核心工具类

  • DicomOverlayTool (src/components/overlay/DicomOverlayTool.ts)

    • 继承: AnnotationTool (Cornerstone3D)
    • 职责: 管理四角信息的生命周期(创建、更新、渲染)
    • 监听: IMAGE_RENDERED, STACK_NEW_IMAGE 事件
  • OverlayRenderer (src/components/overlay/renderers/OverlayRenderer.ts)

    • 职责: 实际的 Canvas 渲染逻辑
    • 功能: 绘制文本、计算位置、处理样式

⚙️ 配置系统

  • IOverlayConfigProvider (接口)

    • 定义: 配置提供者契约
    • 方法: getOverlayConfig(), isAvailable()
  • LocalOverlayConfigAdapter

    • 实现: 本地硬编码配置
    • 默认配置: 患者信息、检查信息、图像参数等
  • RemoteOverlayConfigAdapter

    • 实现: 从服务器获取配置
    • API: /api/config/overlay
  • OverlayConfigManager

    • 职责: 统一管理配置访问,处理本地/远程切换
    • 策略: 优先远程,失败降级到本地

🔧 工具类

  • DicomTagFormatter (src/utils/dicomTagFormatter.ts)

    • 职责: 格式化 DICOM 标签值
    • 支持: 日期、时间、数字、枚举等格式
  • OverlayPositionCalculator (src/utils/overlayPositionCalculator.ts)

    • 职责: 计算四角显示位置
    • 考虑: padding、字体大小、多行文本

📊 状态管理

  • dicomOverlaySlice (src/states/view/dicomOverlaySlice.ts)
    • State: enabled, configSource, config
    • Actions: toggleOverlay, setConfigSource, updateConfig
    • Selectors: selectOverlayEnabled, selectOverlayConfig

1.2 数据类型

配置类型

// 单个标签配置
interface TagConfig {
  tag: string;              // DICOM tag (如 "0010,0010")
  label?: string;           // 显示标签 (如 "患者姓名")
  format?: TagFormat;       // 格式化选项
  defaultValue?: string;    // 默认值
}

// 位置配置
type CornerPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';

// 角落配置
interface CornerConfig {
  position: CornerPosition;
  tags: TagConfig[];
  visible: boolean;
}

// 完整配置
interface OverlayConfig {
  enabled: boolean;
  corners: CornerConfig[];
  style: OverlayStyle;      // 字体、颜色等
}

✅ 2. TodoList

2.1 配置系统 (Config)

  • [ ] 创建类型定义 src/config/overlayConfig/types/overlayConfig.ts

    • 定义 TagConfig 接口
    • 定义 CornerConfig 接口
    • 定义 OverlayConfig 接口
    • 定义 TagFormat 枚举
    • 定义 OverlayStyle 接口
  • [ ] 创建配置接口 src/config/overlayConfig/ports/IOverlayConfigProvider.ts

    • 定义 getOverlayConfig() 方法签名
    • 定义 isAvailable() 方法签名
  • [ ] 实现本地适配器 src/config/overlayConfig/adapters/LocalOverlayConfigAdapter.ts

    • 实现 IOverlayConfigProvider 接口
    • 提供默认配置(患者信息、检查参数等)
    • 硬编码四个角的标签配置
  • [ ] 实现远程适配器 src/config/overlayConfig/adapters/RemoteOverlayConfigAdapter.ts

    • 实现 IOverlayConfigProvider 接口
    • 实现 API 调用逻辑
    • 实现错误处理和重试
  • [ ] 创建配置管理器 src/config/overlayConfig/OverlayConfigManager.ts

    • 实现配置源切换逻辑
    • 实现配置缓存
    • 实现降级策略(远程失败→本地)

2.2 工具类 (Utils)

  • [ ] 创建格式化工具 src/utils/dicomTagFormatter.ts

    • 实现日期格式化 (YYYYMMDD → YYYY-MM-DD)
    • 实现时间格式化 (HHMMSS → HH:MM:SS)
    • 实现数字格式化(窗宽窗位、像素间距等)
    • 实现枚举格式化(性别、体位等)
    • 实现自定义格式化函数
  • [ ] 创建位置计算工具 src/utils/overlayPositionCalculator.ts

    • 实现四角位置计算
    • 考虑 padding 和边距
    • 处理多行文本的行高
    • 处理文本溢出

2.3 核心组件 (Components)

  • [ ] 创建渲染器 src/components/overlay/renderers/OverlayRenderer.ts

    • 实现 Canvas 文本渲染
    • 实现背景绘制(半透明背景)
    • 实现样式应用(颜色、字体)
    • 优化渲染性能
  • [ ] 创建 DicomOverlayTool src/components/overlay/DicomOverlayTool.ts

    • 继承 AnnotationTool 基类
    • 实现工具注册逻辑
    • 监听 IMAGE_RENDERED 事件
    • 监听 STACK_NEW_IMAGE 事件
    • 实现 metadata 提取逻辑
    • 集成 OverlayRenderer
    • 实现配置更新响应
  • [ ] 创建文档 src/components/overlay/DicomOverlayTool.README.md

    • 编写功能说明
    • 编写使用示例
    • 编写配置说明

2.4 状态管理 (States)

  • [ ] 创建 Redux Slice src/states/view/dicomOverlaySlice.ts

    • 定义 State 接口
    • 实现 toggleOverlay action
    • 实现 setConfigSource action
    • 实现 updateConfig action
    • 实现 selectors
  • [ ] 集成到 Store src/states/store.ts

    • 导入 dicomOverlaySlice
    • 添加到 rootReducer

2.5 UI 集成 (Pages)

  • [ ] 修改 MorePanel src/pages/view/components/MorePanel.tsx

    • 实现 handleTagInfo 函数
    • 调用 Redux action 切换状态
    • 添加状态指示(显示当前开关状态)
  • [ ] 修改 StackViewer src/pages/view/components/viewers/stack.image.viewer.tsx

    • registerTools 中注册 DicomOverlayTool
    • 添加 useEffect 监听 overlay 状态
    • 实现启用/禁用逻辑
    • 传递配置到工具

2.6 工具注册 (Setup)

  • 修改工具设置 src/utils/cornerstoneToolsSetup.ts
    • 导入 DicomOverlayTool
    • 添加到全局工具注册

2.7 测试与文档

  • [ ] 编写完整文档 docs/实现/四角DICOM信息显示功能.md

    • 架构设计说明
    • Mermaid 图表
    • 测试方案
    • 潜在问题分析
  • [ ] 制定测试方案

    • 功能测试场景
    • 边界情况测试
    • 性能测试

🏊 3. 交互流程(泳道图)

sequenceDiagram
    actor User as 用户
    participant MP as MorePanel
    participant Redux as Redux Store
    participant SV as StackViewer
    participant Tool as DicomOverlayTool
    participant Config as ConfigManager
    participant Renderer as OverlayRenderer
    participant CS as Cornerstone3D

    User->>MP: 点击"tag信息"按钮
    MP->>Redux: dispatch(toggleOverlay())
    Redux->>Redux: 更新 enabled 状态
    Redux-->>SV: 状态变化通知
    
    SV->>SV: useEffect 检测状态变化
    
    alt overlay enabled
        SV->>Tool: 激活工具
        Tool->>Config: 获取配置
        Config-->>Tool: 返回配置
        Tool->>CS: 监听 IMAGE_RENDERED 事件
        
        CS->>Tool: IMAGE_RENDERED 事件
        Tool->>Tool: 提取 DICOM metadata
        Tool->>Tool: 格式化标签值
        Tool->>Renderer: 渲染四角信息
        Renderer->>CS: 绘制到 Canvas
    else overlay disabled
        SV->>Tool: 停用工具
        Tool->>CS: 移除监听
        Tool->>Renderer: 清除渲染
    end
    
    User->>User: 切换图像 (Stack Scroll)
    CS->>Tool: STACK_NEW_IMAGE 事件
    Tool->>Tool: 提取新图像 metadata
    Tool->>Renderer: 更新渲染
    Renderer->>CS: 重新绘制

📊 4. 数据流

flowchart TD
    A[用户点击按钮] --> B{Redux State}
    B -->|enabled=true| C[StackViewer 检测]
    B -->|enabled=false| D[禁用工具]
    
    C --> E[激活 DicomOverlayTool]
    E --> F[ConfigManager]
    F --> G{配置源}
    
    G -->|Remote| H[RemoteAdapter]
    G -->|Local| I[LocalAdapter]
    
    H -->|成功| J[配置数据]
    H -->|失败| I
    I --> J
    
    J --> K[DicomOverlayTool]
    K --> L[监听事件]
    
    L --> M{事件类型}
    M -->|IMAGE_RENDERED| N[提取 Metadata]
    M -->|STACK_NEW_IMAGE| N
    M -->|VOI_MODIFIED| N
    
    N --> O[DicomTagFormatter]
    O --> P[格式化值]
    P --> Q[OverlayPositionCalculator]
    Q --> R[计算位置]
    R --> S[OverlayRenderer]
    S --> T[Canvas 渲染]
    
    D --> U[清除显示]

🗂️ 5. 数据结构

5.1 配置数据结构

/**
 * DICOM 标签格式化选项
 */
export enum TagFormat {
  /** 原始值 */
  RAW = 'raw',
  /** 日期格式 YYYY-MM-DD */
  DATE = 'date',
  /** 时间格式 HH:MM:SS */
  TIME = 'time',
  /** 日期时间格式 */
  DATETIME = 'datetime',
  /** 数字,保留2位小数 */
  NUMBER_2 = 'number_2',
  /** 数字,保留1位小数 */
  NUMBER_1 = 'number_1',
  /** 整数 */
  INTEGER = 'integer',
  /** 性别枚举 (M/F/O) */
  SEX = 'sex',
  /** 自定义函数 */
  CUSTOM = 'custom',
}

/**
 * 单个 DICOM 标签配置
 */
export interface TagConfig {
  /** DICOM 标签 (如 "0010,0010" 或 "PatientName") */
  tag: string;
  /** 显示标签文本 */
  label?: string;
  /** 格式化选项 */
  format?: TagFormat;
  /** 自定义格式化函数 */
  customFormatter?: (value: any) => string;
  /** 默认值(标签不存在时) */
  defaultValue?: string;
  /** 是否显示标签名 */
  showLabel?: boolean;
}

/**
 * 角落位置
 */
export type CornerPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';

/**
 * 单个角落的配置
 */
export interface CornerConfig {
  /** 位置 */
  position: CornerPosition;
  /** 该角落显示的标签列表 */
  tags: TagConfig[];
  /** 是否可见 */
  visible: boolean;
  /** 对齐方式 */
  align?: 'left' | 'right';
}

/**
 * 样式配置
 */
export interface OverlayStyle {
  /** 字体系列 */
  fontFamily?: string;
  /** 字体大小 */
  fontSize?: number;
  /** 字体颜色 */
  color?: string;
  /** 背景颜色(支持透明度) */
  backgroundColor?: string;
  /** 内边距 */
  padding?: number;
  /** 行高 */
  lineHeight?: number;
  /** 是否显示背景 */
  showBackground?: boolean;
}

/**
 * 完整的 Overlay 配置
 */
export interface OverlayConfig {
  /** 全局开关 */
  enabled: boolean;
  /** 四个角的配置 */
  corners: CornerConfig[];
  /** 样式配置 */
  style: OverlayStyle;
  /** 配置版本 */
  version?: string;
  /** 更新时间 */
  updatedAt?: string;
}

/**
 * 配置响应
 */
export interface OverlayConfigResponse {
  data: OverlayConfig;
  success: boolean;
  message?: string;
}

5.2 Redux State 结构

/**
 * Overlay Redux State
 */
export interface DicomOverlayState {
  /** 是否启用 */
  enabled: boolean;
  /** 配置源 */
  configSource: 'local' | 'remote';
  /** 当前配置 */
  config: OverlayConfig | null;
  /** 加载状态 */
  loading: boolean;
  /** 错误信息 */
  error: string | null;
}

5.3 渲染数据结构

/**
 * 渲染项
 */
export interface OverlayRenderItem {
  /** 显示文本 */
  text: string;
  /** 位置 */
  position: { x: number; y: number };
  /** 角落 */
  corner: CornerPosition;
  /** 样式 */
  style: OverlayStyle;
}

🔄 6. 执行流程

6.1 功能起点

用户操作: 用户点击 MorePanel 中的 "tag信息" 按钮

6.2 完整执行流程

flowchart TD
    Start([用户点击 tag信息 按钮]) --> A[MorePanel.handleTagInfo]
    A --> B[dispatch toggleOverlay action]
    B --> C[Redux 更新 enabled 状态]
    
    C --> D{enabled?}
    
    D -->|true 启用| E[StackViewer useEffect 触发]
    E --> F[获取 toolGroup]
    F --> G[检查 DicomOverlayTool 是否已注册]
    
    G -->|未注册| H[注册工具到 toolGroup]
    G -->|已注册| I[激活工具]
    H --> I
    
    I --> J[ConfigManager.getConfig]
    J --> K{配置源}
    
    K -->|Remote| L[RemoteAdapter.fetch]
    L -->|成功| M[返回配置]
    L -->|失败| N[LocalAdapter.getConfig]
    
    K -->|Local| N
    N --> M
    
    M --> O[DicomOverlayTool.setConfig]
    O --> P[添加事件监听器]
    P --> P1[IMAGE_RENDERED]
    P --> P2[STACK_NEW_IMAGE]
    P --> P3[VOI_MODIFIED]
    
    P1 --> Q[onImageRendered]
    P2 --> Q
    P3 --> Q
    
    Q --> R[获取当前 imageId]
    R --> S[从 metaDataManager 提取 metadata]
    S --> T[遍历配置的 tags]
    
    T --> U[DicomTagFormatter.format]
    U --> V[OverlayPositionCalculator.calculate]
    V --> W[准备渲染数据]
    
    W --> X[OverlayRenderer.render]
    X --> Y[Canvas 绘制文本]
    Y --> End1([四角信息显示])
    
    D -->|false 禁用| Z[StackViewer useEffect 触发]
    Z --> AA[停用工具]
    AA --> AB[移除事件监听器]
    AB --> AC[清除 Canvas]
    AC --> End2([四角信息隐藏])

6.3 关键步骤说明

  1. 初始化阶段

    • 工具注册: 在 cornerstoneToolsSetup.ts 中全局注册 DicomOverlayTool
    • Viewport 初始化: 在 StackViewerregisterTools 函数中添加工具到 toolGroup
  2. 激活阶段

    • 用户点击按钮 → Redux state 更新
    • StackViewer 通过 useSelector 监听状态
    • useEffect 响应状态变化,调用 toolGroup.setToolActive/Passive
  3. 配置加载

    • 优先尝试远程配置
    • 失败则降级到本地配置
    • 配置缓存避免重复请求
  4. 事件监听

    • IMAGE_RENDERED: 图像渲染完成时更新
    • STACK_NEW_IMAGE: 切换图像时更新
    • VOI_MODIFIED: 窗宽窗位变化时更新(如需显示WW/WL)
  5. 数据提取与格式化

    • cornerstoneDICOMImageLoader.wadors.metaDataManager 获取 metadata
    • 使用配置中的 tag 标识符提取值
    • 应用格式化规则(日期、数字等)
  6. 渲染

    • 计算四个角的文本位置
    • 考虑 padding、多行文本、字体大小
    • 使用 Canvas API 绘制文本和背景

🧪 7. 测试操作步骤

7.1 功能测试

测试场景 1: 基本开关功能

前置条件:

  • 系统已启动
  • 已加载 DICOM 图像

测试步骤:

  1. 打开图像查看界面
  2. 点击右侧工具栏切换到 "更多功能" 面板
  3. 找到 "信息查看" 组,点击 "tag信息" 按钮
  4. 预期结果: 图像四角显示 DICOM 信息
  5. 再次点击 "tag信息" 按钮
  6. 预期结果: 四角信息消失

测试场景 2: 图像切换更新

前置条件:

  • 四角信息已启用
  • 当前为多帧图像或序列

测试步骤:

  1. 使用鼠标滚轮或键盘上下键切换图像
  2. 预期结果: 四角信息实时更新,显示当前图像的 metadata

测试场景 3: 窗宽窗位变化

前置条件:

  • 四角信息已启用
  • 配置中包含窗宽窗位标签

测试步骤:

  1. 使用鼠标左键拖动调整窗宽窗位
  2. 预期结果: 右上角的窗宽窗位数值实时更新

测试场景 4: 多 Viewport 显示

前置条件:

  • 四角信息已启用
  • 切换到 2x2 布局

测试步骤:

  1. 切换到 2x2 分格布局
  2. 加载不同的图像到各个 viewport
  3. 预期结果: 每个 viewport 都正确显示各自图像的四角信息

测试场景 5: 配置切换

前置条件:

  • 实现了远程配置

测试步骤:

  1. 启用四角信息(使用本地配置)
  2. 切换到远程配置源
  3. 预期结果: 四角信息根据远程配置重新渲染

7.2 边界测试

测试场景 6: 缺失 Metadata

测试步骤:

  1. 加载一个 metadata 不完整的 DICOM 文件
  2. 启用四角信息
  3. 预期结果: 缺失的标签显示默认值或空白,不应崩溃

测试场景 7: 极长文本

测试步骤:

  1. 配置显示超长的 DICOM 标签(如 StudyDescription)
  2. 预期结果: 文本应该被截断或换行,不应溢出 viewport

测试场景 8: 特殊字符

测试步骤:

  1. 加载包含特殊字符(中文、emoji等)的 DICOM 文件
  2. 预期结果: 特殊字符正确显示,不乱码

7.3 性能测试

测试场景 9: 快速切换图像

测试步骤:

  1. 加载大序列(100+ 帧)
  2. 快速滚动鼠标滚轮切换图像
  3. 预期结果: 四角信息更新流畅,无明显延迟,不影响图像切换性能

7.4 集成测试

测试场景 10: 与其他功能协同

测试步骤:

  1. 启用四角信息
  2. 同时使用测量工具(长度、角度等)
  3. 旋转、翻转、缩放图像
  4. 预期结果: 四角信息不受影响,始终正确显示

📐 8. Mermaid 图表

8.1 系统架构图

graph TB
    subgraph UI["UI 层"]
        MP[MorePanel<br/>tag信息按钮]
        SV[StackViewer<br/>图像显示]
    end
    
    subgraph State["状态管理层"]
        Redux[Redux Store<br/>dicomOverlaySlice]
    end
    
    subgraph Core["核心层"]
        Tool[DicomOverlayTool<br/>核心工具]
        Renderer[OverlayRenderer<br/>渲染器]
    end
    
    subgraph Config["配置层"]
        Manager[ConfigManager<br/>配置管理器]
        LocalAdapter[LocalAdapter<br/>本地配置]
        RemoteAdapter[RemoteAdapter<br/>远程配置]
    end
    
    subgraph Utils["工具层"]
        Formatter[DicomTagFormatter<br/>格式化]
        Calculator[PositionCalculator<br/>位置计算]
    end
    
    subgraph External["外部依赖"]
        CS[Cornerstone3D<br/>渲染引擎]
        MetaData[MetaDataManager<br/>元数据]
    end
    
    MP -->|dispatch action| Redux
    Redux -->|state change| SV
    SV -->|activate/deactivate| Tool
    
    Tool -->|get config| Manager
    Manager -->|try remote| RemoteAdapter
    Manager -->|fallback| LocalAdapter
    
    Tool -->|extract metadata| MetaData
    Tool -->|format values| Formatter
    Tool -->|calculate position| Calculator
    Tool -->|render| Renderer
    
    Renderer -->|draw| CS
    Tool -->|listen events| CS
    
    style MP fill:#e1f5ff
    style Redux fill:#fff4e1
    style Tool fill:#ffe1e1
    style Manager fill:#e1ffe1

8.2 类图

classDiagram
    class IOverlayConfigProvider {
        <<interface>>
        +getOverlayConfig() Promise~OverlayConfig~
        +isAvailable() Promise~boolean~
    }
    
    class LocalOverlayConfigAdapter {
        -defaultConfig: OverlayConfig
        +getOverlayConfig() Promise~OverlayConfig~
        +isAvailable() Promise~boolean~
    }
    
    class RemoteOverlayConfigAdapter {
        -apiUrl: string
        -cache: OverlayConfig
        +getOverlayConfig() Promise~OverlayConfig~
        +isAvailable() Promise~boolean~
        -fetchFromServer() Promise~OverlayConfig~
    }
    
    class OverlayConfigManager {
        -providers: IOverlayConfigProvider[]
        -currentConfig: OverlayConfig
        +getConfig() Promise~OverlayConfig~
        +setConfigSource(source: string) void
        -selectProvider() IOverlayConfigProvider
    }
    
    class DicomOverlayTool {
        -config: OverlayConfig
        -renderer: OverlayRenderer
        -eventHandlers: Map
        +onSetToolEnabled() void
        +onSetToolDisabled() void
        +renderAnnotation() void
        -onImageRendered() void
        -extractMetadata() object
    }
    
    class OverlayRenderer {
        -canvas: HTMLCanvasElement
        -ctx: CanvasRenderingContext2D
        +render(items: OverlayRenderItem[]) void
        +clear() void
        -drawText(text: string, position: Point) void
        -drawBackground(rect: Rect) void
    }
    
    class DicomTagFormatter {
        +formatDate(value: string) string
        +formatTime(value: string) string
        +formatNumber(value: number, decimals: number) string
        +formatSex(value: string) string
        +format(value: any, format: TagFormat) string
    }
    
    class OverlayPositionCalculator {
        +calculate(corner: CornerPosition, viewport: Viewport) Point
        -getCornerOffset(corner: CornerPosition) Point
        -calculateMultilineHeight(lines: number) number
    }
    
    IOverlayConfigProvider <|.. LocalOverlayConfigAdapter
    IOverlayConfigProvider <|.. RemoteOverlayConfigAdapter
    OverlayConfigManager o-- IOverlayConfigProvider
    DicomOverlayTool --> OverlayConfigManager
    DicomOverlayTool --> OverlayRenderer
    DicomOverlayTool --> DicomTagFormatter
    DicomOverlayTool --> OverlayPositionCalculator

🐛 9. 潜在问题分析

9.1 性能问题

问题 1: 频繁渲染导致性能下降

场景:

  • 用户快速切换图像时,每次都触发 IMAGE_RENDERED 事件
  • 如果渲染逻辑复杂,可能导致卡顿

解决方案:

// 使用防抖优化
const debouncedRender = debounce(() => {
  this.renderOverlay();
}, 16); // ~60fps

eventTarget.addEventListener(EVENTS.IMAGE_RENDERED, debouncedRender);

优化措施:

  • 实现渲染缓存,避免重复计算相同的 metadata
  • 只在数据真正变化时才重新渲染
  • 使用 requestAnimationFrame 优化渲染时机

问题 2: Metadata 提取耗时

场景:

  • 某些 DICOM 文件 metadata 非常大
  • 提取和格式化可能耗时

解决方案:

// 使用 Web Worker 处理
const worker = new Worker('metadata-processor.worker.js');
worker.postMessage({ imageId, tags });
worker.onmessage = (e) => {
  this.renderOverlay(e.data);
};

9.2 兼容性问题

问题 3: Canvas 文本渲染中文乱码

场景:

  • 某些浏览器或字体不支持中文字符
  • 导致显示为方框或乱码

解决方案:

// 指定支持中文的字体
const style = {
  fontFamily: 'Microsoft YaHei, SimHei, Arial, sans-serif',
  // ...
};

// 检测字体是否加载
document.fonts.ready.then(() => {
  this.renderOverlay();
});

问题 4: 不同浏览器的 Canvas API 差异

场景:

  • 不同浏览器对 Canvas measureText 的实现有差异
  • 可能导致布局偏移

解决方案:

// 添加浏览器检测和兼容代码
const measureText = (text: string, font: string) => {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d')!;
  ctx.font = font;
  const metrics = ctx.measureText(text);
  
  // 兼容不支持 fontBoundingBox 的浏览器
  const height = metrics.fontBoundingBoxAscent 
    ? metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent
    : parseInt(font) * 1.2; // fallback
  
  return { width: metrics.width, height };
};

9.3 数据问题

问题 5: Metadata 缺失或格式不正确

场景:

  • DICOM 文件不完整或损坏
  • 某些必需的 tag 缺失

解决方案:

const extractTagValue = (metadata: any, tag: string, defaultValue: string = '') => {
  try {
    // 支持多种 tag 格式: "0010,0010" 或 "PatientName"
    const value = metadata[tag] || metadata[tag.replace(',', '')];
    
    if (!value) {
      console.warn(`Tag ${tag} not found in metadata`);
      return defaultValue;
    }
    
    // 处理不同的值格式
    if (typeof value === 'object' && value.Value) {
      return value.Value[0] || defaultValue;
    }
    
    return String(value) || defaultValue;
  } catch (error) {
    console.error(`Error extracting tag ${tag}:`, error);
    return defaultValue;
  }
};

问题 6: 日期/时间格式不统一

场景:

  • DICOM 标准的日期格式是 YYYYMMDD
  • 但实际文件可能包含其他格式

解决方案:

const formatDate = (value: string): string => {
  // 处理各种可能的日期格式
  const formats = [
    /^(\d{4})(\d{2})(\d{2})$/,           // YYYYMMDD
    /^(\d{4})-(\d{2})-(\d{2})$/,         // YYYY-MM-DD
    /^(\d{4})\/(\d{2})\/(\d{2})$/,       // YYYY/MM/DD
  ];
  
  for (const format of formats) {
    const match = value.match(format);
    if (match) {
      return `${match[1]}-${match[2]}-${match[3]}`;
    }
  }
  
  return value; // 无法识别,返回原始值
};

9.4 用户体验问题

问题 7: 信息过多导致遮挡图像

场景:

  • 配置了太多 tag,四角信息占据大量空间
  • 影响用户查看图像

解决方案:

// 添加透明度控制
const style = {
  backgroundColor: 'rgba(0, 0, 0, 0.5)', // 半透明背景
  // 或者在鼠标移入时隐藏
};

// 鼠标悬停时隐藏
canvas.addEventListener('mouseenter', () => {
  this.setOpacity(0.2);
});
canvas.addEventListener('mouseleave', () => {
  this.setOpacity(1.0);
});

问题 8: 文本溢出 viewport

场景:

  • 某些 tag 值非常长
  • 可能超出 viewport 边界

解决方案:

const truncateText = (
  text: string, 
  maxWidth: number, 
  ctx: CanvasRenderingContext2D
): string => {
  const metrics = ctx.measureText(text);
  
  if (metrics.width <= maxWidth) {
    return text;
  }
  
  // 逐字符减少直到符合宽度
  let truncated = text;
  while (ctx.measureText(truncated + '...').width > maxWidth && truncated.length > 0) {
    truncated = truncated.slice(0, -1);
  }
  
  return truncated + '...';
};

9.5 配置问题

问题 9: 远程配置加载失败

场景:

  • 网络问题导致远程配置无法加载
  • 服务器返回错误配置

解决方案:

class OverlayConfigManager {
  async getConfig(): Promise<OverlayConfig> {
    try {
      // 尝试远程配置
      const remoteConfig = await this.remoteAdapter.getOverlayConfig();
      
      // 验证配置有效性
      if (this.validateConfig(remoteConfig)) {
        return remoteConfig;
      }
      
      console.warn('Remote config invalid, falling back to local');
    } catch (error) {
      console.error('Failed to load remote config:', error);
    }
    
    // 降级到本地配置
    return this.localAdapter.getOverlayConfig();
  }
  
  private validateConfig(config: OverlayConfig): boolean {
    return (
      config &&
      Array.isArray(config.corners) &&
      config.corners.every(corner => 
        corner.position && Array.isArray(corner.tags)
      )
    );
  }
}

问题 10: 配置更新时的竞态条件

场景:

  • 用户快速切换配置源
  • 可能导致旧的配置覆盖新的配置

解决方案:

class DicomOverlayTool {
  private configVersion = 0;
  
  async updateConfig(newConfig: OverlayConfig) {
    const currentVersion = ++this.configVersion;
    
    // 模拟异步操作
    await this.processConfig(newConfig);
    
    // 只有当前版本最新时才应用
    if (currentVersion === this.configVersion) {
      this.config = newConfig;
      this.render();
    } else {
      console.log('Config outdated, skipping');
    }
  }
}

9.6 内存问题

问题 11: 事件监听器未正确清理

场景:

  • 多次启用/禁用工具
  • 事件监听器累积导致内存泄漏

解决方案:

class DicomOverlayTool {
  private eventHandlers = new Map<string, Function>();
  
  onSetToolEnabled() {
    const handler = this.onImageRendered.bind(this);
    this.eventHandlers.set('IMAGE_RENDERED', handler);
    eventTarget.addEventListener(EVENTS.IMAGE_RENDERED, handler);
  }
  
  onSetToolDisabled() {
    // 清理所有事件监听器
    this.eventHandlers.forEach((handler, event) => {
      eventTarget.removeEventListener(event, handler);
    });
    this.eventHandlers.clear();
  }
}

📚 10. 实现注意事项

10.1 代码规范

  1. 使用 TypeScript 严格模式

    • 启用 strict 选项
    • 明确定义所有类型
    • 避免使用 any
  2. 遵循项目代码风格

    • 使用现有的 ESLint 配置
    • 运行 npm run lint:fix 格式化代码
  3. 注释和文档

    • 所有公共方法添加 JSDoc 注释
    • 复杂逻辑添加行内注释说明

10.2 测试建议

  1. 单元测试

    • 测试格式化函数的各种输入
    • 测试位置计算的边界情况
    • 测试配置验证逻辑
  2. 集成测试

    • 测试完整的开关流程
    • 测试配置加载和降级
    • 测试与其他工具的兼容性
  3. 性能测试

    • 测试大序列(1000+ 帧)的性能
    • 测试快速切换图像的响应时间
    • 监控内存使用情况

10.3 部署检查清单

  • 所有代码通过 ESLint 检查
  • 所有代码通过 TypeScript 编译
  • 完成单元测试,覆盖率 > 80%
  • 完成手动测试所有场景
  • 更新用户文档
  • 更新 CHANGELOG
  • Code Review 通过
  • 在测试环境验证
  • 准备回滚方案

🎓 11. 学习资源

11.1 Cornerstone3D 文档

11.2 DICOM 标准

11.3 Canvas API


📝 12. 总结

本实现方案采用适配器模式关注点分离的设计原则,提供了一个灵活、可扩展、易维护的四角 DICOM 信息显示功能。

核心优势

  1. 配置灵活: 支持本地和远程配置,可动态切换
  2. 格式丰富: 支持多种 DICOM 标签格式化
  3. 性能优化: 使用缓存和防抖减少不必要的渲染
  4. 错误处理: 完善的降级和异常处理机制
  5. 易于扩展: 模块化设计,易于添加新功能
  6. 一致性: 与现有架构保持一致

后续优化方向

  1. 🔮 支持用户自定义 tag 配置界面
  2. 🔮 支持更多格式化选项(如单位转换)
  3. 🔮 支持主题切换(亮色/暗色)
  4. 🔮 支持导出当前配置
  5. 🔮 性能监控和优化


🏊 13. 启用/停用工具流程图(泳道图)

13.1 启用流程

sequenceDiagram
    participant User as 👤 用户
    participant MP as MorePanel
    participant Redux as Redux Store
    participant SV as StackViewer
    participant TG as ToolGroup
    participant Tool as DicomOverlayTool
    participant CM as ConfigManager
    participant CS as Cornerstone3D
    
    User->>MP: 点击"tag信息"按钮
    Note over MP: handleTagInfo()
    MP->>Redux: dispatch(toggleOverlay())
    Note over Redux: enabled: false → true
    
    Redux-->>SV: 状态变化通知
    Note over SV: useEffect监听overlayEnabled
    
    SV->>TG: getToolGroup(viewportId)
    TG-->>SV: 返回toolGroup实例
    
    SV->>TG: setToolEnabled(DicomOverlayTool.toolName)
    Note over TG: 工具状态: Passive → Enabled
    
    TG->>Tool: onSetToolEnabled()
    Note over Tool: 工具被激活
    
    Tool->>CM: getConfig()
    Note over CM: 尝试加载配置
    
    alt 远程配置可用
        CM->>CM: RemoteAdapter.fetch()
        CM-->>Tool: 返回远程配置
    else 远程配置失败
        CM->>CM: LocalAdapter.getConfig()
        CM-->>Tool: 返回本地配置
    end
    
    Note over Tool: 配置加载完成
    Tool->>CS: 触发viewport.render()
    
    CS->>Tool: renderAnnotation(enabledElement, svgHelper)
    Note over Tool: Cornerstone自动调用渲染
    
    Tool->>Tool: extractMetadata(imageId)
    Tool->>Tool: prepareTextLines()
    Tool->>Tool: renderToSVG()
    
    Tool->>CS: 创建SVG文本元素
    Note over CS: SVG layer
    CS-->>User: 显示四角信息 ✅

13.2 停用流程

sequenceDiagram
    participant User as 👤 用户
    participant MP as MorePanel
    participant Redux as Redux Store
    participant SV as StackViewer
    participant TG as ToolGroup
    participant Tool as DicomOverlayTool
    participant CS as Cornerstone3D
    
    User->>MP: 再次点击"tag信息"按钮
    Note over MP: handleTagInfo()
    MP->>Redux: dispatch(toggleOverlay())
    Note over Redux: enabled: true → false
    
    Redux-->>SV: 状态变化通知
    Note over SV: useEffect监听overlayEnabled
    
    SV->>TG: getToolGroup(viewportId)
    TG-->>SV: 返回toolGroup实例
    
    SV->>TG: setToolDisabled(DicomOverlayTool.toolName)
    Note over TG: 工具状态: Enabled → Disabled
    
    TG->>Tool: onSetToolDisabled()
    Note over Tool: 工具被停用
    
    Tool->>CS: 触发viewport.render()
    Note over CS: 重新渲染viewport
    
    CS->>Tool: renderAnnotation(enabledElement, svgHelper)
    Note over Tool: 由于工具disabled,<br/>Cornerstone不再调用渲染
    
    Note over CS: SVG元素被清除
    CS-->>User: 四角信息消失 ✅

13.3 参与者说明

🎨 UI层

  1. User (用户)

    • 角色: 触发操作的人
    • 操作: 点击"tag信息"按钮
  2. MorePanel (src/pages/view/components/MorePanel.tsx)

    • 角色: UI控制面板
    • 关键方法:
      • handleTagInfo(): 处理按钮点击
    • 职责: 触发Redux action

📦 状态管理层

  1. Redux Store (src/states/view/dicomOverlaySlice.ts)
    • 角色: 全局状态管理
    • 关键状态:
      • enabled: boolean: overlay开关状态
    • 关键Action:
      • toggleOverlay(): 切换enabled状态
    • 职责: 存储和通知状态变化

🖼️ 视图层

  1. StackViewer (src/pages/view/components/viewers/stack.image.viewer.tsx)

    • 角色: 图像查看器组件
    • 关键Hook:
      • useSelector(selectOverlayEnabled): 监听状态
      • useEffect([overlayEnabled]): 响应状态变化
    • 关键逻辑:

      useEffect(() => {
      const toolGroup = ToolGroupManager.getToolGroup(`STACK_TOOL_GROUP_ID_${viewportId}`);
      if (overlayEnabled) {
       toolGroup.setToolEnabled(DicomOverlayTool.toolName);
      } else {
       toolGroup.setToolDisabled(DicomOverlayTool.toolName);
      }
      }, [overlayEnabled, viewportId]);
      
      • 职责: 连接Redux状态和Cornerstone工具

      🛠️ 工具管理层

      1. ToolGroup (Cornerstone3D提供)
      2. 角色: 工具集合管理器
      3. 关键方法:
      4. setToolEnabled(toolName): 启用工具
      5. setToolDisabled(toolName): 停用工具
      6. 职责: 管理工具生命周期,触发工具回调

      🎯 核心工具层

      1. DicomOverlayTool (src/components/overlay/DicomOverlayTool.ts)
      2. 角色: DICOM信息显示工具(继承AnnotationTool)
      3. 关键生命周期方法:
      4. onSetToolEnabled(): 工具启用回调 typescript onSetToolEnabled(): void { console.log('[DicomOverlayTool] Tool enabled'); this.loadConfig(); // 异步加载配置 }
      • onSetToolDisabled(): 工具停用回调

        onSetToolDisabled(): void {
        console.log('[DicomOverlayTool] Tool disabled');
        // Cornerstone会自动停止调用renderAnnotation
        }
        
        • renderAnnotation(enabledElement, svgHelper): 渲染回调 typescript renderAnnotation(enabledElement, svgHelper): boolean { if (!this.config) return false; // 提取metadata const metadata = this.extractMetadata(imageId); // 渲染到SVG this.renderer.renderToSVG(svgHelper, viewport, metadata, ...); return true; }
    • 职责: 管理配置、提取数据、触发渲染

⚙️ 配置管理层

  1. ConfigManager (src/config/overlayConfig/OverlayConfigManager.ts)
    • 角色: 配置提供者
    • 关键方法:
      • getConfig(): 获取配置 typescript async getConfig(): Promise<OverlayConfig> { // 优先远程,失败降级本地 try { return await this.remoteAdapter.getOverlayConfig(); } catch { return await this.localAdapter.getOverlayConfig(); } } - 职责: 统一配置访问,处理降级 #### 🎨 渲染引擎层 8. Cornerstone3D - 角色: 医学图像渲染引擎 - 关键职责: - 在渲染循环中自动调用renderAnnotation() - 管理SVG图层 - 响应工具状态变化 - 工作机制: - 工具Enabled时: 渲染循环包含该工具 - 工具Disabled时: 渲染循环跳过该工具 ### 13.4 关键流程说明 #### 启用时的关键步骤 1. 状态更新 (MorePanel → Redux) - 用户点击触发toggleOverlay() - Redux将enabledfalse改为true 2. 状态监听 (Redux → StackViewer) - useSelector检测到状态变化 - useEffect被触发 3. 工具激活 (StackViewer → ToolGroup) - 调用toolGroup.setToolEnabled() - ToolGroup触发工具的onSetToolEnabled() 4. 配置加载 (Tool → ConfigManager) - 异步加载配置 - 加载完成后触发viewport.render() 5. 自动渲染 (Cornerstone → Tool) - Cornerstone在渲染循环中调用renderAnnotation() - 工具创建SVG元素显示信息 #### 停用时的关键步骤 1. 状态更新 (MorePanel → Redux) - 用户再次点击触发toggleOverlay() - Redux将enabledtrue改为false 2. 状态监听 (Redux → StackViewer) - useSelector检测到状态变化 - useEffect被触发 3. 工具停用 (StackViewer → ToolGroup) - 调用toolGroup.setToolDisabled() - ToolGroup触发工具的onSetToolDisabled() 4. 停止渲染 (Cornerstone自动处理) - Cornerstone不再调用该工具的renderAnnotation() - SVG元素被清除 - 四角信息消失 ### 13.5 核心优势 ✅ 自动化渲染: Cornerstone3D自动管理渲染循环,工具无需手动监听事件 ✅ 声明式状态: 使用Redux管理状态,React自动响应变化 ✅ 清晰的职责分离: - UI层只负责触发action - 工具层只负责渲染逻辑 - Cornerstone负责渲染调度 ✅ SVG渲染: 不会被Canvas覆盖,永久显示 --- 文档版本: 1.1.0
        创建日期: 2025-01-25
        最后更新: 2025-01-25
        作者: Cline (AI Assistant)