TibialPlateauAngleTool.README.md 20 KB

TibialPlateauAngleTool 胫骨平台夹角 TPA 测量工具文档

一、业务需求分析

1.1 测量目的

胫骨平台角(TPA)的测量目的是评估狗狗膝关节胫骨平台的倾斜程度,从而判断其前交叉韧带受伤风险。

1.2 点定义

点编号 作用 位置说明
1、2 生成胫骨内侧平台线 连接点1→点2形成平台线
3、4 生成胫骨功能轴线 连接点3→点4形成功能轴线

关键线段定义:

  • 胫骨内侧平台线: 点1→点2的连线
  • 胫骨功能轴线: 点3→点4的连线
  • 交汇点: 胫骨内侧平台线与胫骨功能轴线延长后的交点(即两条线的数学交点)
  • 平台线中垂线: 从交汇点出发,垂直于胫骨内侧平台线的直线

1.3 角度计算公式

TPA角度 = 胫骨内侧平台线(1→2)与胫骨功能轴线(3→4)的垂线夹角

具体计算步骤:

  1. 计算胫骨内侧平台线向量: plateauVector = 点2 - 点1
  2. 计算胫骨功能轴线向量: functionalAxisVector = 点4 - 点3
  3. 计算平台线的垂线向量: perpendicularVector = (-plateauVector.y, plateauVector.x, 0) (2D平面垂直向量)
  4. 计算功能轴线与垂线的夹角: TPA = angle(functionalAxisVector, perpendicularVector)

1.4 交互规则

  • 4个可交互控制点: 用户可拖动4个点以实时更新所有线段和TPA值
  • 实时更新: 所有变化均即时刷新显示
  • TPA数值显示:
    • 黄色标注
    • 可自由拖动调整显示位置
  • 选中状态:
    • 所有线段以蓝色显示
    • 控制点以白色圆圈显示
  • 非选中状态:
    • 线段以白色显示
    • 点仍为白色圆圈

二、技术原理与使用的 Cornerstone3D API

2.1 核心继承关系

class TibialPlateauAngleTool extends AnnotationTool
  • 继承自 @cornerstonejs/toolsAnnotationTool 基类
  • 获得注解工具的基础功能:创建、编辑、渲染、状态管理

2.2 使用的主要 Cornerstone3D API

核心模块 (@cornerstonejs/core)

import { getEnabledElement, Types as CoreTypes } from '@cornerstonejs/core';
API 用途
getEnabledElement(element) 获取启用的element及其viewport
viewport.worldToCanvas(point) 将世界坐标转换为canvas坐标(用于渲染)
viewport.canvasToWorld(point) 将canvas坐标转换为世界坐标(用于存储)
viewport.getCamera() 获取相机参数(viewPlaneNormal, viewUp)
viewport.getCurrentImageId() 获取当前图像ID
viewport.getFrameOfReferenceUID() 获取参考坐标系UID

工具模块 (@cornerstonejs/tools)

import { 
  AnnotationTool, 
  utilities, 
  annotation, 
  drawing 
} from '@cornerstonejs/tools';
API/模块 用途
annotation.state.addAnnotation() 将注解添加到状态管理
annotation.state.getAnnotations() 获取指定工具的所有注解
utilities.viewportFilters.getViewportIdsWithToolToRender() 获取需要渲染的viewport ID列表
utilities.triggerAnnotationRenderForViewportIds() 触发指定viewport的渲染更新
drawing.drawLine() 绘制线段
drawing.drawPath() 绘制路径(用于弧线)
drawing.drawHandles() 绘制可拖拽的手柄点
drawing.drawLinkedTextBox() 绘制连接到点的文本框

事件系统

// 鼠标事件监听
element.addEventListener('CORNERSTONE_TOOLS_MOUSE_DOWN', callback);
element.addEventListener('CORNERSTONE_TOOLS_MOUSE_DRAG', callback);
element.addEventListener('CORNERSTONE_TOOLS_MOUSE_UP', callback);

2.3 关键技术实现

1. 坐标系统转换

  • 世界坐标系(World Coordinates): 3D空间中的绝对坐标,用于存储
  • Canvas坐标系(Canvas Coordinates): 2D屏幕坐标,用于渲染和交互
