Преглед на файлове

实现 直线灰度测量

dengdx преди 1 месец
родител
ревизия
44981e7846

+ 383 - 0
docs/实现/直线灰度测量功能-Bug修复方案.md

@@ -0,0 +1,383 @@
+# 直线灰度测量功能 - Bug修复方案
+
+## 📋 Bug描述
+
+**问题**:用户在拖拽直线端点时遇到运行时错误
+
+**错误信息**:
+```
+imageData.getScalarData is not a function
+TypeError: imageData.getScalarData is not a function
+    at LineGrayscaleMeasurementTool._samplePixelsAlongLine (line 690)
+```
+
+**复现步骤**:
+1. 点击"直线灰度"按钮激活工具
+2. 直线自动创建在视图中心
+3. 拖拽直线的任意端点
+4. 控制台报错,功能失败
+
+---
+
+## 🔍 根本原因分析
+
+### 1. 错误代码位置
+
+[`LineGrayscaleMeasurementTool.ts:661`](src/components/measures/LineGrayscaleMeasurementTool.ts:661)
+```typescript
+// ❌ 错误:viewport没有getImageData()方法
+const imageData = (viewport as any).getImageData();
+```
+
+[`LineGrayscaleMeasurementTool.ts:690`](src/components/measures/LineGrayscaleMeasurementTool.ts:690)
+```typescript
+// ❌ 错误:getScalarData()方法不存在
+const scalarData = imageData.getScalarData();
+const dimensions = imageData.getDimensions();
+const spacing = imageData.getSpacing();
+```
+
+### 2. 问题分析
+
+- **API误用**:`viewport.getImageData()` 不是Cornerstone3D的标准API
+- **类型混淆**:即使获取到imageData对象,它也不是vtkImageData类型,没有`getScalarData()`方法
+- **架构不匹配**:Cornerstone3D的AnnotationTool不直接提供像素数据访问
+
+### 3. 正确方式参考
+
+项目中已有正确实现:[`DicomOverlayTool.ts:80`](src/components/overlay/DicomOverlayTool.ts:80)
+
+```typescript
+import * as cornerstone from '@cornerstonejs/core';
+
+// ✅ 正确:从缓存获取image对象
+const image = cornerstone.cache.getImage(imageId);
+const pixelData = image.getPixelData();
+```
+
+---
+
+## 🛠️ 修复方案
+
+### 方案概述
+
+使用Cornerstone3D的**缓存系统**访问像素数据,而不是直接从viewport获取。
+
+### 修复步骤
+
+#### 步骤1:导入cornerstone核心库
+
+**文件**:[`src/components/measures/LineGrayscaleMeasurementTool.ts`](src/components/measures/LineGrayscaleMeasurementTool.ts:1)
+
+**修改**:在文件开头添加导入
+```typescript
+import * as cornerstone from '@cornerstonejs/core';
+```
+
+#### 步骤2:修复 `_updateCachedStats` 方法
+
+**位置**:[line 649-679](src/components/measures/LineGrayscaleMeasurementTool.ts:649)
+
+**修改前**:
+```typescript
+private _updateCachedStats(
+  annotation: LineGrayscaleAnnotation,
+  enabledElement: CoreTypes.IEnabledElement
+): void {
+  const { viewport } = enabledElement;
+  const { points } = annotation.data.handles;
+
+  if (points.length < 2) {
+    return;
+  }
+
+  // ❌ 错误的API
+  const imageData = (viewport as any).getImageData();
+  if (!imageData) {
+    console.warn('[LineGrayscaleTool] Image data not available');
+    return;
+  }
+
+  // ❌ 错误的方法调用
+  const sampleResult = this._samplePixelsAlongLine(points[0], points[1], imageData, viewport);
+  // ...
+}
+```
+
+**修改后**:
+```typescript
+private _updateCachedStats(
+  annotation: LineGrayscaleAnnotation,
+  enabledElement: CoreTypes.IEnabledElement
+): void {
+  const { viewport } = enabledElement;
+  const { points } = annotation.data.handles;
+
+  if (points.length < 2) {
+    return;
+  }
+
+  // ✅ 使用Cornerstone缓存系统获取图像
+  const imageId = viewport.getCurrentImageId?.();
+  if (!imageId) {
+    console.warn('[LineGrayscaleTool] No imageId available');
+    return;
+  }
+
+  const image = cornerstone.cache.getImage(imageId);
+  if (!image) {
+    console.warn('[LineGrayscaleTool] Image not found in cache');
+    return;
+  }
+
+  // ✅ 传入image对象而非imageData
+  const sampleResult = this._samplePixelsAlongLine(points[0], points[1], image, viewport);
+  
+  // 计算统计值
+  const stats = this._calculateGrayscaleStats(sampleResult.values, points[0], points[1]);
+
+  // 更新缓存
+  const targetId = `imageId:${imageId}`;
+  if (!annotation.data.cachedStats) {
+    annotation.data.cachedStats = {};
+  }
+  annotation.data.cachedStats[targetId] = stats;
+}
+```
+
+#### 步骤3:修复 `_samplePixelsAlongLine` 方法
+
+**位置**:[line 684-741](src/components/measures/LineGrayscaleMeasurementTool.ts:684)
+
+**修改前**:
+```typescript
+private _samplePixelsAlongLine(
+  startWorld: CoreTypes.Point3,
+  endWorld: CoreTypes.Point3,
+  imageData: any,  // ❌ 错误的参数类型
+  viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+): PixelSampleResult {
+  // ❌ 错误的方法调用
+  const scalarData = imageData.getScalarData();
+  const dimensions = imageData.getDimensions();
+  const spacing = imageData.getSpacing();
+
+  // ❌ 错误的坐标转换逻辑
+  const imageMetadata = (viewport as any).getImageData();
+  const origin = imageMetadata.origin || [0, 0, 0];
+
+  const startPixel = [
+    Math.floor((startWorld[0] - origin[0]) / spacing[0]),
+    Math.floor((startWorld[1] - origin[1]) / spacing[1]),
+  ];
+  // ...
+}
+```
+
+**修改后**:
+```typescript
+private _samplePixelsAlongLine(
+  startWorld: CoreTypes.Point3,
+  endWorld: CoreTypes.Point3,
+  image: any,  // ✅ 传入Cornerstone的image对象
+  viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+): PixelSampleResult {
+  // ✅ 使用正确的API获取像素数据
+  const pixelData = image.getPixelData();
+  const { width, height } = image;
+
+  // ✅ 使用viewport坐标转换(世界坐标→Canvas坐标→像素坐标)
+  const startCanvas = viewport.worldToCanvas(startWorld);
+  const endCanvas = viewport.worldToCanvas(endWorld);
+
+  // ✅ Canvas坐标直接对应像素坐标(对于2D视图)
+  const startPixel = [
+    Math.floor(startCanvas[0]),
+    Math.floor(startCanvas[1]),
+  ];
+
+  const endPixel = [
+    Math.floor(endCanvas[0]),
+    Math.floor(endCanvas[1]),
+  ];
+
+  // Bresenham直线算法
+  const pixels = this._bresenhamLine(
+    startPixel[0],
+    startPixel[1],
+    endPixel[0],
+    endPixel[1]
+  );
+
+  // 采样灰度值
+  const values: number[] = [];
+  const coordinates: Array<{ x: number; y: number }> = [];
+
+  for (const pixel of pixels) {
+    const { x, y } = pixel;
+
+    // 边界检查
+    if (x < 0 || x >= width || y < 0 || y >= height) {
+      continue;
+    }
+
+    // ✅ 计算像素索引(行优先)
+    const index = y * width + x;
+    const value = pixelData[index];
+
+    values.push(value);
+    coordinates.push({ x, y });
+  }
+
+  return {
+    values,
+    coordinates,
+    count: values.length,
+  };
+}
+```
+
+---
+
+## 🔑 关键技术点
+
+### 1. Cornerstone3D 图像访问架构
+
+```mermaid
+graph TB
+A[Viewport] -->|getCurrentImageId| B[imageId]
+B -->|cornerstone.cache.getImage| C[Image对象]
+C -->|getPixelData| D[像素数组]
+C -->|width, height| E[图像尺寸]
+```
+
+### 2. 坐标系转换
+
+```mermaid
+graph LR
+A[世界坐标<br/>Point3] -->|viewport.worldToCanvas| B[Canvas坐标<br/>Point2]
+B -->|直接对应| C[像素坐标<br/>x,y]
+C -->|y*width+x| D[像素索引]
+```
+
+### 3. 正确的API对照表
+
+| 错误用法 ❌ | 正确用法 ✅ | 说明 |
+|---|---|---|
+| `viewport.getImageData()` | `cornerstone.cache.getImage(imageId)` | 获取图像对象 |
+| `imageData.getScalarData()` | `image.getPixelData()` | 获取像素数组 |
+| `imageData.getDimensions()` | `image.width`, `image.height` | 获取尺寸 |
+| 手动计算世界坐标→像素坐标 | `viewport.worldToCanvas()` | 坐标转换 |
+
+---
+
+## 📊 修复前后对比
+
+### 修复前(❌ 错误流程)
+
+```mermaid
+sequenceDiagram
+participant User as 用户
+participant Tool as LineGrayscaleTool
+participant VP as Viewport
+
+User->>Tool: 拖拽端点
+Tool->>Tool: _mouseDragModifyCallback
+Tool->>Tool: _updateCachedStats
+Tool->>VP: viewport.getImageData() ❌
+VP-->>Tool: undefined/错误对象
+Tool->>Tool: imageData.getScalarData() ❌
+Note over Tool: TypeError抛出
+Tool--xUser: 功能失败
+```
+
+### 修复后(✅ 正确流程)
+
+```mermaid
+sequenceDiagram
+participant User as 用户
+participant Tool as LineGrayscaleTool
+participant VP as Viewport
+participant Cache as Cornerstone Cache
+
+User->>Tool: 拖拽端点
+Tool->>Tool: _mouseDragModifyCallback
+Tool->>Tool: _updateCachedStats
+Tool->>VP: getCurrentImageId()
+VP-->>Tool: imageId
+Tool->>Cache: cornerstone.cache.getImage(imageId) ✅
+Cache-->>Tool: Image对象
+Tool->>Tool: image.getPixelData() ✅
+Tool->>Tool: _samplePixelsAlongLine
+Tool->>Tool: _calculateGrayscaleStats
+Tool->>Tool: 更新cachedStats
+Tool-->>User: 显示统计结果
+```
+
+---
+
+## 🧪 测试方案
+
+### 测试1:基本功能测试
+1. 点击"直线灰度"按钮
+2. 验证直线自动出现在视图中心
+3. 拖拽任意端点
+4. **验证**:无错误,统计值实时更新
+
+### 测试2:边界情况测试
+1. 拖拽端点到图像边界外
+2. **验证**:边界检查生效,不会读取越界像素
+
+### 测试3:多图像切换测试
+1. 切换到不同图像
+2. 使用直线灰度工具
+3. **验证**:每张图像的统计值正确
+
+### 测试4:数值准确性测试
+1. 在已知灰度区域画直线
+2. **验证**:平均值、最小值、最大值符合预期
+
+---
+
+## 📝 修复检查清单
+
+- [ ] 导入 `import * as cornerstone from '@cornerstonejs/core'`
+- [ ] 修改 `_updateCachedStats` 方法使用 `cornerstone.cache.getImage()`
+- [ ] 修改 `_samplePixelsAlongLine` 方法参数从 `imageData` 改为 `image`
+- [ ] 修改像素数据访问从 `getScalarData()` 改为 `getPixelData()`
+- [ ] 修改尺寸获取从 `getDimensions()` 改为 `image.width/height`
+- [ ] 修改坐标转换使用 `viewport.worldToCanvas()`
+- [ ] 移除错误的 `spacing` 和 `origin` 计算
+- [ ] 测试拖拽端点功能
+- [ ] 测试统计值计算准确性
+- [ ] 更新技术文档
+
+---
+
+## 🎯 预期结果
+
+修复后,用户应该能够:
+1. ✅ 顺畅拖拽直线端点,无运行时错误
+2. ✅ 实时看到灰度统计值更新(平均、最小、最大)
+3. ✅ 查看直线长度(mm)
+4. ✅ 查看采样点数量
+
+---
+
+## 📚 参考资料
+
+1. **Cornerstone3D 缓存系统文档**  
+   https://www.cornerstonejs.org/docs/concepts/cornerstone-core/cache
+
+2. **项目内正确实现示例**  
+   [`DicomOverlayTool.ts:80`](src/components/overlay/DicomOverlayTool.ts:80)
+
+3. **原设计文档**  
+   [`docs/实现/直线灰度测量功能设计.md`](docs/实现/直线灰度测量功能设计.md)
+
+---
+
+**文档版本**:1.0  
+**创建日期**:2025-12-10  
+**最后更新**:2025-12-10  
+**状态**:待实施

