# 直线灰度测量功能 - 技术设计文档
## 📋 1. 需求详细描述
### 1.1 功能概述
实现一个医学影像直线灰度测量工具,允许用户在DICOM图像上绘制直线,并实时计算和显示该直线路径上的灰度值统计信息。
### 1.2 核心需求(第一版)
基于用户确认,采用以下需求:
- **显示信息**: 平均灰度值、最小值、最大值
- **交互方式**: 用户通过两点绘制直线,可拖拽调整端点和整体位置
- **采样方式**: 沿直线采样所有经过的像素点
### 1.3 扩展需求(备选方案)
以下需求可在后续迭代中实现:
#### 方案2: 曲线图模式
- 显示灰度值曲线图和统计信息
- 绘制后固定不可调整
- 按1mm间隔采样
#### 方案3: 简化模式
- 仅显示平均灰度值和标准差
- 完整交互支持(拖拽、移动、删除)
- 采样所有像素点
### 1.4 功能入口
- **位置**: [`src/pages/view/components/MeasurementPanel.tsx:275-280`](src/pages/view/components/MeasurementPanel.tsx:275)
- **按钮**:
```tsx
```
---
## 👥 2. 参与者清单
### 2.1 UI组件层
#### 2.1.1 MeasurementPanel(测量面板)
- **文件**: [`src/pages/view/components/MeasurementPanel.tsx`](src/pages/view/components/MeasurementPanel.tsx)
- **作用**: 提供"直线灰度"按钮,触发Redux action
- **关键方法**:
- `handleMeasurementAction(action: string)` - 分发测量操作
#### 2.1.2 ViewerContainer(视口容器)
- **文件**: [`src/pages/view/components/ViewerContainer.tsx`](src/pages/view/components/ViewerContainer.tsx)
- **作用**: 监听Redux action,调用工具激活函数
- **关键逻辑**:
- `useEffect` 监听 `measurementAction` (line:558-697)
- `switch-case` 处理 "直线灰度" action
- 调用 `activateLineGrayscaleMeasurement(viewportId)`
### 2.2 工具类层
#### 2.2.1 LineGrayscaleMeasurementTool(核心工具类)
- **文件**: `src/components/measures/LineGrayscaleMeasurementTool.ts`(**新建**)
- **继承**: `AnnotationTool` (Cornerstone3D基类)
- **作用**: 实现直线绘制、灰度采样、统计计算、渲染显示
- **关键方法**:
- `static toolName = 'LineGrayscaleMeasurementTool'`
- `static createDefaultAnnotation()` - 创建预设注解
- `addNewAnnotation()` - 用户交互绘制(返回空,禁用手动绘制)
- `_updateCachedStats()` - 计算灰度统计数据
- `renderAnnotation()` - SVG渲染直线和文本
- `_mouseDownModifyCallback()` - 处理鼠标按下
- `_mouseDragModifyCallback()` - 处理拖拽
- `_mouseMoveModifyCallback()` - 处理悬停
- `_keyDownCallback()` - 处理Delete删除
- `_samplePixelsAlongLine()` - **核心算法**:沿直线采样像素
- `_calculateGrayscaleStats()` - 计算统计值
### 2.3 管理器层
#### 2.3.1 MeasurementToolManager
- **文件**: [`src/utils/measurementToolManager.ts`](src/utils/measurementToolManager.ts)
- **作用**: 统一管理测量工具的激活/停用/清除
- **需要添加的方法**:
- `static activateLineGrayscaleMeasurementTool(viewportId: string): boolean`
- `static deactivateLineGrayscaleMeasurementTool(viewportId: string): boolean`
- `static clearLineGrayscaleMeasurements(viewportId: string): boolean`
- `static clearLineGrayscaleMeasurementsForViewports(viewportIds: string[]): boolean[]`
#### 2.3.2 cornerstoneToolsSetup
- **文件**: [`src/utils/cornerstoneToolsSetup.ts`](src/utils/cornerstoneToolsSetup.ts)
- **作用**: 全局注册工具
- **需要修改**:
- 在 `registerGlobalTools()` 中添加 `cornerstoneTools.addTool(LineGrayscaleMeasurementTool)`
### 2.4 工具包装层
#### 2.4.1 stack.image.viewer.tsx
- **文件**: [`src/pages/view/components/viewers/stack.image.viewer.tsx`](src/pages/view/components/viewers/stack.image.viewer.tsx)
- **作用**: 视口级别工具注册和配置
- **需要修改**:
- `registerTools()` 添加工具到工具组 (line:179-254)
- `setupDefaultToolStates()` 设置工具默认状态 (line:259-311)
- 导出 `activateLineGrayscaleMeasurement()` 函数
### 2.5 状态管理层
#### 2.5.1 measurementPanelSlice(Redux Slice)
- **文件**: `src/states/view/measurementPanelSlice.ts`
- **作用**: 管理测量工具状态
- **涉及的Action**:
- `setMeasurementAction('直线灰度')` - 设置当前测量动作
- `clearMeasurementAction()` - 清除动作
- `setToolActive()` - 标记工具为激活状态
- `setToolInactive()` - 标记工具为非激活状态
### 2.6 Cornerstone3D API层
#### 2.6.1 Viewport API
- **来源**: `@cornerstonejs/core`
- **关键接口**:
- `viewport.worldToCanvas(point3D)` - 世界坐标转Canvas坐标
- `viewport.canvasToWorld(point2D)` - Canvas坐标转世界坐标
- `viewport.getImageData()` - 获取图像数据对象
- `viewport.getCurrentImageId()` - 获取当前图像ID
- `viewport.render()` - 触发重新渲染
#### 2.6.2 Image Data API
- **来源**: `@cornerstonejs/core`
- **关键方法**:
- `imageData.getScalarData()` - 获取像素数组(Uint16Array)
- `imageData.getDimensions()` - 获取图像尺寸 [width, height, depth]
- `imageData.getSpacing()` - 获取像素间距 [x, y, z]
#### 2.6.3 Annotation State API
- **来源**: `@cornerstonejs/tools`
- **关键方法**:
- `annotation.state.addAnnotation()` - 添加注解
- `annotation.state.removeAnnotation()` - 删除注解
- `annotation.state.getAnnotations()` - 获取注解列表
#### 2.6.4 SVG Drawing API
- **来源**: `@cornerstonejs/tools`
- **关键方法**:
- `drawLineSvg()` - 绘制SVG线段
- `drawHandles()` - 绘制控制点
- `drawLinkedTextBox()` - 绘制文本框
---
## 📝 3. 实现TodoList
### 3.1 创建新文件
#### ✅ 文件1: LineGrayscaleMeasurementTool.ts
- **路径**: `src/components/measures/LineGrayscaleMeasurementTool.ts`
- **类型**: 工具类实现
- **内容**:
- 导入依赖(Cornerstone3D、工具基类、类型定义)
- 定义 `LineGrayscaleAnnotation` 接口
- 实现 `LineGrayscaleMeasurementTool` 类
- 实现像素采样算法
- 实现统计计算逻辑
- 实现事件处理器
- 实现渲染逻辑
#### ✅ 文件2: LineGrayscaleMeasurementTool.README.md
- **路径**: `src/components/measures/LineGrayscaleMeasurementTool.README.md`
- **类型**: 文档
- **内容**:
- 功能说明
- 使用方法
- API文档
- 算法原理
### 3.2 修改现有文件
#### 📝 修改1: ViewerContainer.tsx
- **文件**: [`src/pages/view/components/ViewerContainer.tsx`](src/pages/view/components/ViewerContainer.tsx)
- **位置1**: 导入区域(line:2-42)
```typescript
import {
// ... 现有导入
activateLineGrayscaleMeasurement,
} from './viewers/stack.image.viewer';
```
- **位置2**: 工具配置映射(line:77-163)
```typescript
const MEASUREMENT_TOOL_CONFIGS: Record = {
// ... 现有配置
'直线灰度': {
toolName: 'LineGrayscaleMeasurementTool',
activateFunction: activateLineGrayscaleMeasurement,
logPrefix: 'LineGrayscale',
},
};
```
- **位置3**: switch-case(line:661-683)
```typescript
case '直线灰度': {
const config = MEASUREMENT_TOOL_CONFIGS[measurementAction];
if (config) {
activateMeasurementTool(config, selectedViewportIds);
}
break;
}
```
- **位置4**: 清除测量(line:600-621)
```typescript
case '清除测量':
if (selectedViewportIds.length > 0) {
// ... 现有清除逻辑
MeasurementToolManager.clearLineGrayscaleMeasurementsForViewports(selectedViewportIds);
}
break;
```
#### 📝 修改2: stack.image.viewer.tsx
- **文件**: [`src/pages/view/components/viewers/stack.image.viewer.tsx`](src/pages/view/components/viewers/stack.image.viewer.tsx)
- **位置1**: 导入区域(line:9-32)
```typescript
import LineGrayscaleMeasurementTool from '@/components/measures/LineGrayscaleMeasurementTool';
```
- **位置2**: registerTools函数(line:218-246)
```typescript
toolGroup.addTool(LineGrayscaleMeasurementTool.toolName);
```
- **位置3**: setupDefaultToolStates函数(line:294-310)
```typescript
toolGroup.setToolPassive(LineGrayscaleMeasurementTool.toolName);
```
- **位置4**: 新增导出函数(文件末尾,约line:1432后)
```typescript
export function activateLineGrayscaleMeasurement(viewportId: string): boolean {
console.log(`[activateLineGrayscaleMeasurement] Activating for viewport: ${viewportId}`);
return MeasurementToolManager.activateLineGrayscaleMeasurementTool(viewportId);
}
export function deactivateLineGrayscaleMeasurement(viewportId: string): boolean {
console.log(`[deactivateLineGrayscaleMeasurement] Deactivating for viewport: ${viewportId}`);
return MeasurementToolManager.deactivateLineGrayscaleMeasurementTool(viewportId);
}
export function clearLineGrayscaleMeasurements(viewportId: string): boolean {
console.log(`[clearLineGrayscaleMeasurements] Clearing for viewport: ${viewportId}`);
return MeasurementToolManager.clearLineGrayscaleMeasurements(viewportId);
}
```
#### 📝 修改3: measurementToolManager.ts
- **文件**: [`src/utils/measurementToolManager.ts`](src/utils/measurementToolManager.ts)
- **位置1**: 导入区域(line:3-19)
```typescript
import LineGrayscaleMeasurementTool from '@/components/measures/LineGrayscaleMeasurementTool';
```
- **位置2**: 文件末尾(line:3421后),添加完整的管理方法
```typescript
// ==================== LineGrayscale(直线灰度)测量工具 ====================
static activateLineGrayscaleMeasurementTool(viewportId: string): boolean {
const toolGroup = this.getToolGroup(viewportId);
if (!toolGroup) return false;
try {
// 停用其他工具
toolGroup.setToolPassive(WindowLevelTool.toolName, { removeAllBindings: true });
// ... 其他工具
toolGroup.setToolActive(LineGrayscaleMeasurementTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Primary }],
});
const toolInstance = toolGroup.getToolInstance(LineGrayscaleMeasurementTool.toolName);
const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
if (toolInstance && viewport.element) {
toolInstance._activateModify(viewport.element);
}
// 创建预设注解
try {
if (viewport && viewport.element) {
const defaultAnnotation = LineGrayscaleMeasurementTool.createDefaultAnnotation(
viewport.element as HTMLDivElement,
viewport as cornerstone.Types.IStackViewport
);
cornerstoneTools.annotation.state.addAnnotation(defaultAnnotation, viewport.element);
const enabledElement = cornerstone.getEnabledElement(viewport.element);
if (enabledElement && toolInstance && '_updateCachedStats' in toolInstance) {
(toolInstance as any)._updateCachedStats(defaultAnnotation, enabledElement);
}
viewport.render();
}
} catch (error) {
console.error('[MeasurementToolManager] Failed to create default LineGrayscale annotation:', error);
}
return true;
} catch (error) {
console.error('[MeasurementToolManager] Error activating LineGrayscale tool:', error);
return false;
}
}
static deactivateLineGrayscaleMeasurementTool(viewportId: string): boolean {
const toolGroup = this.getToolGroup(viewportId);
if (!toolGroup) return false;
try {
toolGroup.setToolPassive(LineGrayscaleMeasurementTool.toolName, { removeAllBindings: true });
return true;
} catch (error) {
console.error('[MeasurementToolManager] Error deactivating LineGrayscale tool:', error);
return false;
}
}
static clearLineGrayscaleMeasurements(viewportId: string): boolean {
try {
const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
if (!viewport) return false;
const annotations = cornerstoneTools.annotation.state.getAnnotations(
LineGrayscaleMeasurementTool.toolName,
viewport.element
);
let removedCount = 0;
annotations.forEach((annotation) => {
if (annotation.annotationUID) {
cornerstoneTools.annotation.state.removeAnnotation(annotation.annotationUID);
removedCount++;
}
});
viewport.render();
console.log(`[MeasurementToolManager] Cleared ${removedCount} LineGrayscale measurements`);
return true;
} catch (error) {
console.error('[MeasurementToolManager] Error clearing LineGrayscale measurements:', error);
return false;
}
}
static clearLineGrayscaleMeasurementsForViewports(viewportIds: string[]): boolean[] {
return viewportIds.map((viewportId) => this.clearLineGrayscaleMeasurements(viewportId));
}
```
#### 📝 修改4: cornerstoneToolsSetup.ts
- **文件**: [`src/utils/cornerstoneToolsSetup.ts`](src/utils/cornerstoneToolsSetup.ts)
- **位置1**: 导入区域(line:2-19)
```typescript
import LineGrayscaleMeasurementTool from '@/components/measures/LineGrayscaleMeasurementTool';
```
- **位置2**: registerGlobalTools函数(line:75后)
```typescript
cornerstoneTools.addTool(LineGrayscaleMeasurementTool); // 添加直线灰度测量工具
```
---
## 🏊 4. 交互泳道图
```mermaid
sequenceDiagram
participant User as 用户
participant MP as MeasurementPanel
participant Redux as Redux Store
participant VC as ViewerContainer
participant MTM as MeasurementToolManager
participant Tool as LineGrayscaleTool
participant CS as Cornerstone3D
participant VP as Viewport
User->>MP: 点击"直线灰度"按钮
MP->>Redux: dispatch setMeasurementAction
Redux-->>VC: measurementAction = 直线灰度
VC->>VC: 解析selectedViewers
VC->>MTM: activateLineGrayscaleMeasurementTool
MTM->>MTM: getToolGroup
MTM->>MTM: 停用其他工具
MTM->>Tool: setToolActive
MTM->>Tool: activateModify
MTM->>Tool: createDefaultAnnotation
Tool->>VP: 获取图像中心和尺寸
Tool-->>MTM: 返回默认注解对象
MTM->>CS: annotation.state.addAnnotation
MTM->>Tool: updateCachedStats
Tool->>VP: getImageData
Tool->>Tool: samplePixelsAlongLine
Tool->>Tool: calculateGrayscaleStats
Tool-->>MTM: 更新cachedStats
MTM->>VP: viewport.render
VP->>Tool: renderAnnotation
Tool->>CS: drawLineSvg, drawHandles, drawLinkedTextBox
CS-->>User: 显示直线和灰度统计数据
Redux->>Redux: setToolActive
Redux->>Redux: clearMeasurementAction
Note over User,CS: 用户拖拽端点
User->>Tool: 鼠标按下端点
Tool->>Tool: mouseDownModifyCallback
Tool->>Tool: 记录activeHandleIndex
User->>Tool: 拖拽鼠标
Tool->>Tool: mouseDragModifyCallback
Tool->>VP: canvasToWorld
Tool->>Tool: 更新handles.points
Tool->>Tool: updateCachedStats
Tool->>VP: render
VP-->>User: 实时显示新的统计数据
User->>Tool: 鼠标释放
Tool->>Tool: mouseUpModifyCallback
Tool->>Tool: activeHandleIndex = null
Note over User,CS: 用户删除注解
User->>Tool: 按Delete键
Tool->>Tool: keyDownCallback
Tool->>CS: annotation.state.removeAnnotation
Tool->>VP: render
VP-->>User: 注解消失
```
---
## 🔄 5. 数据流
### 5.1 数据流向图
```mermaid
graph TD
A[用户点击按钮] --> B[Redux Action]
B --> C[ViewerContainer监听]
C --> D[调用激活函数]
D --> E[MeasurementToolManager]
E --> F[创建默认注解对象]
F --> G[获取Viewport图像数据]
G --> H[执行像素采样算法]
H --> I[Bresenham直线算法]
I --> J[遍历直线上的像素坐标]
J --> K[从ImageData读取灰度值]
K --> L[构建灰度值数组]
L --> M[计算统计值]
M --> N[更新cachedStats]
N --> O[触发渲染]
O --> P[绘制SVG直线]
O --> Q[绘制控制点]
O --> R[绘制文本框]
R --> S[显示统计数据]
T[用户拖拽端点] --> U[鼠标事件]
U --> V[更新坐标]
V --> W[重新采样计算]
W --> M
```
### 5.2 数据转换流程
#### 阶段1: 用户交互 → 世界坐标
```
Canvas坐标(鼠标位置)
→ viewport.canvasToWorld()
→ 世界坐标Point3[x, y, z]
→ 存储在annotation.data.handles.points
```
#### 阶段2: 世界坐标 → 像素索引
```
世界坐标Point3[x, y, z]
→ 考虑spacing和dimensions
→ 像素坐标[col, row, slice]
→ 像素索引 = row * width + col
→ scalarData[index]
```
#### 阶段3: 像素数据 → 统计值
```
像素数组Uint16Array
→ 沿直线采样
→ 灰度值数组number[]
→ Math.min/max/average
→ cachedStats对象
```
#### 阶段4: 统计值 → 显示文本
```
cachedStats: {mean, min, max}
→ 格式化文本: "平均:123 最小:45 最大:234"
→ drawLinkedTextBox()
→ SVG 元素
→ 屏幕显示
```
---
## 📊 6. 数据结构定义
### 6.1 核心接口
```typescript
/**
* 直线灰度测量注解接口
*/
interface LineGrayscaleAnnotation extends Types.Annotation {
data: {
handles: {
// 控制点(两个端点,世界坐标)
points: [CoreTypes.Point3, CoreTypes.Point3];
// 当前激活的手柄索引(0或1,null表示未激活)
activeHandleIndex: number | null;
};
// 文本框位置(Canvas坐标)
textBox?: CoreTypes.Point2;
// 缓存的统计结果
cachedStats?: {
[targetId: string]: {
// 平均灰度值
mean: number;
// 最小灰度值
min: number;
// 最大灰度值
max: number;
// 采样点数量
sampleCount: number;
// 直线长度(mm)
lineLength: number;
};
};
};
}
```
### 6.2 像素采样数据结构
```typescript
/**
* 像素采样结果
*/
interface PixelSampleResult {
// 灰度值数组
values: number[];
// 采样点的像素坐标数组
coordinates: Array<{x: number, y: number}>;
// 采样点总数
count: number;
}
```
### 6.3 灰度统计数据结构
```typescript
/**
* 灰度统计结果
*/
interface GrayscaleStats {
// 平均值
mean: number;
// 最小值
min: number;
// 最大值
max: number;
// 标准差(扩展需求)
stdDev?: number;
// 中位数(扩展需求)
median?: number;
}
```
---
## 🎯 7. 执行流程
### 7.1 完整执行流程(文字描述)
1. **启动阶段**:
- 用户点击测量面板的"直线灰度"按钮
- `MeasurementPanel.handleMeasurementAction()` 分发Redux action
- Redux Store更新 `measurementAction = '直线灰度'`
2. **激活阶段**:
- `ViewerContainer.useEffect` 监听到action变化
- 解析选中的viewports,获取viewportIds列表
- 遍历viewportIds,调用 `activateLineGrayscaleMeasurement(viewportId)`
- `MeasurementToolManager.activateLineGrayscaleMeasurementTool()` 执行激活逻辑
3. **工具配置阶段**:
- 获取工具组 `getToolGroup(viewportId)`
- 停用冲突工具(WindowLevel, Magnify等)
- 激活LineGrayscaleTool,绑定鼠标左键
- 调用工具实例的 `_activateModify(element)`
4. **创建注解阶段**:
- 调用 `LineGrayscaleTool.createDefaultAnnotation()`
- 获取viewport尺寸和图像中心点
- 设置起点 = 中心左侧50px,终点 = 中心右侧50px
- 创建annotation对象,包含handles、textBox等数据
- 调用 `annotation.state.addAnnotation()` 添加到状态管理
5. **数据计算阶段**:
- 调用 `_updateCachedStats(annotation, enabledElement)`
- 获取图像数据 `viewport.getImageData()`
- 调用 `_samplePixelsAlongLine()` 执行Bresenham算法
- 遍历直线上的每个像素点,读取灰度值
- 调用 `_calculateGrayscaleStats()` 计算统计值
- 更新 `annotation.data.cachedStats`
6. **渲染阶段**:
- 调用 `viewport.render()` 触发渲染
- 工具的 `renderAnnotation()` 方法被调用
- 转换世界坐标到Canvas坐标
- 使用SVG绘制直线(`drawLineSvg`)
- 绘制两个端点手柄(`drawHandles`)
- 格式化统计文本
- 绘制文本框(`drawLinkedTextBox`)
7. **完成阶段**:
- 分发 `setToolActive({toolName, viewportId})` 更新工具状态
- 分发 `clearMeasurementAction()` 清除action
- 用户看到图像上显示直线和统计数据
### 7.2 用户交互流程
#### 拖拽端点:
1. 用户鼠标按下端点 → `_mouseDownModifyCallback()` → 记录activeHandleIndex
2. 用户移动鼠标 → `_mouseDragModifyCallback()` → 更新坐标 → 重新计算 → 实时渲染
3. 用户释放鼠标 → `_mouseUpModifyCallback()` → activeHandleIndex=null
#### 移动整体:
1. 用户鼠标按下直线 → 检测在线段上 → 记录初始位置
2. 用户拖拽 → 两个端点同时平移 → 重新计算 → 实时渲染
3. 用户释放鼠标 → 完成移动
#### 删除注解:
1. 用户选中注解后按Delete键
2. `_keyDownCallback()` 处理
3. 调用 `annotation.state.removeAnnotation()`
4. 触发重新渲染,注解消失
---
## 🧪 8. 测试方案
### 8.1 手动测试操作步骤
#### 测试场景1: 基本功能测试
**前置条件**: 已登录系统,进入检查页面,图像已加载
| 步骤 | 操作 | 预期结果 |
|------|------|----------|
| 1 | 点击右侧测量面板的"直线灰度"按钮 | 按钮高亮激活 |
| 2 | 观察图像中心 | 自动生成一条水平直线(两个绿色端点) |
| 3 | 观察直线旁文本框 | 显示"平均:xxx 最小:yyy 最大:zzz" |
| 4 | 鼠标悬停在左端点上 | 光标变为十字形,端点高亮 |
| 5 | 拖拽左端点到图像左上角 | 直线跟随移动,统计数据实时更新 |
| 6 | 拖拽右端点到图像右下角 | 直线呈对角线,统计数据更新 |
| 7 | 鼠标悬停在直线中间 | 光标变为移动形状 |
| 8 | 拖拽直线整体 | 直线平移到新位置,统计数据更新 |
| 9 | 点击选中注解后按Delete键 | 注解被删除,直线消失 |
#### 测试场景2: 多viewport测试
**前置条件**: 布局切换为2x2,加载4张图像
| 步骤 | 操作 | 预期结果 |
|------|------|----------|
| 1 | 点击左上角图像 | 图像高亮选中(蓝色边框) |
| 2 | 点击"直线灰度"按钮 | 仅左上角图像出现直线 |
| 3 | 按住Ctrl点击右下角图像 | 两张图像同时选中 |
| 4 | 再次点击"直线灰度"按钮 | 两张图像都出现直线 |
| 5 | 观察统计数据 | 不同图像显示不同的灰度值 |
#### 测试场景3: 边界情况测试
| 步骤 | 操作 | 预期结果 |
|------|------|----------|
| 1 | 拖拽端点到图像边缘外 | 端点被限制在图像边界内 |
| 2 | 绘制极短直线(2个像素) | 仍能计算统计值,不报错 |
| 3 | 绘制极长直线(跨整张图) | 统计值正确,性能流畅 |
| 4 | 在纯黑色区域绘制 | 平均/最小/最大值均为0 |
| 5 | 在纯白色区域绘制 | 统计值接近最大值(如4095) |
---
## 🐛 9. 潜在问题与异常处理
### 9.1 边界情况
#### 问题1: 直线超出图像边界
**解决方案**: 在拖拽时限制坐标在图像范围内
#### 问题2: 直线长度为0
**解决方案**: 返回默认统计值(全0)
#### 问题3: 图像数据未加载
**解决方案**: 检查imageData是否存在,不存在则返回null
### 9.2 数值异常
#### 问题4: 灰度值超出预期范围
**解决方案**: 应用DICOM Rescale Slope/Intercept参数
#### 问题5: 除零错误
**解决方案**: 检查sampleCount > 0
### 9.3 性能问题
#### 问题6: 长直线采样性能
**解决方案**: 超过5000像素考虑使用Web Worker
#### 问题7: 频繁渲染
**解决方案**: 使用requestAnimationFrame节流
---
## 🔧 10. 核心算法原理
### 10.1 Bresenham直线算法
**算法目的**: 确定从起点到终点的所有像素坐标
**伪代码**:
```typescript
function bresenhamLine(x0, y0, x1, y1): Pixel[] {
const dx = Math.abs(x1 - x0);
const dy = Math.abs(y1 - y0);
const sx = x0 < x1 ? 1 : -1;
const sy = y0 < y1 ? 1 : -1;
let err = dx - dy;
const pixels: Pixel[] = [];
let x = x0, y = y0;
while (true) {
pixels.push({x, y});
if (x === x1 && y === y1) break;
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x += sx;
}
if (e2 < dx) {
err += dx;
y += sy;
}
}
return pixels;
}
```
**优势**:
- 仅使用整数运算,性能高
- 确保直线连续无断点
- 对称性好
### 10.2 像素索引计算
**公式**:
```
index = (row * imageWidth) + col
```
**坐标系转换**:
```
世界坐标 → 像素坐标:
pixelX = (worldX - origin[0]) / spacing[0]
pixelY = (worldY - origin[1]) / spacing[1]
像素坐标 → 数组索引:
index = Math.floor(pixelY) * dimensions[0] + Math.floor(pixelX)
```
### 10.3 统计值计算
**平均值**:
```typescript
const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
```
**最小/最大值**:
```typescript
const min = Math.min(...values);
const max = Math.max(...values);
```
---
## ✅ 12. 验收标准
### 12.1 功能验收
- [ ] 用户能点击"直线灰度"按钮激活工具
- [ ] 自动在图像中心生成预设直线
- [ ] 直线旁显示统计文本(平均、最小、最大值)
- [ ] 用户能拖拽端点调整直线
- [ ] 用户能拖拽直线整体移动
- [ ] 统计值随直线调整实时更新
- [ ] 用户能按Delete键删除注解
- [ ] 点击"清除测量"能清除所有直线灰度注解
### 12.2 性能验收
- [ ] 创建注解响应时间 < 100ms
- [ ] 拖拽操作帧率 ≥ 30fps
- [ ] 5000像素长直线采样时间 < 100ms
### 12.3 兼容性验收
- [ ] 支持CT、MRI、DR等常见模态
- [ ] 支持8-bit、12-bit、16-bit位深
- [ ] 与现有测量工具共存无冲突
- [ ] 在1x1、1x2、2x1、2x2布局下正常工作
---
## 🚀 13. 实施计划
### 阶段1: 核心工具开发(2-3天)
1. 创建 `LineGrayscaleMeasurementTool.ts`
2. 实现基础注解结构和数据模型
3. 实现像素采样算法(Bresenham)
4. 实现统计计算逻辑
5. 实现基础渲染(直线+端点)
### 阶段2: 交互功能开发(1-2天)
1. 实现鼠标事件处理器
2. 实现拖拽调整功能
3. 实现Delete删除功能
### 阶段3: 集成和配置(1天)
1. 在各文件中注册和配置工具
2. 导出激活/停用/清除函数
### 阶段4: 测试和优化(1-2天)
1. 手动功能测试
2. 数据准确性验证
3. 性能测试和优化
4. 边界情况修复
### 阶段5: 文档和交付(0.5天)
1. 编写README
2. 添加代码注释
3. 更新设计文档
**预计总工期**: 5-8天
---
## 📝 14. 版本历史
| 版本 | 日期 | 作者 | 变更内容 |
|------|------|------|----------|
| v1.0 | 2025-12-10 | Architect Mode | 初始版本,完整设计文档 |