文章目录
- 前言
- 一、实现过程
-
- [1. 学习CobbAngleTool源码](#1. 学习CobbAngleTool源码)
- [2. 新建CTRTool.js文件](#2. 新建CTRTool.js文件)
- [3. 重写constructor函数](#3. 重写constructor函数)
- [4. 重写defaultGetTextLines函数](#4. 重写defaultGetTextLines函数)
- [5. 增加_calculateLength函数](#5. 增加_calculateLength函数)
- [6. 重写_calculateCachedStats函数](#6. 重写_calculateCachedStats函数)
- [7. 重写renderAnnotation函数](#7. 重写renderAnnotation函数)
- 二、使用步骤
-
- 1.引入库
- [2. 添加到cornerstoneTools](#2. 添加到cornerstoneTools)
- [3. 添加到toolGroup](#3. 添加到toolGroup)
- 总结
前言
在cornerstone3D中找了找,没有找到测量心胸比例的工具,观察CobbAngleTool,已经有两条直线,但是显示的文字是两直线的夹角,可以从CobbAngleTool派生,只需重写renderAnnotation函数,显示每条直线的长度及两条直线的比值即可。
本章实现心胸比例测量工具CTRTool,效果如下:
一、实现过程
1. 学习CobbAngleTool源码
- 源码位置:
packages\tools\src\tools\annotation\CobbAngleTool.ts
- 确定两个需要重写的函数constructor,renderAnnotation
- 从constructor,renderAnnotation两个函数中找出所有需要重写的函数以及需要导入的库。
尤其是导入库,因为源码中都是从相对路径导入,查找比较费劲,我已整理如下:
1)需要重写或新增加的函数
因为CobbAngleTool中不需要计算直线长度,所以要新增一个函数_calculateLength来计算直线长度
javascript
constructor
renderAnnotation
defaultGetTextLines
_throttledCalculateCachedStats
_calculateCachedStats
_calculateLength
2)导入库
其中还有一个函数midPoint2没找到导入方法,直接把函数拷贝过来。
javascript
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"
javascript
class CTRTool extends CobbAngleTool {
static toolName = "CardiothoracicRatio";
}
3. 重写constructor函数
修改三处:
- 设置配置项中的getTextLines为重写的defaultGetTextLines函数,就可以获取我们想要显示的两条直线长度之比。
- 增加配置项showLinesText,用来控制是否显示每条直线的长度。
- 为主计算函数_calculateCachedStats生成节流函数
- 代码如下,注意注释中标有如:"修改1" 的地方
javascript
class CTRTool extends CobbAngleTool {
static toolName = "CardiothoracicRatio";
constructor(
toolProps = {},
defaultToolProps = {
supportedInteractionTypes: ["Mouse", "Touch"],
configuration: {
shadow: true,
preventHandleOutsideImage: false,
getTextLines: defaultGetTextLines, // 修改1
showLinesText: true // 修改2
}
}
) {
super(toolProps, defaultToolProps);
// 修改3
this._throttledCalculateCachedStats = throttle(this._calculateCachedStats, 25, {
trailing: true
});
}
}
4. 重写defaultGetTextLines函数
单独函数,不属于CTRTool类
从cachedStates中找到自定义的ratio生成要显示的文字返回
javascript
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函数
用来计算直线长度。
javascript
_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结构:
javascript
{
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。本文就不改了。
javascript
{
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
}
}
};
完成后代码:
javascript
_calculateCachedStats(annotation, renderingEngine, enabledElement) {
const data = annotation.data;
// Until we have all four anchors bail out
if (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 invalidated
if (invalidated) {
triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
}
return cachedStats;
}
7. 重写renderAnnotation函数
修改处标有注释 "修改..."
javascript
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 triggerAnnotationRender
if (!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 SVG
for (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 TEXT
if (!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 rendering
if (!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 points
if (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 arcs
const { 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.引入库
javascript
import CTRTool from "./CTRTool";
2. 添加到cornerstoneTools
javascript
cornerstoneTools.addTool(CTRTool);
3. 添加到toolGroup
javascript
toolGroup.addTool(CTRTool.toolName, {
showLinesText: true
});
总结
本章实现心胸比例测量工具CTRTool。
展示了从cornerstonejs库中派生自定义类的过程