基于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库中派生自定义类的过程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/pingmian/84043.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[嵌入式AI从0开始到入土]18_Ascend C算子开发环境(S5赛季)

[嵌入式AI从0开始到入土]嵌入式AI系列教程 注&#xff1a;等我摸完鱼再把链接补上 可以关注我的B站号工具人呵呵的个人空间&#xff0c;后期会考虑出视频教程&#xff0c;务必催更&#xff0c;以防我变身鸽王。 第1期 昇腾Altas 200 DK上手 第2期 下载昇腾案例并运行 第3期 官…

《前端面试题:JavaScript 闭包深度解析》

JavaScript 闭包深度解析&#xff1a;从原理到高级应用 一、闭包的本质与核心概念 闭包&#xff08;Closure&#xff09;是 JavaScript 中最强大且最常被误解的概念之一。理解闭包不仅是掌握 JavaScript 的关键&#xff0c;也是区分初级和高级开发者的重要标志。 1. 什么是闭…

【FPGA开发】DDS信号发生器设计

一、常见IP模块介绍 IP(IntellectualProperty)原指知识产权、著作权等&#xff0c;在IC设计领域通常被理解为实现某种功能的设计。IP模块则是完成某种比较复杂算法或功能&#xff08;如FIR滤波器、FFT、SDRAM控制器、PCIe接口、CPU核等&#xff09;并且参数可修改的电路模块&a…

板凳-------Mysql cookbook学习 (九--3)

4.3 使用临时表 Drop table 语句来删除表&#xff0c; 选择使用create temporary table 语句&#xff0c;创建的是一张临时表。 Create temporary table tb1_name(…列定义…) 克隆表 Create temporary table new_table like original_table 根据查询结果建表 Create temporary…

Python Web项目打包(Wheel)与服务器部署全流程

目录 一、本地开发环境准备二、创建setup.py打包配置三、创建WSGI入口文件四、打包生成Wheel文件五、服务器端部署流程1. 传输文件到服务器2. 服务器环境准备3. 配置生产环境变量4. 使用Gunicorn启动服务 六、高级部署方案&#xff08;Systemd服务&#xff09;1. 创建Systemd服…

c++ 基于openssl MD5用法

基于openssl MD5用法 #include <iostream> #include <openssl/md5.h> using namespace std; int main(int argc, char* argv[]) { cout << "Test Hash!" << endl; unsigned char data[] "测试md5数据"; unsigned char out[1024…

如何通过外网访问内网服务器?怎么让互联网上连接本地局域网的网址

服务器作为一个数据终端&#xff0c;是很多企事业单位不可获缺的重要设备&#xff0c;多数公司本地都会有部署服务器供测试或部署一些网络项目使用。有人说服务器就是计算机&#xff0c;其实这种说法不是很准确。准确的说服务器算是计算机的一种&#xff0c;它的作用是管理计算…

安装Openstack

基本按照Ubuntu官网的指南来安装&#xff0c;使用单节点模式&#xff0c;官网步骤参见网址&#xff1a;https://ubuntu.com/openstack/install 系统为Ubuntu 24.04.2&#xff0c;全新安装. Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.11.0-24-generic x86_64)kaiexperiment…

‌Kafka与RabbitMQ的核心区别

‌1.设计目标与适用场景‌ ‌Kafka‌&#xff1a;专注于高吞吐量的分布式流处理平台&#xff0c;适合处理大数据流&#xff08;如日志收集、实时数据分析&#xff09;&#xff0c;强调消息的顺序性和扩展性。‌‌ ‌RabbitMQ‌&#xff1a;作为消息中间件&#xff0c;侧重于消…

深入理解 Spring Cache 及其核心注解

一、Spring Cache 概述​ Spring Cache 并不是一个具体的缓存实现方案&#xff0c;而是一套抽象的缓存规范。它支持多种缓存技术&#xff0c;如 Ehcache、Redis、Caffeine 等&#xff0c;开发者可以根据项目需求灵活选择合适的缓存技术。其核心思想是通过在方法上添加注解&…

STM32H562----------串口通信(UART)

