一文了解Cornerstone3D中窗宽窗位的3种设置场景及原理

🔆 引言

在使用Cornerstone3D渲染影像时,有一个常用功能“设置窗宽窗位(windowWidth&windowLevel)”,通过精确调整窗宽窗位,医生能够更清晰地区分各种组织,如区别软组织、骨骼、脑组织等。本文将围绕窗宽窗位的基础概念、如何使用工具调整及工具调整的实现原理、js动态调整、MPR视图下多视图同步调整等展开。

🔎 关于窗宽窗位

窗宽窗位在医学影像学中是一项重要概念,特别是在CT和MRI中。它们主要通过调整影像的对比度和亮度来改善组织的可视化,以便于更好的观察影像中不同组织的细节。所以在介绍如何设置窗宽窗位前,先简单说明下它们是什么。

窗宽(Window Width, WW)

窗宽是指在医学影像上可视化的灰度范围。它决定了影像中最黑和最白两个点之间的对比度。

  • 窗宽值越大,影像上显示的灰度差异就越小,对比度就越低

  • 窗宽值越小,影像上显示的灰度差异就越大,对比度就越高

窗位(Window Level, WL)

窗位是指影像中的中间灰度值,它决定了影像灰度范围的中心。通过调整窗位,可以改变影像的亮度,进而使某些结构更加明显。

  • 增加窗位值可以使影像整体变亮,有助于观察较深的结构

  • 减少窗位值可以使影像整体变暗,有助于观察较浅的结构。

为什么需要设置不同的窗宽窗位

医生或影像技师可以根据需要观察的组织类型选择合适的窗宽窗位设置,以下是医学中常用的窗宽窗位设置,所以我们在设计功能时一般会将常用数据设置为快捷操作,便于直接调整。

  • 脑窗: 窗宽(WW)约为 80-100 (HU),窗位(WL)约为 30-40 HU,用于优化灰质和白质的对比度,常用于检测脑部病变

  • 软组织窗:窗宽(WW)约为 300-500 HU (HU),窗位(WL)约为 40-60 HU,用于观察和区分身体软组织,如肌肉、器官等

  • 肺窗:窗宽(WW)约为 1500-2000 HU,窗位(WL)约为 -450 ~ -600 HU,用于观察肺部结构,能够清晰显示气道和肺实质

  • 骨窗:窗宽(WW)约为 1000-1500 HU,窗位(WL)约为 250-350 HU,用于观察骨骼的细节,常用于查找骨折和其他骨骼病变

  • 血管窗:窗宽(WW)约为 600-800 HU,窗位(WL)约为 120-160 HU,主要用于评估血管的情况,特别是在血管造影研究中

🪜 使用工具调整

在Cornerstone3D Tools中提供了调整窗宽窗位的工具 WindowLevelTool,操作应用于视图的WindowLevel。它提供了一种通过在图像上拖动鼠标来设置视窗的windowCenter和windowWidth的方法。

windowLevelTool 基础使用

部分关键代码,整体可运行代码可查看:在线演示

import {addTool,Enums as cstEnums,destroy as cstDestroy,ToolGroupManager,WindowLevelTool,
} from "@cornerstonejs/tools";// 声明注册激活工具的业务函数
addTools() {//  顶层API全局添加addTool(WindowLevelTool);// 创建工具组,在工具组添加const toolGroup = ToolGroupManager.createToolGroup(this.toolGroupId);toolGroup.addTool(WindowLevelTool.toolName);toolGroup.addViewport(this.viewportId1, this.renderingEngineId);toolGroup.addViewport(this.viewportId2, this.renderingEngineId);toolGroup.addViewport(this.viewportId3, this.renderingEngineId);// 设置当前激活的工具toolGroup.setToolActive(WindowLevelTool.toolName, {bindings: [{mouseButton: cstEnums.MouseBindings.Primary,},],});
}

WindowLevelTool 实现原理

在了解到WindowLevelTool如何使用后,那接下来我们来看一下它到底是如何执行的。

🧘 逻辑大纲梳理

在看具体的源码前,我们先大致梳理一下,如果想要在拖拽鼠标移动时更新窗宽位,我们都需要哪些数据?

  1. 当前窗宽和窗位值:调整时的起始点,dicom文件的元数据属性中通常包含当前的窗宽和窗位值,可以作为调整的初始值。

  2. 鼠标拖拽的位移数据: 水平方向的位移量和垂直方向的位移量,一般使用canvas的2D位移坐标,通常包含在事件监听中。

  3. 🚀 敏感度乘数(重点): 根据图像的动态范围,计算位移量对窗宽窗位的敏感度影响【这个是整个逻辑中重要且计算复杂的部分,具体实现逻辑在源码解读中展开】,

  4. 最新的窗宽窗位值:由以上三点计算出最新的窗宽窗位值,并赋值渲染