+ 871 - 0
docs/实现/直线灰度测量功能设计.md

@@ -0,0 +1,871 @@
+# 直线灰度测量功能 - 技术设计文档
+
+## 📋 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
+  <FunctionButton
+    title="直线灰度"
+    action="直线灰度"
+    iconName="btn_LineGrayscale"
+    productId="animal"
+  />
+  ```
+
+---
+
+## 👥 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<string, MeasurementToolConfig> = {
+    // ... 现有配置
+    '直线灰度': {
+      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 <text>元素
+  → 屏幕显示
+```
+
+---
+
+## 📊 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 | 初始版本,完整设计文档 |

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "zsis",
-  "version": "1.2.4",
+  "version": "1.2.7",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "zsis",
-      "version": "1.2.4",
+      "version": "1.2.7",
       "dependencies": {
         "@babel/runtime": "^7.24.4",
         "@cornerstonejs/core": "^3.28.0",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "zsis",
-  "version": "1.2.7",
+  "version": "1.3.0",
   "private": true,
   "description": "医学成像系统",
   "main": "main.js",

+ 937 - 0
src/components/measures/LineGrayscaleMeasurementTool.ts

@@ -0,0 +1,937 @@
+import { Point3, Point2 } from './mathUtils';
+import {
+  utilities as csUtils,
+  Types as CoreTypes,
+  getEnabledElement,
+  cache,
+} from '@cornerstonejs/core';
+import * as cornerstone from '@cornerstonejs/core';
+
+import {
+  AnnotationTool,
+  utilities,
+  Types,
+  annotation,
+  drawing,
+} from '@cornerstonejs/tools';
+
+const {
+  drawHandles,
+  drawLinkedTextBox,
+  drawLine: drawLineSvg,
+} = drawing;
+
+import {
+  PublicToolProps,
+  ToolProps,
+  EventTypes,
+  SVGDrawingHelper,
+} from '@cornerstonejs/tools/dist/esm/types';
+
+/**
+ * 直线灰度测量注解接口
+ */
+interface LineGrayscaleAnnotation extends Types.Annotation {
+  data: {
+    handles: {
+      // 控制点(两个端点,世界坐标)
+      points: CoreTypes.Point3[];
+      // 当前激活的手柄索引(0或1,null表示未激活)
+      activeHandleIndex: number | null;
+    };
+    label?: string;
+    // 文本框位置(Canvas坐标)
+    textBox?: CoreTypes.Point2;
+    // 缓存的统计结果
+    cachedStats?: {
+      [targetId: string]: {
+        // 平均灰度值
+        mean: number;
+        // 最小灰度值
+        min: number;
+        // 最大灰度值
+        max: number;
+        // 采样点数量
+        sampleCount: number;
+        // 直线长度(mm)
+        lineLength: number;
+      };
+    };
+  };
+}
+
+/**
+ * 像素采样结果
+ */
+interface PixelSampleResult {
+  values: number[];
+  coordinates: Array<{ x: number; y: number }>;
+  count: number;
+}
+
+/**
+ * 直线灰度测量工具
+ * 功能: 在医学影像上绘制直线,计算并显示直线路径上的灰度值统计信息
+ */
+export default class LineGrayscaleMeasurementTool extends AnnotationTool {
+  static toolName = 'LineGrayscaleMeasurementTool';
+
+  editData: {
+    annotation: Types.Annotation;
+    viewportIdsToRender: string[];
+    handleIndex?: number;
+    newAnnotation?: boolean;
+    hasMoved?: boolean;
+    textBoxBeingMoved?: boolean;
+    textBoxOffset?: CoreTypes.Point2;
+  } | null = null;
+
+  isDrawing: boolean = false;
+
+  /**
+   * 创建默认注解(工具激活时自动创建)
+   */
+  static createDefaultAnnotation(
+    element: HTMLDivElement,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): LineGrayscaleAnnotation {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      throw new Error('Element is not enabled');
+    }
+
+    // 获取viewport的尺寸
+    const canvas = viewport.canvas;
+    const { width, height } = canvas;
+    const centerX = width / 2;
+    const centerY = height / 2;
+
+    // 设置起点和终点(Canvas坐标,水平线,长度100px)
+    const startCanvas: CoreTypes.Point2 = [centerX - 50, centerY];
+    const endCanvas: CoreTypes.Point2 = [centerX + 50, centerY];
+
+    // 转换为世界坐标
+    const startWorld = viewport.canvasToWorld(startCanvas);
+    const endWorld = viewport.canvasToWorld(endCanvas);
+
+    const camera = viewport.getCamera();
+    const { viewPlaneNormal, viewUp } = camera;
+    if (viewPlaneNormal === undefined || viewUp === undefined) {
+      throw new Error('Camera parameters are undefined');
+    }
+
+    // 创建注解对象
+    const annotationData = {
+      invalidated: true,
+      highlighted: false,
+      metadata: {
+        viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
+        viewUp: [...viewUp] as CoreTypes.Point3,
+        FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
+        referencedImageId: viewport.getCurrentImageId?.() || '',
+        toolName: LineGrayscaleMeasurementTool.toolName,
+      },
+      data: {
+        label: '',
+        handles: {
+          points: [startWorld, endWorld],
+          activeHandleIndex: null,
+        },
+        cachedStats: {},
+      },
+    } as LineGrayscaleAnnotation;
+
+    return annotationData;
+  }
+
+  constructor(
+    toolProps: PublicToolProps = {},
+    defaultToolProps: ToolProps = {
+      supportedInteractionTypes: ['Mouse', 'Touch'],
+      configuration: {
+        shadow: true,
+        preventHandleOutsideImage: false,
+      },
+    }
+  ) {
+    super(toolProps, defaultToolProps);
+  }
+
+  /**
+   * 禁用手动绘制(仅支持预设注解)
+   */
+  addNewAnnotation(
+    evt: EventTypes.InteractionEventType
+  ): LineGrayscaleAnnotation {
+    evt.preventDefault();
+    return {} as LineGrayscaleAnnotation;
+  }
+
+  /**
+   * 检查点是否靠近工具
+   */
+  isPointNearTool(
+    element: HTMLDivElement,
+    annotation: LineGrayscaleAnnotation,
+    canvasCoords: CoreTypes.Point2,
+    proximity: number
+  ): boolean {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return false;
+    }
+    const { viewport } = enabledElement;
+
+    const points = annotation.data.handles.points;
+
+    // 检查是否靠近任意一个手柄点
+    for (let i = 0; i < points.length; i++) {
+      const point = points[i];
+      const canvasPoint = viewport.worldToCanvas(point);
+      const distance = Math.sqrt(
+        Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
+        Math.pow(canvasPoint[1] - canvasCoords[1], 2)
+      );
+
+      if (distance < proximity) {
+        return true;
+      }
+    }
+
+    // 检查是否靠近直线
+    if (points.length >= 2) {
+      const p1Canvas = viewport.worldToCanvas(points[0]);
+      const p2Canvas = viewport.worldToCanvas(points[1]);
+      const dist = this._distanceToSegment(canvasCoords, p1Canvas, p2Canvas);
+      if (dist < proximity) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * 计算点到线段的距离
+   */
+  private _distanceToSegment(
+    point: CoreTypes.Point2,
+    lineStart: CoreTypes.Point2,
+    lineEnd: CoreTypes.Point2
+  ): number {
+    const x = point[0];
+    const y = point[1];
+    const x1 = lineStart[0];
+    const y1 = lineStart[1];
+    const x2 = lineEnd[0];
+    const y2 = lineEnd[1];
+
+    const A = x - x1;
+    const B = y - y1;
+    const C = x2 - x1;
+    const D = y2 - y1;
+
+    const dot = A * C + B * D;
+    const lenSq = C * C + D * D;
+    let param = -1;
+
+    if (lenSq !== 0) {
+      param = dot / lenSq;
+    }
+
+    let xx, yy;
+
+    if (param < 0) {
+      xx = x1;
+      yy = y1;
+    } else if (param > 1) {
+      xx = x2;
+      yy = y2;
+    } else {
+      xx = x1 + param * C;
+      yy = y1 + param * D;
+    }
+
+    const dx = x - xx;
+    const dy = y - yy;
+
+    return Math.sqrt(dx * dx + dy * dy);
+  }
+
+  /**
+   * 取消操作
+   */
+  cancel(element: HTMLDivElement): string {
+    if (this.isDrawing) {
+      this.isDrawing = false;
+      this._deactivateDraw(element);
+      this._deactivateModify(element);
+
+      const enabledElement = getEnabledElement(element);
+      if (enabledElement) {
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+      }
+
+      this.editData = null;
+      return this.getToolName();
+    }
+
+    return '';
+  }
+
+  _activateDraw(element: HTMLDivElement): void {
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_DRAG',
+      this._dragCallback as EventListener
+    );
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_UP',
+      this._endCallback as EventListener
+    );
+  }
+
+  _deactivateDraw(element: HTMLDivElement): void {
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_DRAG',
+      this._dragCallback as EventListener
+    );
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_UP',
+      this._endCallback as EventListener
+    );
+  }
+
+  _activateModify(element: HTMLDivElement): void {
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_DOWN',
+      this._mouseDownModifyCallback as EventListener
+    );
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_DRAG',
+      this._mouseDragModifyCallback as EventListener
+    );
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_UP',
+      this._mouseUpModifyCallback as EventListener
+    );
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_MOVE',
+      this._mouseMoveModifyCallback as EventListener
+    );
+    element.addEventListener(
+      'keydown',
+      this._keyDownCallback as EventListener
+    );
+  }
+
+  _deactivateModify(element: HTMLDivElement): void {
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_DOWN',
+      this._mouseDownModifyCallback as EventListener
+    );
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_DRAG',
+      this._mouseDragModifyCallback as EventListener
+    );
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_UP',
+      this._mouseUpModifyCallback as EventListener
+    );
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_MOVE',
+      this._mouseMoveModifyCallback as EventListener
+    );
+    element.removeEventListener(
+      'keydown',
+      this._keyDownCallback as EventListener
+    );
+  }
+
+  _mouseDownModifyCallback = (evt: EventTypes.InteractionEventType): void => {
+    const eventDetail = evt.detail;
+    const { element, currentPoints } = eventDetail;
+    const canvasCoords = currentPoints.canvas;
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return;
+    }
+    const { viewport } = enabledElement;
+
+    const annotations = annotation.state.getAnnotations(this.getToolName(), element);
+    
+    if (!annotations || annotations.length === 0) {
+      return;
+    }
+
+    // 查找最近的手柄
+    for (const ann of annotations) {
+      const customAnn = ann as LineGrayscaleAnnotation;
+      const handle = this.getHandleNearImagePoint(
+        element,
+        customAnn,
+        canvasCoords,
+        6
+      );
+
+      if (handle) {
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        this.editData = {
+          annotation: customAnn,
+          viewportIdsToRender,
+          handleIndex: customAnn.data.handles.activeHandleIndex || 0,
+          hasMoved: false,
+        };
+
+        customAnn.isSelected = true;
+        customAnn.highlighted = true;
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 如果没有点击在工具上,取消所有选中状态
+    for (const ann of annotations) {
+      const customAnn = ann as LineGrayscaleAnnotation;
+      customAnn.isSelected = false;
+    }
+  };
+
+  _mouseDragModifyCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.editData) {
+      return;
+    }
+
+    const eventDetail = evt.detail;
+    const { currentPoints } = eventDetail;
+
+    const enabledElement = getEnabledElement(eventDetail.element);
+    if (!enabledElement) {
+      return;
+    }
+
+    const { annotation: ann, viewportIdsToRender } = this.editData;
+    const customAnn = ann as LineGrayscaleAnnotation;
+    const { data } = customAnn;
+
+    // 处理手柄拖拽
+    const worldPos = currentPoints.world;
+    const activeHandleIndex = data.handles.activeHandleIndex;
+
+    if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length) {
+      data.handles.points[activeHandleIndex] = worldPos;
+
+      this._updateCachedStats(customAnn, enabledElement);
+
+      this.editData.hasMoved = true;
+
+      utilities.triggerAnnotationRenderForViewportIds(
+        viewportIdsToRender
+      );
+
+      evt.preventDefault();
+      evt.stopPropagation();
+    }
+  };
+
+  _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.editData) {
+      return;
+    }
+
+    const { annotation: ann } = this.editData;
+    const customAnn = ann as LineGrayscaleAnnotation;
+
+    customAnn.data.handles.activeHandleIndex = null;
+    customAnn.highlighted = false;
+
+    const eventDetail = evt.detail;
+    const { element } = eventDetail;
+
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        element,
+        this.getToolName()
+      );
+
+    utilities.triggerAnnotationRenderForViewportIds(
+      viewportIdsToRender
+    );
+
+    this.editData = null;
+
+    evt.preventDefault();
+    evt.stopPropagation();
+  };
+
+  _mouseMoveModifyCallback = (evt: EventTypes.InteractionEventType): void => {
+    const eventDetail = evt.detail;
+    const { element, currentPoints } = eventDetail;
+    if (!currentPoints || !currentPoints.canvas) {
+      return;
+    }
+    const canvasCoords = currentPoints.canvas;
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return;
+    }
+    const { viewport } = enabledElement;
+
+    const annotations = annotation.state.getAnnotations(this.getToolName(), element);
+    if (!annotations || annotations.length === 0) {
+      element.style.cursor = 'default';
+      return;
+    }
+
+    let isHovering = false;
+
+    // 检查是否悬停在手柄或直线上
+    for (const ann of annotations) {
+      const customAnn = ann as LineGrayscaleAnnotation;
+
+      // 检查是否靠近手柄
+      const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
+      if (handle) {
+        element.style.cursor = 'crosshair';
+        customAnn.highlighted = true;
+        isHovering = true;
+        break;
+      }
+
+      // 检查是否靠近直线
+      if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
+        element.style.cursor = 'crosshair';
+        customAnn.highlighted = true;
+        isHovering = true;
+        break;
+      }
+    }
+
+    // 如果没有悬停,重置高亮和光标
+    if (!isHovering) {
+      for (const ann of annotations) {
+        const customAnn = ann as LineGrayscaleAnnotation;
+        customAnn.highlighted = false;
+      }
+      element.style.cursor = 'default';
+    }
+
+    // 触发渲染以更新高亮状态
+    const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
+      element,
+      this.getToolName()
+    );
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+  };
+
+  _keyDownCallback = (evt: KeyboardEvent): void => {
+    if (evt.key === 'Delete') {
+      const element = document.activeElement as HTMLDivElement;
+      if (!element) return;
+
+      const annotations = annotation.state.getAnnotations(this.getToolName(), element);
+
+      // 查找选中的注解
+      const selectedAnnotation = annotations.find(ann => ann.isSelected);
+      if (selectedAnnotation) {
+        // 删除选中的注解
+        annotation.state.removeAnnotation(selectedAnnotation.annotationUID || '');
+
+        // 触发视图更新
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+        utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+        evt.preventDefault();
+        evt.stopPropagation();
+      }
+    }
+  };
+
+  handleSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: LineGrayscaleAnnotation
+  ): void {
+    const eventDetail = evt.detail;
+    const { element } = eventDetail;
+
+    annotation.highlighted = true;
+
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        element,
+        this.getToolName()
+      );
+
+    utilities.triggerAnnotationRenderForViewportIds(
+      viewportIdsToRender
+    );
+
+    evt.preventDefault();
+  }
+
+  toolSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: LineGrayscaleAnnotation
+  ): void {
+    // 实现工具选中逻辑
+  }
+
+  _dragCallback = (evt: EventTypes.InteractionEventType): void => {
+    this.isDrawing = true;
+    // 实现拖拽逻辑
+  };
+
+  _endCallback = (evt: EventTypes.InteractionEventType): void => {
+    // 实现结束逻辑
+  };
+
+  getHandleNearImagePoint(
+    element: HTMLDivElement,
+    annotation: LineGrayscaleAnnotation,
+    canvasCoords: CoreTypes.Point2,
+    proximity: number
+  ): Types.ToolHandle | undefined {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return undefined;
+    }
+    const { viewport } = enabledElement;
+
+    const points = annotation.data.handles.points;
+
+    const handleProximity = Math.max(proximity, 15);
+
+    for (let i = 0; i < points.length; i++) {
+      const point = points[i];
+      const canvasPoint = viewport.worldToCanvas(point);
+      const distance = Math.sqrt(
+        Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
+        Math.pow(canvasPoint[1] - canvasCoords[1], 2)
+      );
+
+      if (distance < handleProximity) {
+        annotation.data.handles.activeHandleIndex = i;
+        return {
+          worldPosition: point,
+        } as Types.ToolHandle;
+      }
+    }
+    
+    annotation.data.handles.activeHandleIndex = null;
+    return undefined;
+  }
+
+  /**
+   * 更新缓存的统计数据
+   */
+  private _updateCachedStats(
+    annotation: LineGrayscaleAnnotation,
+    enabledElement: CoreTypes.IEnabledElement
+  ): void {
+    const { viewport } = enabledElement;
+    const { points } = annotation.data.handles;
+
+    if (points.length < 2) {
+      return;
+    }
+
+    // 使用Cornerstone缓存系统获取图像
+    const imageId = viewport.getCurrentImageId?.();
+    if (!imageId) {
+      console.warn('[LineGrayscaleTool] No imageId available');
+      return;
+    }
+
+    const image = cornerstone.cache.getImage(imageId);
+    if (!image) {
+      console.warn('[LineGrayscaleTool] Image not found in cache');
+      return;
+    }
+
+    // 采样像素
+    const sampleResult = this._samplePixelsAlongLine(points[0], points[1], image, viewport);
+
+    // 计算统计值
+    const stats = this._calculateGrayscaleStats(sampleResult.values, points[0], points[1]);
+
+    // 更新缓存
+    const targetId = `imageId:${imageId}`;
+    if (!annotation.data.cachedStats) {
+      annotation.data.cachedStats = {};
+    }
+    annotation.data.cachedStats[targetId] = stats;
+  }
+
+  /**
+   * 沿直线采样像素(Bresenham算法)
+   */
+  private _samplePixelsAlongLine(
+    startWorld: CoreTypes.Point3,
+    endWorld: CoreTypes.Point3,
+    image: any,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): PixelSampleResult {
+    // 使用正确的API获取像素数据
+    const pixelData = image.getPixelData();
+    const { width, height } = image;
+
+    // 使用viewport坐标转换(世界坐标→Canvas坐标)
+    const startCanvas = viewport.worldToCanvas(startWorld);
+    const endCanvas = viewport.worldToCanvas(endWorld);
+
+    // Canvas坐标直接对应像素坐标(对于2D视图)
+    const startPixel = [
+      Math.floor(startCanvas[0]),
+      Math.floor(startCanvas[1]),
+    ];
+
+    const endPixel = [
+      Math.floor(endCanvas[0]),
+      Math.floor(endCanvas[1]),
+    ];
+
+    // Bresenham直线算法
+    const pixels = this._bresenhamLine(
+      startPixel[0],
+      startPixel[1],
+      endPixel[0],
+      endPixel[1]
+    );
+
+    // 采样灰度值
+    const values: number[] = [];
+    const coordinates: Array<{ x: number; y: number }> = [];
+
+    for (const pixel of pixels) {
+      const { x, y } = pixel;
+
+      // 边界检查
+      if (x < 0 || x >= width || y < 0 || y >= height) {
+        continue;
+      }
+
+      // 计算像素索引(行优先)
+      const index = y * width + x;
+      const value = pixelData[index];
+
+      values.push(value);
+      coordinates.push({ x, y });
+    }
+
+    return {
+      values,
+      coordinates,
+      count: values.length,
+    };
+  }
+
+  /**
+   * Bresenham直线算法实现
+   */
+  private _bresenhamLine(
+    x0: number,
+    y0: number,
+    x1: number,
+    y1: number
+  ): Array<{ x: number; y: number }> {
+    const pixels: Array<{ x: number; y: number }> = [];
+
+    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;
+
+    let x = x0;
+    let 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;
+  }
+
+  /**
+   * 计算灰度统计值
+   */
+  private _calculateGrayscaleStats(
+    values: number[],
+    startWorld: CoreTypes.Point3,
+    endWorld: CoreTypes.Point3
+  ): {
+    mean: number;
+    min: number;
+    max: number;
+    sampleCount: number;
+    lineLength: number;
+  } {
+    if (values.length === 0) {
+      return {
+        mean: 0,
+        min: 0,
+        max: 0,
+        sampleCount: 0,
+        lineLength: 0,
+      };
+    }
+
+    // 计算平均值
+    const sum = values.reduce((acc, val) => acc + val, 0);
+    const mean = Math.round(sum / values.length);
+
+    // 计算最小值和最大值
+    const min = Math.min(...values);
+    const max = Math.max(...values);
+
+    // 计算物理距离(mm)
+    const dx = endWorld[0] - startWorld[0];
+    const dy = endWorld[1] - startWorld[1];
+    const dz = endWorld[2] - startWorld[2];
+    const lineLength = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+    return {
+      mean,
+      min,
+      max,
+      sampleCount: values.length,
+      lineLength: parseFloat(lineLength.toFixed(2)),
+    };
+  }
+
+  /**
+   * 渲染注解
+   */
+  renderAnnotation = (
+    enabledElement: CoreTypes.IEnabledElement,
+    svgDrawingHelper: SVGDrawingHelper
+  ): boolean => {
+    const { viewport } = enabledElement;
+    const { element } = viewport;
+
+    let annotations = annotation.state.getAnnotations(
+      LineGrayscaleMeasurementTool.toolName,
+      element
+    ) as LineGrayscaleAnnotation[];
+
+    if (!annotations || annotations.length === 0) {
+      return false;
+    }
+
+    const targetId = `imageId:${(viewport as any).getCurrentImageId?.() || ''}`;
+    const styleSpecifier: any = {
+      toolGroupId: this.toolGroupId,
+      toolName: this.getToolName(),
+      viewportId: viewport.id,
+    };
+
+    for (const annotationItem of annotations) {
+      const { annotationUID, data } = annotationItem;
+      const { points } = data.handles;
+      const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p));
+
+      if (!annotationUID) {
+        continue;
+      }
+
+      styleSpecifier.annotationUID = annotationUID;
+
+      const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotationItem);
+      const lineDash = this.getStyle('lineDash', styleSpecifier, annotationItem);
+      const color = this.getStyle('color', styleSpecifier, annotationItem);
+
+      // 绘制直线
+      const lineUID = `${annotationUID}-line`;
+      drawLineSvg(
+        svgDrawingHelper,
+        annotationUID,
+        lineUID,
+        canvasCoordinates[0],
+        canvasCoordinates[1],
+        {
+          color,
+          width: lineWidth,
+          lineDash,
+        }
+      );
+
+      // 绘制手柄
+      const handleGroupUID = `${annotationUID}-handles`;
+      drawHandles(
+        svgDrawingHelper,
+        annotationUID,
+        handleGroupUID,
+        canvasCoordinates,
+        {
+          color,
+        }
+      );
+
+      // 绘制统计文本
+      const stats = data.cachedStats?.[targetId];
+      if (stats) {
+        const textLines = [
+          `平均: ${stats.mean}`,
+          `最小: ${stats.min}`,
+          `最大: ${stats.max}`,
+          `长度: ${stats.lineLength.toFixed(2)}mm`,
+        ];
+
+        const textBoxPosition = data.textBox || canvasCoordinates[1];
+        const textBoxUID = `${annotationUID}-text`;
+
+        drawLinkedTextBox(
+          svgDrawingHelper,
+          annotationUID,
+          textBoxUID,
+          textLines,
+          textBoxPosition,
+          [canvasCoordinates[1]],
+          {},
+          {
+            color,
+          }
+        );
+      }
+    }
+
+    return true;
+  };
+
+}