1、串口介绍 1.1、 数据通信概念 在单片机中我们常用的通信方式有 USART、IIC、SPI、CAN、USB 等; 1、数据通信方式 根据数据通信方式可分为串行通信和并行通信两种,如下图: 串行通信基本特征是数据逐位顺序依次传输,优点:传输线少成本低,抗干扰能力强可用于远距离传…

20-Oracle 23 ai free Database Sharding-特性验证

对于Oracle 23ai Sharding 新特性的验证脚本&#xff0c;目标是涵盖其核心改进和新增功能。基于 Oracle 23ai 的 Sharding 特性总结&#xff08;Raft 协议、True Cache、Vector等&#xff09;&#xff0c;结合常见场景验证。 通过SQL脚本验证这些特性。例如&#xff1a; 1.基于…

✅ 常用 Java HTTP 客户端汇总及使用示例

在 Java 开发中,HTTP 客户端是与服务端交互的关键组件。随着技术发展,出现了多种 HTTP 客户端库,本文汇总了常用的 Java HTTP 客户端,介绍其特点、适用场景,并附上简单使用示例,方便开发者快速选择和上手。 1.常用 HTTP 客户端一览 名称简介特点HttpClient(JDK 自带)Ja…

MCP(Model Context Protocol)与提示词撰写

随着大模型&#xff08;LLM&#xff09;在复杂任务中的普及&#xff0c;如何让模型高效调用外部工具和数据成为关键挑战。传统函数调用&#xff08;Function Calling&#xff09;依赖开发者手动封装 API&#xff0c;而 MCP&#xff08;Model Context Protocol&#xff09; 通过…

RootSIFT的目标定位,opencvsharp。

首先截取匹配模板&#xff0c;然后使用rootsift特征匹配&#xff0c;最后定位目标。 对于微弱变化&#xff0c;还是能够识别定位的&#xff0c;对于传统算法来说已经不错了。 目标定位效果&#xff1a; 使用的模板图片。 using OpenCvSharp; using OpenCvSharp.Features2D;u…

Appium如何支持ios真机测试

ios模拟器上UI自动化测试 以appiumwebdriverio为例&#xff0c;详细介绍如何在模拟器上安装和测试app。在使用ios模拟器前&#xff0c;需要安装xcode&#xff0c;创建和启动一个simulator。simulator创建好后&#xff0c;就可以使用xcrun simctl命令安装被测应用并开始测试了。…

近几年字节飞书测开部分面试题整理

文章目录 一、面试问题1. 创建索引2. 拦截器&#xff08;Interceptor&#xff09;和过滤器&#xff08;Filter&#xff09;的区别3. 为什么jwt令牌代替session&#xff1f;4. 有一个100行的数据&#xff0c;和一个1万行的数据&#xff0c;写sql 的时候要注意什么&#xff1f;5.…

JDBC基础关键_001_认识

目 录 一、概述 二、原理 三、接口的作用 四、JDBC 模拟 1.JDBC 接口 2.驱动 3.配置文件 4.调用者 一、概述 JDBC&#xff08;Java DataBase Connectivity&#xff09;&#xff0c;Java 数据库连接&#xff1b;是用 Java 语言操作数据库&#xff0c;使用 Java 语言向数…

SWAN(Scade One) 语言原理介绍

SCADE 团队于2024年推出了下一代 SCADE 工具 Scade One&#xff0c;工具的建模语言也基于Scade 6 进行了演化。在语言命名方面&#xff0c;并没有复用"Scade"这一标志性的名称&#xff0c;而是使用了新的名字&#xff1a;Swan。在本篇中&#xff0c;将叙述 Swan 语言…

【工具教程】多个条形码识别用条码内容对图片重命名,批量PDF条形码识别后用条码内容批量改名,使用教程及注意事项

一、条形码识别改名使用教程 打开软件并选择处理模式&#xff1a;打开软件后&#xff0c;根据要处理的文件类型&#xff0c;选择 “图片识别模式” 或 “PDF 识别模式”。如果是处理包含条形码的 PDF 文件&#xff0c;就选择 “PDF 识别模式”&#xff1b;若是处理图片文件&…