🏄 源码实现解读

在梳理完大致需要的数据后,我们再来看一下源码中是如何获取到这些数据,又有哪些数据是在初始梳理时被忽略掉的。

在 Cornerstone3D的官方github中找到 WindowLevelTool 这个文件,我们可以看到WindowLevelTool继承于BaseTool,但是这个不重要,不在本次讨论计划中,在整个类中,有一个 mouseDragCallback 函数,这个一看上去就像是关键函数,我们来看一下这个函数的实现。

核心目的:拿到最新的窗宽窗位值,并赋值影像渲染

👉 第一阶段:数据准备阶段

由以下流程图可见:在代码开始阶段,WindowLevelTool准备了deltaPointlowerupper(关于lower、upper与窗宽窗距地关系及转换方式在下一章节【动态调整方案】中详细展开)isPreScaledmodality 等变量,我们先来看整体的执行流程

根据上面的流程逻辑,我们对应着源码来具体看一下代码是如何实现的

👉 第二阶段:最新窗宽窗位计算阶段

经过上面的代码,我们已经拿到了计算新的窗宽窗位所需要的数据,那这些数据如何组合计算才可以得到新的窗宽窗位呢?

计算窗宽窗位比较核心的步骤是:计算敏感度比率,然后有比率值得到最新的窗宽窗位值,我们先来了解一下敏感度比率的计算逻辑,然后再看源码是如何通过编程实现这一计算逻辑的。

🔥 敏感度乘数计算逻辑

  1. 定义一个默认的敏感度乘数:在Cornerstone中这个值为4,const DEFAULT_MULTIPLIER = 4;

  2. 计算图像的动态范围

  • **获取动态范围:**动态范围一般指图像中像素值的最大值与最小值之间的差。对于CT图像,可以通过中间切片来获取

  • **动态范围与乘数的关系:**动态范围的大小可以用来改变乘数的计算

  1. 计算乘数
  • 一般乘数的计算为【(动态范围 || 2**元数据像素存储位置 取小)/默认动态范围】,const DEFAULT_IMAGE_DYNAMIC_RANGE = 1024;

🔥 最新窗宽窗位的计算逻辑

  1. 计算窗宽偏移量:由上面得到的敏感度乘数 * 鼠标在x轴上的偏移量,就能得到窗宽的一个偏移量

  2. 计算窗位偏移量: 由上面得到的敏感度乘数 * 鼠标在y轴上的偏移量,就能得到窗位的一个偏移量

  3. 计算最新的窗宽窗位:现在的窗宽窗位加上对应的偏移量,得到最新的窗宽窗位值

以上就是整个算法中比较核心的部分,那了解完计算逻辑后,我们来看一下在Cornerstone3D的源码中,是如何通过代码实现以上的计算逻辑的(由于篇幅问题,暂不展开说明PT模式下的实现,在后续PT工具文章中再展开说明)

👉 第三阶段:为视图设置新的窗宽窗位,并渲染

经过前两个阶段,我们已经拿到了最新的窗宽窗位值,现在我们只需要将最新的窗宽窗位值重新赋值给视图,并让视图重新渲染即可。

viewport.setProperties({voiRange: newRange,
});viewport.render();

如果当前Volume具有多个视图的话,需要多个视图都重新渲染一下

if (viewport instanceof VolumeViewport) {viewportsContainingVolumeUID.forEach((vp) => {if (viewport !== vp) {vp.render();}});return;}

至此,关于WindowLevelTools是如何设置窗宽窗位的源码已完全解读,现在大家应该基本了解了窗宽窗位都跟哪些数据相关,这些数据又是从哪里获取到的,获取到又是如何应用这些数据计算的(关于为什么能够事件的detail中获取到canvas的2d坐标的,会在后续事件监听文章中详细展开)

👩‍💻 动态调整方案

当我们在自己的项目中使用了WindowLevelTool,并成功激活了它,可以让用户自主调整窗宽窗距,这时产品又提出了一个新的需求,不能只让用户通过工具拖拽调整,我们应该内置一些常用的窗宽窗位让用户快速且精准的设置。

这个需求你拍脑袋一想,那直接设置几个快捷按钮不就可以了,但是快捷按钮是响应事件是什么,上面源码解读时获取到的lowerupper 与窗宽窗位又有什么关系?

lower 与 upper

