当前位置: 首页 > news >正文

基于cornerstone3D的dicom影像浏览器 第三十章 心胸比例测量工具CTRTool

文章目录

  • 前言
  • 一、实现过程
    • 1. 学习CobbAngleTool源码
    • 2. 新建CTRTool.js文件
    • 3. 重写constructor函数
    • 4. 重写defaultGetTextLines函数
    • 5. 增加_calculateLength函数
    • 6. 重写_calculateCachedStats函数
    • 7. 重写renderAnnotation函数
  • 二、使用步骤
    • 1.引入库
    • 2. 添加到cornerstoneTools
    • 3. 添加到toolGroup
  • 总结


前言

在cornerstone3D中找了找,没有找到测量心胸比例的工具,观察CobbAngleTool,已经有两条直线,但是显示的文字是两直线的夹角,可以从CobbAngleTool派生,只需重写renderAnnotation函数,显示每条直线的长度及两条直线的比值即可。
本章实现心胸比例测量工具CTRTool,效果如下:
在这里插入图片描述


一、实现过程

1. 学习CobbAngleTool源码

  1. 源码位置:packages\tools\src\tools\annotation\CobbAngleTool.ts
  2. 确定两个需要重写的函数constructor,renderAnnotation
  3. 从constructor,renderAnnotation两个函数中找出所有需要重写的函数以及需要导入的库。
    尤其是导入库,因为源码中都是从相对路径导入,查找比较费劲,我已整理如下:

1)需要重写或新增加的函数
因为CobbAngleTool中不需要计算直线长度,所以要新增一个函数_calculateLength来计算直线长度

constructor
renderAnnotation
defaultGetTextLines
_throttledCalculateCachedStats
_calculateCachedStats
_calculateLength

2)导入库
其中还有一个函数midPoint2没找到导入方法,直接把函数拷贝过来。

import { vec3 } from "gl-matrix";
import * as cornerstoneTools from "@cornerstonejs/tools";
import { utilities as csUtils } from "@cornerstonejs/core";const { Enums: csToolsEnums, CobbAngleTool, annotation, drawing, utilities } = cornerstoneTools;const { transformWorldToIndex } = csUtils;
const { ChangeTypes } = csToolsEnums;
const { getAnnotations, triggerAnnotationModified } = annotation.state;
const { isAnnotationLocked } = annotation.locking;
const { isAnnotationVisible } = annotation.visibility;
const {drawHandles: drawHandlesSvg,drawTextBox: drawTextBoxSvg,drawLine: drawLineSvg,drawLinkedTextBox: drawLinkedTextBoxSvg
} = drawing;const { getCalibratedLengthUnitsAndScale, throttle } = utilities;
const { getTextBoxCoordsCanvas } = utilities.drawing;const midPoint2 = (...args) => {const ret = args[0].length === 2 ? [0, 0] : [0, 0, 0];const len = args.length;for (const arg of args) {ret[0] += arg[0] / len;ret[1] += arg[1] / len;if (ret.length === 3) {ret[2] += arg[2] / len;}}return ret;
};

2. 新建CTRTool.js文件

从CobbAngleTool派生CTRTool类,toolName取为"CardiothoracicRatio"

class CTRTool extends CobbAngleTool {static toolName = "CardiothoracicRatio";
}

3. 重写constructor函数

修改三处:

  1. 设置配置项中的getTextLines为重写的defaultGetTextLines函数,就可以获取我们想要显示的两条直线长度之比。
  2. 增加配置项showLinesText,用来控制是否显示每条直线的长度。
  3. 为主计算函数_calculateCachedStats生成节流函数
  4. 代码如下,注意注释中标有如:“修改1” 的地方