// 存储时:canvas → world
const worldPoint = viewport.canvasToWorld(canvasPoint);

// 渲染时:world → canvas
const canvasPoint = viewport.worldToCanvas(worldPoint);

2. 注解数据结构

interface TibialPlateauAngleAnnotation {
  metadata: {
    viewPlaneNormal: Point3;      // 视图平面法向量
    viewUp: Point3;                // 视图向上向量
    FrameOfReferenceUID: string;  // 参考坐标系
    referencedImageId: string;    // 图像ID
    toolName: string;              // 工具名称
  };
  data: {
    handles: {
      points: Point3[];            // 4个世界坐标点
      activeHandleIndex: number | null;  // 当前活动的手柄索引
      textBoxTPA?: Point2;         // TPA角度文本框位置(canvas坐标)
    };
    cachedStats: {
      [targetId: string]: {
        TPA: number;                      // TPA角度值
        functionalAxisVector: Point3;     // 功能轴线向量(点3→点4)
        plateauLineVector: Point3;        // 平台线向量(点1→点2)
        perpendicularVector: Point3;      // 平台线的垂线向量
        plateauMidpoint: Point3;          // 平台线中点
        intersectionPoint?: Point3;       // 功能轴线与中垂线的交点
      };
    };
  };
}

3. 交互模式

  • 修改模式(Modify Mode): _activateModify() - 用于编辑现有注解
    • 监听鼠标按下、拖拽、释放事件
    • 支持拖拽手柄点和文本框

4. 几何计算步骤

// Step 1: 计算胫骨内侧平台线向量(点1→点2)
const plateauLineVector = vectorSubtract(point2, point1);

// Step 2: 计算胫骨功能轴线向量(点3→点4)
const functionalAxisVector = vectorSubtract(point4, point3);

// Step 3: 计算平台线与功能轴线的交点(作为中垂线的起点)
const intersectionPoint = getLineIntersection(
  point1,                  // 平台线起点
  plateauLineVector,       // 平台线方向
  point3,                  // 功能轴线起点
  functionalAxisVector     // 功能轴线方向
);

// Step 4: 计算平台线的中垂线向量
// 在2D平面上,向量(x,y)的逆时针垂直向量为(-y,x)
// 这条垂线从交点出发,垂直于平台线
const perpendicularVector: Point3 = [
  -plateauLineVector[1],
  plateauLineVector[0],
  0
];

// Step 5: 计算TPA角度(功能轴线与平台线中垂线的夹角)
const angleRad = getAngleBetweenLines(functionalAxisVector, perpendicularVector);
const TPA = Math.round(degrees(angleRad));

5. 可视化元素

  1. 胫骨内侧平台线(点1→点2): 白色/蓝色实线(选中时)
  2. 胫骨功能轴线(点3→点4): 白色/蓝色实线(选中时)
  3. 平台线延长虚线: 灰色虚线,从平台线的较近端点经过交汇点继续延伸50像素
  4. 功能轴线延长虚线: 灰色虚线,从功能轴线的较近端点经过交汇点继续延伸50像素
  5. 交汇点: 延长虚线的交汇处,即平台线与功能轴线的数学交点
  6. 平台线中垂线: 绿色虚线,从交汇点向两端延伸,垂直于平台线
  7. 角度弧线: 黄色,在交汇点处绘制,表示TPA角度
  8. TPA文本: 黄色,可拖拽,显示角度值
  9. 手柄点: 4个白色圆圈,可拖拽

延长线实现细节:

  • 延长线起点: 自动选择离交汇点较近的端点作为延长线起点
  • 延长逻辑:
    1. 计算从端点到交汇点的方向向量
    2. 归一化该方向向量
    3. 从交汇点沿该方向继续延伸额外的50像素
  • 视觉效果: 延长线从端点穿过交汇点后继续延伸,使两条线的交汇更加清晰明显

三、开发人员使用指南

3.1 工具注册与激活

步骤1: 导入并注册工具

import TibialPlateauAngleTool from './TibialPlateauAngleTool';
import { ToolGroupManager } from '@cornerstonejs/tools';

// 注册工具
cornerstoneTools.addTool(TibialPlateauAngleTool);

步骤2: 创建工具组并添加工具

const toolGroupId = 'myToolGroup';
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId);