在医学影像处理时,“lower”和“upper”通常指的是窗宽调整的下限和上限值。这些值定义了在图像显示时用于映射像素值到显示器亮度的范围。

  • Lower (下限):指的是窗宽调整范围的最小边界,计算公式通常是 WL - WW/2,这里的 WL 是窗位,WW 是窗宽。

  • Upper (上限):指的是窗宽调整范围的最大边界,计算公式通常是 WL + WW/2。

如何获取lower和upper

当我们知道lowerupper与窗宽窗位的计算关系后,我们就可以在拿到lower&upper后计算对应的窗宽窗位了,其实对于如何获取到lower&upper在上面WindowLevelTool的源码中已经给出来了,它在viewport的属性中

const enabledElement = getEnabledElement(element);
const { renderingEngine, viewport } = enabledElement; // 获取viewport的方式可以依据上下文多种方案获取const properties = viewport.getProperties(); // 获取到viewport的属性对象:properties
const { lower, upper } = properties.voiRange; // 从 properties 的voiRange属性中获取到当前视图中的 lower, upper

转换lower和upper

我们知道了(lower&upper)与(ww&wl)之间的计算方式后,虽然可以手动计算对应的 ww&wl ,但是Cornerstone本身提供了两个内置工具方法供我们转换使用

  • 由 lower&upper 转 ww&wl
  let { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower,upper
);
  • 由 ww&wl 转 lower&upper
 let { lower, upper } = utilities.windowLevel.toLowHighRange(windowWidth, windowCenter)

假设我们已经有了按钮设置对应的窗宽窗位,以下为Vue项目中MPR视图下每个按钮对应的点击事件示例:

// windowWidth,windowLevel 为当前按钮需要设置的窗框窗距
handleWindowLevelClick(windowWidth, windowLevel) {if (windowWidth && windowWidth) {const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidth, windowLevel);[viewportId1, viewportId2,viewportId3].forEach((id) => {const vp = this.renderingEngine.getViewport(id);vp.setProperties({voiRange: {lower,upper,},});vp.render();});}},

内置函数源码解读

虽然在上面给出了lowerupper的通用计算方式,但是在处理Dicom文件时,Dicom标准已经明确给出了相关的计算方式,具体原理可查看 https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.11.2.1.2,在内置的工具函数中使用的计算方式即Dicom标准中给出的计算方式。

对应源码地址:https://github.com/cornerstonejs/cornerstone3D/blob/bc54ae70cb2180d5ce42cc7eaa17633f0bb5f34a/packages/core/src/utilities/windowLevel.ts

toLowHighRange

function toLowHighRange(windowWidth: number,windowCenter: number
): {lower: number;upper: number;
} {const lower = windowCenter - 0.5 - (windowWidth - 1) / 2;const upper = windowCenter - 0.5 + (windowWidth - 1) / 2;return { lower, upper };
}

toWindowLevel

function toWindowLevel(low: number,high: number
): {windowWidth: number;windowCenter: number;
} {// Allow for swapping high/lowconst windowWidth = Math.abs(high - low) + 1;const windowCenter = (low + high + 1) / 2;return { windowWidth, windowCenter };
}

计算方式浅析

🤔 在计算lowerupper 时为什么窗宽 -1 ?

窗宽定义为要显示的灰度范围的宽度。在考虑窗的两端时,减去 1 是为了确保窗宽覆盖的是指定的像素范围内完整的单位数,减去的是开始的中心点。例如我们想要一个6个单位的窗宽时,减1主要是如下进行的:

  • 准确的窗边界定位:窗宽为6意味着从窗位中心开始,向每侧扩展出去的范围一共涵盖6个单位。在不减1的情况下,如果直接将窗宽的一半加/减到窗位上,可能会导致计算的范围实际上比预期宽或窄,因为这种计算可能不会精确考虑到窗位中心所在的那一个单位。

  • 确保窗宽精确覆盖期望的单位数:通过减去1后再除以2,实际上是在计算从窗位中心点向两边扩展时,确切地排除了中心点占用的那一个单位,然后均匀分配剩余的窗宽到中心点的两侧。这样做确保了,不管窗位中心点如何定位,从中心点向两侧扩展出的范围总是精确地覆盖了除中心点外的额外5个单位,从而确保整个窗宽为6个单位。

    🤔 在计算lowerupper 时为什么窗位 - 0.5 ?

窗位减去0.5,是为了在计算时能够处理半个像素单位的偏移,这样做有助于更精确地定位和调整图像窗的中心。

这种微调主要是考虑到像素值通常是整数,而窗宽和窗位的调整可能需要更细致的控制,特别是在灰度值的分布和转换过程中。减去0.5是一种常用的技巧,以确保在离散的像素值和连续的窗宽调整之间达到更好的对应和平滑过渡。