class CTRTool extends CobbAngleTool {static toolName = "CardiothoracicRatio";constructor(toolProps = {},defaultToolProps = {supportedInteractionTypes: ["Mouse", "Touch"],configuration: {shadow: true,preventHandleOutsideImage: false,getTextLines: defaultGetTextLines, // 修改1showLinesText: true                // 修改2}}) {super(toolProps, defaultToolProps);// 修改3this._throttledCalculateCachedStats = throttle(this._calculateCachedStats, 25, {trailing: true});}
}

4. 重写defaultGetTextLines函数

单独函数,不属于CTRTool类
从cachedStates中找到自定义的ratio生成要显示的文字返回

function defaultGetTextLines(data, targetId) {const cachedVolumeStats = data.cachedStats[targetId];const { ratio } = cachedVolumeStats;if (ratio === undefined) {return;}const textLines = [`${ratio.toFixed(2)}`];return textLines;
}

5. 增加_calculateLength函数

用来计算直线长度。

_calculateLength(pos1, pos2) {const dx = pos1[0] - pos2[0];const dy = pos1[1] - pos2[1];const dz = pos1[2] - pos2[2];return Math.sqrt(dx * dx + dy * dy + dz * dz);
}

6. 重写_calculateCachedStats函数

从源码拷贝原函数,修改处做了注释。
重点:
CobbAngleTool的cachedStats结构:

{angle: null,arc1Angle: null,arc2Angle: null,points: {world: {arc1Start: null,arc1End: null,arc2Start: null,arc2End: null,},canvas: {arc1Start: null,arc1End: null,arc2Start: null,arc2End: null,}}
};

CTRTool的cachedStats结构:
其中如arc1Start,arc1End等属性名可以改为如line1Start,line1End。本文就不改了。

{length1: null,length2: null,unit: null,ratio: null,points: {world: {arc1Start: null,arc1End: null,arc2Start: null,arc2End: null},canvas: {arc1Start: null,arc1End: null,arc2Start: null,arc2End: null}}
};

完成后代码:

_calculateCachedStats(annotation, renderingEngine, enabledElement) {const data = annotation.data;// Until we have all four anchors bail outif (data.handles.points.length !== 4) {return;}const seg1 = [null, null];const seg2 = [null, null];let minDist = Number.MAX_VALUE;// Order the endpoints of each line segment such that seg1[1] and seg2[0]// are the closest (Euclidean distance-wise) to each other. Thus// the angle formed between the vectors seg1[1]->seg1[0] and seg2[0]->seg[1]// is calculated.// The assumption here is that the Cobb angle line segments are drawn// such that the segments intersect nearest the segment endpoints// that are closest AND those closest endpoints are the tails of the// vectors used to calculate the angle between the vectors/line segments.for (let i = 0; i < 2; i += 1) {for (let j = 2; j < 4; j += 1) {const dist = vec3.distance(data.handles.points[i], data.handles.points[j]);if (dist < minDist) {minDist = dist;seg1[1] = data.handles.points[i];seg1[0] = data.handles.points[(i + 1) % 2];seg2[0] = data.handles.points[j];seg2[1] = data.handles.points[2 + ((j - 1) % 2)];}}}const { viewport } = enabledElement;const { element } = viewport;const canvasPoints = data.handles.points.map(p => viewport.worldToCanvas(p));const firstLine = [canvasPoints[0], canvasPoints[1]];const secondLine = [canvasPoints[2], canvasPoints[3]];const mid1 = midPoint2(firstLine[0], firstLine[1]);const mid2 = midPoint2(secondLine[0], secondLine[1]);const { arc1Start, arc1End, arc2End, arc2Start } =this.getArcsStartEndPoints({firstLine,secondLine,mid1,mid2});// 新增,两条直线世界坐标,用来计算长度const wdArc1Start = data.handles.points[0];const wdArc1End = data.handles.points[1];const wdArc2Start = data.handles.points[2];const wdArc2End = data.handles.points[3];const { cachedStats } = data;const targetIds = Object.keys(cachedStats);for (let i = 0; i < targetIds.length; i++) {const targetId = targetIds[i];// 新增,计算两条直线长度,获取长度单位,计算两直线比例const image = this.getTargetImageData(targetId);if (!image) {continue;}const { imageData } = image;let index1 = transformWorldToIndex(imageData, wdArc1Start);let index2 = transformWorldToIndex(imageData, wdArc1End);let handles = [index1, index2];const len1 = getCalibratedLengthUnitsAndScale(image, handles);const length1 = this._calculateLength(wdArc1Start, wdArc1End) / len1.scale;index1 = transformWorldToIndex(imageData, wdArc2Start);index2 = transformWorldToIndex(imageData, wdArc2End);handles = [index1, index2];const { scale, unit } = getCalibratedLengthUnitsAndScale(image, handles);const length2 = this._calculateLength(wdArc2Start, wdArc2End) / scale;// 计算两直线比例const ratio = length1 / length2;/cachedStats[targetId] = {length1,length2,unit,ratio,points: {canvas: {arc1Start,arc1End,arc2End,arc2Start},world: {arc1Start: viewport.canvasToWorld(arc1Start),arc1End: viewport.canvasToWorld(arc1End),arc2End: viewport.canvasToWorld(arc2End),arc2Start: viewport.canvasToWorld(arc2Start)}}};}const invalidated = annotation.invalidated;annotation.invalidated = false;// Dispatching annotation modified only if it was invalidatedif (invalidated) {triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);}return cachedStats;}

7. 重写renderAnnotation函数

修改处标有注释 “修改…”

renderAnnotation = (enabledElement, svgDrawingHelper) => {let renderStatus = false;const { viewport } = enabledElement;const { element } = viewport;let annotations = getAnnotations(this.getToolName(), element);// Todo: We don't need this anymore, filtering happens in triggerAnnotationRenderif (!annotations?.length) {return renderStatus;}annotations = this.filterInteractableAnnotationsForElement(element, annotations);if (!annotations?.length) {return renderStatus;}const targetId = this.getTargetId(viewport);const renderingEngine = viewport.getRenderingEngine();const styleSpecifier = {toolGroupId: this.toolGroupId,toolName: this.getToolName(),viewportId: enabledElement.viewport.id};// Draw SVGfor (let i = 0; i < annotations.length; i++) {const annotation = annotations[i];const { annotationUID, data } = annotation;const { points, activeHandleIndex } = data.handles;styleSpecifier.annotationUID = annotationUID;const { color, lineWidth, lineDash } = this.getAnnotationStyle({annotation,styleSpecifier});const canvasCoordinates = points.map(p => viewport.worldToCanvas(p));// WE HAVE TO CACHE STATS BEFORE FETCHING TEXTif (!data.cachedStats[targetId] || data.cachedStats[targetId].ratio == null) {data.cachedStats[targetId] = {length1: null,length2: null,unit: null,ratio: null,points: {world: {arc1Start: null,arc1End: null,arc2Start: null,arc2End: null},canvas: {arc1Start: null,arc1End: null,arc2Start: null,arc2End: null}}};this._calculateCachedStats(annotation, renderingEngine, enabledElement);} else if (annotation.invalidated) {this._throttledCalculateCachedStats(annotation, renderingEngine, enabledElement);}let activeHandleCanvasCoords;if (!isAnnotationLocked(annotationUID) &&!this.editData &&activeHandleIndex !== null) {// Not locked or creating and hovering over handle, so render handle.activeHandleCanvasCoords = [canvasCoordinates[activeHandleIndex]];}// If rendering engine has been destroyed while renderingif (!viewport.getRenderingEngine()) {console.warn("Rendering Engine has been destroyed");return renderStatus;}if (!isAnnotationVisible(annotationUID)) {continue;}if (activeHandleCanvasCoords) {const handleGroupUID = "0";drawHandlesSvg(svgDrawingHelper, annotationUID, handleGroupUID, canvasCoordinates, {color,lineDash,lineWidth});}const firstLine = [canvasCoordinates[0], canvasCoordinates[1]];const secondLine = [canvasCoordinates[2], canvasCoordinates[3]];let lineUID = "line1";drawLineSvg(svgDrawingHelper, annotationUID, lineUID, firstLine[0], firstLine[1], {color,width: lineWidth,lineDash});renderStatus = true;// Don't add the stats until annotation has 4 anchor pointsif (canvasCoordinates.length < 4) {return renderStatus;}lineUID = "line2";drawLineSvg(svgDrawingHelper, annotationUID, lineUID, secondLine[0], secondLine[1], {color,width: lineWidth,lineDash});lineUID = "linkLine";const mid1 = midPoint2(firstLine[0], firstLine[1]);const mid2 = midPoint2(secondLine[0], secondLine[1]);drawLineSvg(svgDrawingHelper, annotationUID, lineUID, mid1, mid2, {color,lineWidth: "1",lineDash: "1,4"});// Calculating the arcsconst { arc1Start, arc1End, arc2End, arc2Start } =data.cachedStats[targetId].points.canvas;const { length1, length2, unit } = data.cachedStats[targetId];if (!data.cachedStats[targetId]?.ratio) {continue;}const options = this.getLinkedTextBoxStyle(styleSpecifier, annotation);if (!options.visibility) {data.handles.textBox = {hasMoved: false,worldPosition,worldBoundingBox: {topLeft,topRight,bottomLeft,bottomRight}};continue;}const textLines = this.configuration.getTextLines(data, targetId);if (!data.handles.textBox.hasMoved) {const canvasTextBoxCoords = getTextBoxCoordsCanvas(canvasCoordinates);data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);}// 修改,绘制主文本,两直线之比const textBoxPosition = viewport.worldToCanvas(data.handles.textBox.worldPosition);textBoxPosition[1] -= 50;const textBoxUID = "ctrRatioText";const boundingBox = drawLinkedTextBoxSvg(svgDrawingHelper,annotationUID,textBoxUID,textLines,textBoxPosition,canvasCoordinates,{},options);const { x: left, y: top, width, height } = boundingBox;data.handles.textBox.worldBoundingBox = {topLeft: viewport.canvasToWorld([left, top]),topRight: viewport.canvasToWorld([left + width, top]),bottomLeft: viewport.canvasToWorld([left, top + height]),bottomRight: viewport.canvasToWorld([left + width, top + height])};if (this.configuration.showLinesText) {// 修改,绘制直线1长度const arc1TextBoxUID = "lineText1";const arc1TextLine = [`${length1.toFixed(2)} ${unit}`];const arch1TextPosCanvas = midPoint2(arc1Start, arc1End);arch1TextPosCanvas[0] -= 30;arch1TextPosCanvas[1] = arc1Start[1] - 24;drawTextBoxSvg(svgDrawingHelper,annotationUID,arc1TextBoxUID,arc1TextLine,arch1TextPosCanvas,{...options,padding: 3});// 修改,绘制直线2长度const arc2TextBoxUID = "lineText2";const arc2TextLine = [`${length2.toFixed(2)} ${unit}`];const arch2TextPosCanvas = midPoint2(arc2Start, arc2End);arch2TextPosCanvas[0] -= 30;arch2TextPosCanvas[1] = arc2Start[1] - 24;drawTextBoxSvg(svgDrawingHelper,annotationUID,arc2TextBoxUID,arc2TextLine,arch2TextPosCanvas,{...options,padding: 3});}}return renderStatus;
};

二、使用步骤

与添加cornerstoneTool中的工具流程一样。

1.引入库

import CTRTool from "./CTRTool";

2. 添加到cornerstoneTools

cornerstoneTools.addTool(CTRTool);

3. 添加到toolGroup

toolGroup.addTool(CTRTool.toolName, {showLinesText: true
});

总结

本章实现心胸比例测量工具CTRTool。
展示了从cornerstonejs库中派生自定义类的过程

相关文章:

  • 免费批量Markdown转Word工具
  • 单线程模型中消息机制解析
  • C++ OpenCV 学习路线图
  • CAD多面体密堆积3D插件
  • 数据库入门:从原理到应用
  • 我用Cursor写了一个视频转文字工具,已开源,欢迎体验
  • 深入理解 React Hooks
  • 基于SpringBoot利用死信队列解决RabbitMQ业务队列故障重试无效场景问题
  • bugku 网络安全事件应急响应
  • Git配置代理
  • SCFSlRAE1通过调节SlWRKY1的稳定性来调控番茄对灰霉菌的抗性。
  • 自然语言处理——语言模型
  • jieba实现和用RNN实现中文分词的区别
  • 拼多多官方内部版 7.58.0 | 极限精简,只有2.5M
  • ASM,LVM,扫描并扩容步骤-linux
  • JAVA反序列化应用 : URLDNS案例
  • 基于 React Native for HarmonyOS5 的跨平台组件库开发指南,以及组件示例
  • 【Go语言基础【20】】Go的包与工程
  • 【Go语言基础【19】】接口:灵活实现多态的核心机制
  • 《Go小技巧易错点100例》第三十五篇
  • 保险代理人做网站/中国新闻网
  • 珠海建站程序/关键词搜索引擎工具爱站
  • 真人真做网站/广东全网推广
  • 建设工程规划许可证公示网站/seo学院
  • 多php网站建设/做网站找哪个公司好
  • 广州高端品牌网站建设/网店营销策略有哪些