// 添加工具到工具组
toolGroup.addTool(TibialPlateauAngleTool.toolName);

// 将工具组关联到viewport
toolGroup.addViewport(viewportId, renderingEngineId);

步骤3: 激活工具

// 激活工具
toolGroup.setToolActive(TibialPlateauAngleTool.toolName, {
  bindings: [{ mouseButton: MouseBindings.Primary }],
});

// 激活修改模式(允许拖拽)
const toolInstance = toolGroup.getToolInstance(
  TibialPlateauAngleTool.toolName
) as TibialPlateauAngleTool;

if (toolInstance && elementRef.current) {
  toolInstance._activateModify(elementRef.current);
}

3.2 创建注解(输入)

方式1: 使用默认配置

const annotation = TibialPlateauAngleTool.createDefaultAnnotation(
  element,
  viewport
);

方式2: 使用自定义配置

const config = {
  axisLineLength: 200,        // 功能轴线长度(px)
  plateauLineLength: 150,     // 平台线长度(px)
  axisLineCenterX: 400,       // 功能轴线中心X坐标
  axisLineCenterY: 300,       // 功能轴线中心Y坐标
  plateauLineCenterX: 600,    // 平台线中心X坐标
  plateauLineCenterY: 300,    // 平台线中心Y坐标
};

const annotation = TibialPlateauAngleTool.createDefaultAnnotation(
  element,
  viewport,
  config
);

配置参数说明

参数 类型 默认值 说明
axisLineLength number 200 胫骨功能轴线长度(像素)
plateauLineLength number 150 胫骨内侧平台线长度(像素)
axisLineCenterX number centerX - 100 功能轴线中心点X坐标
axisLineCenterY number centerY 功能轴线中心点Y坐标
plateauLineCenterX number centerX + 100 平台线中心点X坐标
plateauLineCenterY number centerY 平台线中心点Y坐标

默认布局: 功能轴线在左侧(垂直),平台线在右侧(水平)

将注解添加到状态管理

// 添加注解
cornerstoneTools.annotation.state.addAnnotation(annotation, element);

// 计算统计数据
const enabledElement = cornerstone.getEnabledElement(element);
if (enabledElement) {
  toolInstance._updateCachedStats(annotation, enabledElement);
}

// 触发渲染
const viewportIdsToRender = 
  cornerstoneTools.utilities.viewportFilters.getViewportIdsWithToolToRender(
    element,
    TibialPlateauAngleTool.toolName
  );
cornerstoneTools.utilities.triggerAnnotationRenderForViewportIds(
  viewportIdsToRender
);

3.3 导出注解数据(输出)

导出数据结构

export interface ExportedAnnotationData {
  // 4个点的世界坐标
  points: Point3[];

  // TPA角度值(度数)
  TPA: number;

  // 文本框位置(可选,canvas坐标)
  textBoxPosition?: Point2;

  // 元数据
  metadata: {
    viewPlaneNormal: Point3;
    viewUp: Point3;
    FrameOfReferenceUID: string;
    referencedImageId: string;
  };
}

导出方法

// 获取注解
const annotations = cornerstoneTools.annotation.state.getAnnotations(
  TibialPlateauAngleTool.toolName,
  element
);

// 导出第一个注解的数据
if (annotations && annotations.length > 0) {
  const exportedData = TibialPlateauAngleTool.exportAnnotationData(
    annotations[0],
    viewport
  );

  if (exportedData) {
    // 保存数据(例如发送到服务器或保存到localStorage)
    console.log('导出的数据:', exportedData);

    // 获取TPA角度值
    console.log('TPA角度:', exportedData.TPA, '度');

    // 获取4个控制点的世界坐标
    console.log('点1(平台线起点):', exportedData.points[0]);
    console.log('点2(平台线终点):', exportedData.points[1]);
    console.log('点3(功能轴线起点):', exportedData.points[2]);
    console.log('点4(功能轴线终点):', exportedData.points[3]);

    // 获取文本框位置
    if (exportedData.textBoxPosition) {
      console.log('TPA文本框位置:', exportedData.textBoxPosition);
    }
  } else {
    console.log('注解数据未计算完成,无法导出');
  }
}

3.4 恢复注解数据

// 从保存的数据恢复注解
const restoredAnnotation = TibialPlateauAngleTool.restoreFromExportedData(
  exportedData,
  element,
  viewport
);