+ 9 - 1
src/pages/view/components/ViewerContainer.tsx

@@ -39,6 +39,7 @@ import StackViewer, {
   activateHorizontalTiltMeasurement,
   activatePolygonLengthMeasurement,
   activatePolylineLengthMeasurement,
+  activateLineGrayscaleMeasurement,
 } from './viewers/stack.image.viewer';
 import { useSelector, useDispatch } from 'react-redux';
 import { getDcmImageUrl } from '@/API/bodyPosition';
@@ -160,6 +161,11 @@ const MEASUREMENT_TOOL_CONFIGS: Record<string, MeasurementToolConfig> = {
     activateFunction: activatePolylineLengthMeasurement,
     logPrefix: 'PolylineLength',
   },
+  '直线灰度': {
+    toolName: 'LineGrayscaleMeasurementTool',
+    activateFunction: activateLineGrayscaleMeasurement,
+    logPrefix: 'LineGrayscale',
+  },
 };
 
 const setup = () => {
@@ -618,6 +624,7 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
             MeasurementToolManager.clearHorizontalTiltMeasurementsForViewports(selectedViewportIds);
             MeasurementToolManager.clearPolygonLengthMeasurementsForViewports(selectedViewportIds);
             MeasurementToolManager.clearPolylineLengthMeasurementsForViewports(selectedViewportIds);
+            MeasurementToolManager.clearLineGrayscaleMeasurementsForViewports(selectedViewportIds);
           }
           console.log('Clearing All Measurements from MeasurementPanel');
           break;