📡 多视图同步

当我们终于搞定动态设置窗宽窗距后,产品又又又又提了个需求:在MPR视图时,调整其中一个视图的窗宽窗位,其他两个要同步响应💥

听完这个需求后,第一反应是这还不简单,我都知道怎么动态设置了,设置个同步还不是手到擒来,

  • 先监听每个视图的VOI变化

  • 当他变化时将拿到的窗宽窗位动态设置给其他视图

但是这么一想,一方面要监听多个视图,还容易一不小心就陷入个死循环,有没有更好的实现方式呢?当然有:那就是之前提的同步器,以下为示例代码

import {SynchronizerManager,synchronizers,
} from '@cornerstonejs/tools';// 使用内置的createVOISynchronizer,创建一个VOI同步器
synchronizers.createVOISynchronizer(VOI_SYNCHRONIZER_ID);// 获取创建的VOI同步器
const voiSynchronizer = SynchronizerManager.getSynchronizer(VOI_SYNCHRONIZER_ID);// 为同步器添加同步视图[viewportid1, viewportid2, viewportid3].forEach((viewportId) => {voiSynchronizer.add({renderingEngineId,viewportId,});
});

这样我们就为每个视图添加了同步,当变化的时候会同步变化(由于篇幅问题,这里就不展开详细讲同步器相关源码实现了,会在后续自定义同步器中展示详说)

🎉 结语

到这里,窗宽窗位相关的知识点、3种场景下的设置方案及源码解读就介绍,欢迎交流沟通任何Cornerstone3D相关知识点 👏

本系列为从0上手Cornerstone3D系列文章,包括cornerstone核心概念、基础使用、常见案例、工具使用、运行原理、源码解读等等,欢迎Start演示Github:https://github.com/jianyaoo/vue-cornerstone-demo 交流更多相关使用技巧~

  • CornerStone3D核心概念:https://juejin.cn/post/7326432875955798027
  • Cornerstone3DTools常用工具:https://juejin.cn/post/7330300019022495779

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

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

相关文章

mac【启动elasticsearch报错:can not run elasticsearch as root

mac【启动elasticsearch报错:can not run elasticsearch as root 问题原因 es默认不能用root用户启动,生产环境建议为elasticsearch创建用户。 解决方案 为elaticsearch创建用户并赋予相应权限。 尝试了以下命令创建用户,adduser esh 和u…

C# ListView 控件使用

1.基本设置 listView1.Columns.Add("序号", 60); //向 listView1控件中添加1列 同时设置列名称和宽度listView1.Columns.Add("温度", 100); //下同listView1.Columns.Add("偏移", 100);listView1.Columns.Add("分割", 50);listView1…

ssm蛋糕甜品商城系统(程序+文档+数据库)

** 🍅点赞收藏关注 → 私信领取本源代码、数据库🍅 本人在Java毕业设计领域有多年的经验,陆续会更新更多优质的Java实战项目,希望你能有所收获,少走一些弯路。🍅关注我不迷路🍅** 一、研究背景…

计算机视觉研究院 | EdgeYOLO:边缘设备上实时运行的目标检测器及Pytorch实现

本文来源公众号“计算机视觉研究院”,仅用于学术分享,侵权删,干货满满。 原文链接:EdgeYOLO:边缘设备上实时运行的目标检测器及Pytorch实现 代码地址:https://github.com/LSH9832/edgeyolo 今天分享的研究…

【LeetCode】升级打怪之路 Day 21:二叉树的最近公共祖先(LCA)问题

今日题目: 236. 二叉树的最近公共祖先1644. 二叉树的最近公共祖先 II235. 二叉搜索树的最近公共祖先 目录 LCA 问题LC 236. 二叉树的最近公共祖先 【classic】LC 1644. 二叉树的最近公共祖先 II 【稍有难度】LC 235. 二叉搜索树的最近公共祖先 ⭐⭐⭐ 今天做了几道有…

python备份库

个人简介 👨🏻‍💻个人主页:九黎aj 🏃🏻‍♂️幸福源自奋斗,平凡造就不凡 🌟如果文章对你有用,麻烦关注点赞收藏走一波,感谢支持! 🌱欢迎订阅我的…

SAM分割 图片bbox提示任意数量目标输出mask

前提条件:labelimg打标签得到bbox 1.代码 import torchfrom segment_anything import SamPredictor, sam_model_registry import cv2 import numpy as np import os import glob import xml.etree.ElementTree as ETcheckpoint "./weight/sam_vit_h_4b8939.…

分布式数据处理MapReduce简单了解