// 添加到状态
cornerstoneTools.annotation.state.addAnnotation(restoredAnnotation, element);

// 重新计算统计数据
const enabledElement = cornerstone.getEnabledElement(element);
if (enabledElement) {
  const toolInstance = toolGroup.getToolInstance(
    TibialPlateauAngleTool.toolName
  ) as TibialPlateauAngleTool;
  
  if (toolInstance) {
    toolInstance._updateCachedStats(restoredAnnotation, enabledElement);
  }
}

// 触发渲染
const viewportIdsToRender = 
  cornerstoneTools.utilities.viewportFilters.getViewportIdsWithToolToRender(
    element,
    TibialPlateauAngleTool.toolName
  );
cornerstoneTools.utilities.triggerAnnotationRenderForViewportIds(
  viewportIdsToRender
);

3.5 完整使用示例

import React, { useEffect, useRef, useState } from 'react';
import * as cornerstone from '@cornerstonejs/core';
import * as cornerstoneTools from '@cornerstonejs/tools';
import TibialPlateauAngleTool from './TibialPlateauAngleTool';

const TPAMeasurementComponent = () => {
  const elementRef = useRef<HTMLDivElement>(null);
  const [tpaValue, setTpaValue] = useState<number | null>(null);

  useEffect(() => {
    const setup = async () => {
      // 1. 初始化
      await cornerstone.init();
      await cornerstoneTools.init();
      
      // 2. 注册工具
      cornerstoneTools.addTool(TibialPlateauAngleTool);
      
      // 3. 创建rendering engine和viewport
      const renderingEngine = new cornerstone.RenderingEngine('myEngine');
      const viewportInput = {
        viewportId: 'myViewport',
        element: elementRef.current,
        type: cornerstone.Enums.ViewportType.STACK,
      };
      renderingEngine.enableElement(viewportInput);
      
      // 4. 加载图像...
      
      // 5. 创建工具组
      const toolGroup = cornerstoneTools.ToolGroupManager.createToolGroup('myToolGroup');
      toolGroup.addTool(TibialPlateauAngleTool.toolName);
      toolGroup.addViewport('myViewport', 'myEngine');
      
      // 6. 激活工具
      toolGroup.setToolActive(TibialPlateauAngleTool.toolName, {
        bindings: [{ mouseButton: cornerstoneTools.Enums.MouseBindings.Primary }],
      });
      
      // 7. 创建预设注解
      const viewport = renderingEngine.getViewport('myViewport');
      const annotation = TibialPlateauAngleTool.createDefaultAnnotation(
        elementRef.current,
        viewport,
        {
          axisLineLength: 200,
          plateauLineLength: 150,
        }
      );
      
      // 8. 添加注解
      cornerstoneTools.annotation.state.addAnnotation(annotation, elementRef.current);
      
      // 9. 计算统计数据
      const enabledElement = cornerstone.getEnabledElement(elementRef.current);
      const toolInstance = toolGroup.getToolInstance(
        TibialPlateauAngleTool.toolName
      ) as TibialPlateauAngleTool;
      
      if (enabledElement && toolInstance) {
        toolInstance._updateCachedStats(annotation, enabledElement);
        toolInstance._activateModify(elementRef.current);
      }
      
      // 10. 触发渲染
      const viewportIdsToRender = 
        cornerstoneTools.utilities.viewportFilters.getViewportIdsWithToolToRender(
          elementRef.current,
          TibialPlateauAngleTool.toolName
        );
      cornerstoneTools.utilities.triggerAnnotationRenderForViewportIds(
        viewportIdsToRender
      );
    };
    
    setup();
  }, []);

  // 导出TPA数据
  const handleExportTPA = () => {
    const annotations = cornerstoneTools.annotation.state.getAnnotations(
      TibialPlateauAngleTool.toolName,
      elementRef.current
    );
    
    if (annotations && annotations.length > 0) {
      const renderingEngine = cornerstone.getRenderingEngine('myEngine');
      const viewport = renderingEngine.getViewport('myViewport');
      
      const data = TibialPlateauAngleTool.exportAnnotationData(
        annotations[0],
        viewport
      );
      
      if (data) {
        setTpaValue(data.TPA);
        console.log('TPA测量结果:', data);
        
        // 发送到服务器或其他处理
        // sendToServer(data);
      }
    }
  };

  return (
    <div>
      <div ref={elementRef} style={{ width: '100%', height: '500px' }} />
      <button onClick={handleExportTPA}>获取TPA值</button>
      {tpaValue !== null && (
        <div>当前TPA值: {tpaValue}°</div>
      )}
    </div>
  );
};