@@ -674,7 +681,8 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
         case '直线垂直倾斜度':
         case '直线水平倾斜度':
         case '多边形长度测量':
-        case '拆线长度测量': {
+        case '拆线长度测量':
+        case '直线灰度': {
           const config = MEASUREMENT_TOOL_CONFIGS[measurementAction];
           if (config) {
             activateMeasurementTool(config, selectedViewportIds);

+ 29 - 0
src/pages/view/components/viewers/stack.image.viewer.tsx

@@ -21,6 +21,7 @@ import MidlineMeasurementTool from '@/components/measures/MidlineMeasurementTool
 import FindMidpointMeasurementTool from '@/components/measures/FindMidpointMeasurementTool';
 import VerticalTiltMeasurementTool from '@/components/measures/VerticalTiltMeasurementTool';
 import HorizontalTiltMeasurementTool from '@/components/measures/HorizontalTiltMeasurementTool';
+import LineGrayscaleMeasurementTool from '@/components/measures/LineGrayscaleMeasurementTool';
 import DicomOverlayTool from '@/components/overlay/DicomOverlayTool';
 import ImageViewerErrorBoundary from './ImageViewerErrorBoundary';
 import { boolean } from 'zod';
@@ -243,6 +244,7 @@ function registerTools(viewportId, renderingEngineId) {
   toolGroup.addTool(DicomOverlayTool.toolName); // 添加DICOM四角信息显示工具
   toolGroup.addTool(PolygonLengthMeasurementTool.toolName); // 添加多边形长度测量工具
   toolGroup.addTool(PolylineLengthMeasurementTool.toolName); // 添加拆线长度测量工具
+  toolGroup.addTool(LineGrayscaleMeasurementTool.toolName); // 添加直线灰度测量工具
 
   // 设置默认工具状态
   setupDefaultToolStates(toolGroup);
@@ -308,6 +310,7 @@ function setupDefaultToolStates(toolGroup: cornerstoneTools.Types.IToolGroup) {
   toolGroup.setToolPassive(HorizontalTiltMeasurementTool.toolName);
   toolGroup.setToolPassive(PolygonLengthMeasurementTool.toolName);
   toolGroup.setToolPassive(PolylineLengthMeasurementTool.toolName);
+  toolGroup.setToolPassive(LineGrayscaleMeasurementTool.toolName);
 }
 export function addLMark(currentViewportId: string): void {
   // Implement the logic to add an L mark
@@ -1429,6 +1432,32 @@ export function clearPolylineLengthMeasurements(viewportId: string): boolean {
   console.log(`[clearPolylineLengthMeasurements] Clearing PolylineLength measurements for viewport: ${viewportId}`);
   return MeasurementToolManager.clearPolylineLengthMeasurements(viewportId);
 }
+// ==================== 直线灰度测量相关函数 ====================
+
+/**
+ * 激活直线灰度测量工具
+ */
+export function activateLineGrayscaleMeasurement(viewportId: string): boolean {
+  console.log(`[activateLineGrayscaleMeasurement] Activating LineGrayscale measurement for viewport: ${viewportId}`);
+  return MeasurementToolManager.activateLineGrayscaleMeasurementTool(viewportId);
+}
+
+/**
+ * 停用直线灰度测量工具
+ */
+export function deactivateLineGrayscaleMeasurement(viewportId: string): boolean {
+  console.log(`[deactivateLineGrayscaleMeasurement] Deactivating LineGrayscale measurement for viewport: ${viewportId}`);
+  return MeasurementToolManager.deactivateLineGrayscaleMeasurementTool(viewportId);
+}
+
+/**
+ * 清除直线灰度测量标注
+ */
+export function clearLineGrayscaleMeasurements(viewportId: string): boolean {
+  console.log(`[clearLineGrayscaleMeasurements] Clearing LineGrayscale measurements for viewport: ${viewportId}`);
+  return MeasurementToolManager.clearLineGrayscaleMeasurements(viewportId);
+}
+
 export class ImageLoadError extends Error {
   constructor(message: string, failedImageIds: string[]) {
     super(message);

+ 2 - 0
src/utils/cornerstoneToolsSetup.ts

@@ -17,6 +17,7 @@ import HorizontalTiltMeasurementTool from '@/components/measures/HorizontalTiltM
 import PolygonLengthMeasurementTool from '@/components/measures/PolygonLengthMeasurementTool';
 import DicomOverlayTool from '@/components/overlay/DicomOverlayTool';
 import PolylineLengthMeasurementTool from '@/components/measures/PolylineLengthMeasurementTool';
+import LineGrayscaleMeasurementTool from '@/components/measures/LineGrayscaleMeasurementTool';
 
 const {
   MagnifyTool,
@@ -73,6 +74,7 @@ export function registerGlobalTools(): void {
     cornerstoneTools.addTool(HorizontalTiltMeasurementTool); // 添加直线水平倾斜度测量工具
     cornerstoneTools.addTool(PolygonLengthMeasurementTool); // 添加多边形长度测量工具
     cornerstoneTools.addTool(PolylineLengthMeasurementTool); // 添加拆线长度测量工具
+    cornerstoneTools.addTool(LineGrayscaleMeasurementTool); // 添加直线灰度测量工具
     cornerstoneTools.addTool(DicomOverlayTool); // 添加DICOM四角信息显示工具
 
     toolsRegistered = true;

+ 133 - 0
src/utils/measurementToolManager.ts

@@ -17,6 +17,7 @@ import VerticalTiltMeasurementTool from '@/components/measures/VerticalTiltMeasu
 import HorizontalTiltMeasurementTool from '@/components/measures/HorizontalTiltMeasurementTool';
 import PolygonLengthMeasurementTool from '@/components/measures/PolygonLengthMeasurementTool';
 import PolylineLengthMeasurementTool from '@/components/measures/PolylineLengthMeasurementTool';
+import LineGrayscaleMeasurementTool from '@/components/measures/LineGrayscaleMeasurementTool';
 
 const {
   ToolGroupManager,
@@ -3418,4 +3419,136 @@ viewport.render();
   static clearPolylineLengthMeasurementsForViewports(viewportIds: string[]): boolean[] {
     return viewportIds.map((viewportId) => this.clearPolylineLengthMeasurements(viewportId));
   }
+
+  // ==================== LineGrayscaleMeasurement(直线灰度测量)工具 ====================
+
+  /**
+   * 激活直线灰度测量工具
+   */
+  static activateLineGrayscaleMeasurementTool(viewportId: string): boolean {
+    const toolGroup = this.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      // 停用其他可能冲突的工具
+      toolGroup.setToolPassive(WindowLevelTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(MagnifyTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(LengthTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(AngleTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(TibialPlateauAngleTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(DARAMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipDIMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipNHAAngleMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(VHSMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(TPLOMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(TTAMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(CBLOMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipCoverageMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipDorsalCoverageTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(CircleCenterMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(MidlineMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(FindMidpointMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(VerticalTiltMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HorizontalTiltMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(PolygonLengthMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(PolylineLengthMeasurementTool.toolName, { removeAllBindings: true });
+
+      // 激活直线灰度测量工具
+      toolGroup.setToolActive(LineGrayscaleMeasurementTool.toolName, {
+        bindings: [{ mouseButton: MouseBindings.Primary }],
+      });
+
+      // 获取工具实例并激活修改模式
+      const toolInstance = toolGroup.getToolInstance(LineGrayscaleMeasurementTool.toolName) as LineGrayscaleMeasurementTool;
+      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) {
+            const toolInstance = toolGroup.getToolInstance(LineGrayscaleMeasurementTool.toolName) as LineGrayscaleMeasurementTool;
+            if (toolInstance && '_updateCachedStats' in toolInstance) {
+              (toolInstance as any)._updateCachedStats(defaultAnnotation, enabledElement);
+            }
+          }
+
+          viewport.render();
+          console.log('[MeasurementToolManager] Default LineGrayscale annotation created successfully');
+        }
+      } catch (error) {
+        console.error('[MeasurementToolManager] Failed to create default LineGrayscale annotation:', error);
+      }
+
+      console.log(`[MeasurementToolManager] LineGrayscaleMeasurement tool activated for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error activating LineGrayscaleMeasurement 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 });
+      console.log(`[MeasurementToolManager] LineGrayscaleMeasurement tool deactivated for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error deactivating LineGrayscaleMeasurement tool:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 清除指定 viewport 的所有直线灰度测量标注
+   */
+  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 for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error clearing LineGrayscale measurements:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 为多个 viewport 批量清除直线灰度测量
+   */
+  static clearLineGrayscaleMeasurementsForViewports(viewportIds: string[]): boolean[] {
+    return viewportIds.map((viewportId) => this.clearLineGrayscaleMeasurements(viewportId));
+  }
 }