网站开发分前台后台,中华室内设计师,wordpress 分享 可见,北京朝阳网站#x1f506; 引言 
在使用Cornerstone3D渲染影像时#xff0c;有一个常用功能“设置窗宽窗位#xff08;windowWidthwindowLevel#xff09;”#xff0c;通过精确调整窗宽窗位#xff0c;医生能够更清晰地区分各种组织#xff0c;如区别软组织、骨骼、脑组织等。… 引言 
在使用Cornerstone3D渲染影像时有一个常用功能“设置窗宽窗位windowWidthwindowLevel”通过精确调整窗宽窗位医生能够更清晰地区分各种组织如区别软组织、骨骼、脑组织等。本文将围绕窗宽窗位的基础概念、如何使用工具调整及工具调整的实现原理、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如何使用后那接下来我们来看一下它到底是如何执行的。 逻辑大纲梳理 
在看具体的源码前我们先大致梳理一下如果想要在拖拽鼠标移动时更新窗宽位我们都需要哪些数据 当前窗宽和窗位值调整时的起始点dicom文件的元数据属性中通常包含当前的窗宽和窗位值可以作为调整的初始值。  鼠标拖拽的位移数据 水平方向的位移量和垂直方向的位移量一般使用canvas的2D位移坐标通常包含在事件监听中。   敏感度乘数重点 根据图像的动态范围计算位移量对窗宽窗位的敏感度影响【这个是整个逻辑中重要且计算复杂的部分具体实现逻辑在源码解读中展开】  最新的窗宽窗位值由以上三点计算出最新的窗宽窗位值并赋值渲染  源码实现解读 
在梳理完大致需要的数据后我们再来看一下源码中是如何获取到这些数据又有哪些数据是在初始梳理时被忽略掉的。 
在 Cornerstone3D的官方github中找到 WindowLevelTool 这个文件我们可以看到WindowLevelTool继承于BaseTool但是这个不重要不在本次讨论计划中在整个类中有一个 mouseDragCallback 函数这个一看上去就像是关键函数我们来看一下这个函数的实现。 
核心目的拿到最新的窗宽窗位值并赋值影像渲染 第一阶段数据准备阶段 
由以下流程图可见在代码开始阶段WindowLevelTool准备了deltaPoint、lower、upper关于lower、upper与窗宽窗距地关系及转换方式在下一章节【动态调整方案】中详细展开、isPreScaled、modality 等变量我们先来看整体的执行流程 根据上面的流程逻辑我们对应着源码来具体看一下代码是如何实现的 第二阶段最新窗宽窗位计算阶段 
经过上面的代码我们已经拿到了计算新的窗宽窗位所需要的数据那这些数据如何组合计算才可以得到新的窗宽窗位呢 
计算窗宽窗位比较核心的步骤是计算敏感度比率然后有比率值得到最新的窗宽窗位值我们先来了解一下敏感度比率的计算逻辑然后再看源码是如何通过编程实现这一计算逻辑的。 敏感度乘数计算逻辑 定义一个默认的敏感度乘数在Cornerstone中这个值为4const DEFAULT_MULTIPLIER  4;  计算图像的动态范围  **获取动态范围**动态范围一般指图像中像素值的最大值与最小值之间的差。对于CT图像可以通过中间切片来获取  **动态范围与乘数的关系**动态范围的大小可以用来改变乘数的计算  
计算乘数 
一般乘数的计算为【(动态范围 || 2**元数据像素存储位置 取小)/默认动态范围】const DEFAULT_IMAGE_DYNAMIC_RANGE  1024; 最新窗宽窗位的计算逻辑 计算窗宽偏移量由上面得到的敏感度乘数 * 鼠标在x轴上的偏移量就能得到窗宽的一个偏移量  计算窗位偏移量 由上面得到的敏感度乘数 * 鼠标在y轴上的偏移量就能得到窗位的一个偏移量  计算最新的窗宽窗位现在的窗宽窗位加上对应的偏移量得到最新的窗宽窗位值  
以上就是整个算法中比较核心的部分那了解完计算逻辑后我们来看一下在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并成功激活了它可以让用户自主调整窗宽窗距这时产品又提出了一个新的需求不能只让用户通过工具拖拽调整我们应该内置一些常用的窗宽窗位让用户快速且精准的设置。 
这个需求你拍脑袋一想那直接设置几个快捷按钮不就可以了但是快捷按钮是响应事件是什么上面源码解读时获取到的lower和upper 与窗宽窗位又有什么关系 
lower 与 upper 
在医学影像处理时“lower”和“upper”通常指的是窗宽调整的下限和上限值。这些值定义了在图像显示时用于映射像素值到显示器亮度的范围。 Lower (下限)指的是窗宽调整范围的最小边界计算公式通常是 WL - WW/2这里的 WL 是窗位WW 是窗宽。  Upper (上限)指的是窗宽调整范围的最大边界计算公式通常是 WL  WW/2。  
如何获取lower和upper 
当我们知道lower、upper与窗宽窗位的计算关系后我们就可以在拿到lowerupper后计算对应的窗宽窗位了其实对于如何获取到lowerupper在上面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 
我们知道了lowerupper与wwwl之间的计算方式后虽然可以手动计算对应的 wwwl 但是Cornerstone本身提供了两个内置工具方法供我们转换使用 
由 lowerupper 转 wwwl let { windowWidth, windowCenter }  utilities.windowLevel.toWindowLevel(lower,upper
);由 wwwl 转 lowerupper let { lower, upper }  utilities.windowLevel.toLowHighRange(windowWidth, windowCenter)假设我们已经有了按钮设置对应的窗宽窗位以下为Vue项目中MPR视图下每个按钮对应的点击事件示例 
// windowWidthwindowLevel 为当前按钮需要设置的窗框窗距
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();});}},内置函数源码解读 
虽然在上面给出了lower和upper的通用计算方式但是在处理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 };
}计算方式浅析 在计算lower 和 upper 时为什么窗宽 -1  
窗宽定义为要显示的灰度范围的宽度。在考虑窗的两端时减去 1 是为了确保窗宽覆盖的是指定的像素范围内完整的单位数减去的是开始的中心点。例如我们想要一个6个单位的窗宽时减1主要是如下进行的 准确的窗边界定位窗宽为6意味着从窗位中心开始向每侧扩展出去的范围一共涵盖6个单位。在不减1的情况下如果直接将窗宽的一半加/减到窗位上可能会导致计算的范围实际上比预期宽或窄因为这种计算可能不会精确考虑到窗位中心所在的那一个单位。  确保窗宽精确覆盖期望的单位数通过减去1后再除以2实际上是在计算从窗位中心点向两边扩展时确切地排除了中心点占用的那一个单位然后均匀分配剩余的窗宽到中心点的两侧。这样做确保了不管窗位中心点如何定位从中心点向两侧扩展出的范围总是精确地覆盖了除中心点外的额外5个单位从而确保整个窗宽为6个单位。  在计算lower 和 upper 时为什么窗位 - 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演示Githubhttps://github.com/jianyaoo/vue-cornerstone-demo 交流更多相关使用技巧~ CornerStone3D核心概念https://juejin.cn/post/7326432875955798027Cornerstone3DTools常用工具https://juejin.cn/post/7330300019022495779