四、常见问题与解决方案

4.1 手柄点不能拖拽

原因: 未调用 _activateModify() 或事件监听器未注册

解决:

const toolInstance = toolGroup.getToolInstance(
  TibialPlateauAngleTool.toolName
) as TibialPlateauAngleTool;

if (toolInstance && elementRef.current) {
  toolInstance._activateModify(elementRef.current);
}

4.2 TPA文本框拖拽跳跃

原因: 未记录鼠标相对文本框的偏移量

解决: 已在实现中通过 textBoxOffset 解决,在mouseDown时记录偏移量,mouseDrag时应用

4.3 角度计算不符合预期

原因: 垂线向量计算错误或角度方向问题

检查:

  • 确保平台线的垂线向量计算正确: (-y, x, 0)
  • 验证向量方向是否符合业务需求
  • 检查角度计算函数的实现

4.4 中垂线不显示或位置错误

原因: 交点计算失败或延伸长度不够

解决:

  • 确认两条直线不平行(判断行列式非零)
  • 增加延伸长度使中垂线清晰可见
  • 检查归一化向量计算

4.5 渲染不更新

原因: 未触发渲染更新

解决:

utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);

五、技术要点总结

5.1 几何计算要点

  1. 交点计算: 首先计算平台线与功能轴线的交点,这是中垂线的起点
  2. 垂线向量: 2D平面上向量(x,y)的垂直向量为(-y,x),中垂线从交点出发垂直于平台线
  3. 角度计算: 使用 getAngleBetweenLines() 计算功能轴线与中垂线的夹角
  4. 向量归一化: 用于延长线段的绘制,确保延伸距离准确

5.2 可视化要点

  1. 实线: 功能轴线、平台线使用实线绘制(白色/蓝色)
  2. 灰色虚线: 平台线和功能轴线的延长线
    • 从各自较近的端点出发
    • 经过交汇点后继续延伸50像素
    • 清晰显示两线的交汇关系
  3. 绿色虚线: 平台线的中垂线,从交点延伸,垂直于平台线
  4. 角度弧线: 在交点处绘制,半径适中(如30px)
  5. 文本框: 黄色显示,支持拖拽,避免遮挡关键线段

延长线绘制技术细节:

// 1. 选择离交点较近的端点
const dist1 = distance(intersection, point1);
const dist2 = distance(intersection, point2);
const extensionStart = dist1 < dist2 ? point1 : point2;

// 2. 计算从端点到交点的方向向量并归一化
const directionVector = subtract(intersection, extensionStart);
const normalizedDir = normalize(directionVector);

// 3. 计算延长线的终点(交点后继续延伸50像素)
const extraExtensionLength = 50;
const extensionEnd = add(intersection, scale(normalizedDir, extraExtensionLength));

// 4. 绘制从端点到延长终点的完整虚线
drawLine(extensionStart, extensionEnd, { lineDash: '4,4' });

5.3 交互要点

  1. 手柄点识别: 检测鼠标与点的距离,设置合理的proximity阈值
  2. 文本框拖拽: 记录初始偏移量,避免跳跃
  3. 实时更新: 每次拖拽后立即调用 _updateCachedStats() 重新计算
  4. 事件管理: 正确注册和注销事件监听器

六、总结

TibialPlateauAngleTool展示了:

  1. 医学影像测量: 基于4个点计算胫骨平台角度
  2. 几何计算: 中点、垂线、交点、角度的综合运用
  3. 可视化设计: 实线+虚线组合,清晰表达测量逻辑
  4. 交互体验: 支持拖拽调整,实时反馈
  5. 数据持久化: 完整的导出/恢复机制

这个工具可用于:

  • 临床诊断: 评估犬类前交叉韧带风险
  • 术前规划: 确定手术方案
  • 疗效评估: 对比治疗前后的TPA变化
  • 教学研究: 演示胫骨平台角度测量方法