文章目录 产生背景编程模型统计词频案例 实现机制容错机制Master的容错机制Worker的容错机制 产生背景 MapReduce是一种分布式数据处理模型和编程技术,由Google开发,旨在简化大规模数据集的处理。产生MapReduce的背景: 数据量的急剧增长&…

通过OceanBase 3.x中not in无法走hash连接的变化,来看OB优化器的发展

作者简介: 张瑞远,曾从事银行、证券数仓设计、开发、优化类工作,现主要从事电信级IT系统及数据库的规划设计、架构设计、运维实施、运维服务、故障处理、性能优化等工作。 持有Orale OCM,MySQL OCP及国产代表数据库认证。 获得的专业技能与认…

C#,数值计算,矩阵相乘的斯特拉森(Strassen’s Matrix Multiplication)分治算法与源代码

Volker Strassen 1 矩阵乘法 矩阵乘法是机器学习中最基本的运算之一,对其进行优化是多种优化的关键。通常,将两个大小为N X N的矩阵相乘需要N^3次运算。从那以后,我们在更好、更聪明的矩阵乘法算法方面取得了长足的进步。沃尔克斯特拉森于1969年首次发表了他的算法。这是第…

【刷题】双指针进阶

请看入门篇 :双指针入门 送给我们一句话: 如今我努力奔跑,不过是为了追上那个曾经被寄予厚望的自己 —— 约翰。利文斯顿 双指针进阶 Leetcode 611 有效三角形的个数Leetcode LCR179.查找总价格为目标值的两个商品Leetcode 15.三数之和Thanks…

手把手教你使用Python第三方模块

1.第三方模块 一般是别人解决特定问题的功能进行了封装,可以通过安装直接使用 注意 第三方模块需要先安装,才能使用 常见的安装方式:通过pip工具或者通过pycharm编辑器进行安装 2.pip指令安装 pip -V # 查看pip的版本 pip 23.2.1 fr…

基于PHP的数字化档案管理系统

有需要请加文章底部Q哦 可远程调试 基于PHP的数字化档案管理系统 一 介绍 此数字化档案管理系统基于原生PHP,MVC架构开发,数据库mysql,前端bootstrap。系统角色分为用户和管理员。 技术栈 php(mvc)mysqlbootstrapphpstudyvscode 二 功能 …

网络原理(网络协议初识)

目录 1.网络通信基础 1.1IP地址 1.2端口号 1.3认识协议 1.4五元组 1.5 协议分层 2.TCP/IP五层(或四层)模型 2.1网络设备所在分层 2.2网络分层对应 3.封装和分用 1.网络通信基础 网络互连的目的是进行网络通信,也即是网络数据传输&#…

手写简易操作系统(九)--实现打印函数

前情提要 前面我们已经进入内核程序了,中间穿插了一点特权级的知识,现在我们开始准备一个打印函数 很不幸,还有汇编程序 一、C调用规约 因为涉及到C与汇编的联合编程,我们这里简述一下调用规约,调用规约就是约定参…

Node.js 学习笔记 fs、path、http模块;模块化;包;npm

Node.js学习 Node.js一、定义1.前端工程化2.Node.js 为何能执行 JS?3.安装nodejs、删除之前的nodejs4.使用 Node.js 二、fs模块 \- 读写文件三、path 模块案例 - 压缩前端html四、HTTP相关URL中的端口号常见的服务程序http 模块-创建 Web 服务案例:浏览时…

Python二级备考

考试大纲如下: 基本要求 考试内容 考试方式 比较希望能直接刷题,因为不懂的比较多可能会看视频。 基础操作刷题: 知乎大头计算机1-13题 import jieba txtinput() lsjieba.lcut(txt) print("{:.1f}".format(len(txt)/len(ls)…

XIAO ESP32S3部署Edge Impulse模型

在上一篇文章中我们介绍了如何使用edge impulse训练一个图片分类模型并导出arduino库文件。在这篇文章中我们将介绍如何在esp32s3中部署这个训练好的图片分类模型。 添加进Arduino库 有两种方法将下载的文件添加进Arduino库。 在Arduino IDE程序中,转到项目选项卡…

✅技术社区—集成xxl-job实现定时任务的管理和执行

xxl-job是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。(其中XXL是作者许雪里的简称) xxl-job是对老牌调度平台Quartz进行的封装。在开始介绍xxl-job之前我来先简单…

Jmeter —— jmeter中元件的执行顺序作用域

元件的执行顺序: 配置元件-->前置处理程序-->定时器-->取样器-->后置处理程序(除非Sampler 得 到的返回结果为空)-->断言(除非Sampler 得到的返回结果为空)-->监听器 (除非Sampler 得到…