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

news2024/11/18 7:40:14

🔆 引言

在使用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/low
  const 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.coloradmin.cn/o/1509379.html

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

相关文章

SSM整合项目(校验)

文章目录 1.前端校验1.需求分析2.HomeView.vue的数据池中添加校验规则3.HomeView.vue 绑定校验规则![image-20240311213428771](https://img-blog.csdnimg.cn/img_convert/7770bfa16814a0efd4eb818c9869a5bd.png)4.验证是否生效5.如果验证不通过,阻止用户提交表单1.…

机器学习之分类回归模型(决策数、随机森林)

回归分析 回归分析属于监督学习方法的一种,主要用于预测连续型目标变量,可以预测、计算趋势以及确定变量之间的关系等。 Regession Evaluation Metrics 以下是一些最流行的回归评估指标: 平均绝对误差(MAE):目标变量的预测值与实际值之间的平均绝对差…

webpack5零基础入门-4使用webpack处理less文件

1.安装less npm install less -D 2.创建less文件 .box{width: 100px;height: 100px;background: red; } 3.引入less文件并打包 执行npx webpack 报错无法识别less文件 4.安装less-loader并配置 npm install less-loader9 -D 这里指定一下版本不然会因为node版本过低报错 …

Java 启动参数 -- 和 -D写法的区别

当我们配置启动1个java 项目通常需要带一些参数 例如 -Denv uat , --spring.profiles.activedev 这些 那么用-D 和 – 的写法区别是什么? 双横线写法 其中这种写法基本上是spring 和 spring 框架独有 最常用的无非是就是上面提到的 --spring.profiles.activede…

【golang】28、用 httptest 做 web server 的 controller 的单测

文章目录 一、构建 HTTP server1.1 model.go1.2 server.go1.3 curl 验证 server 功能1.3.1 新建1.3.2 查询1.3.3 更新1.3.4 删除 二、httptest 测试2.1 完整示例2.2 实现逻辑2.3 其他示例2.4 用 TestMain 避免重复的测试代码2.5 gin 框架的 httptest 一、构建 HTTP server 1.1…

如何配置固定TCP公网地址实现远程访问内网MongoDB数据库

文章目录 前言1. 安装数据库2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射2.3 测试随机公网地址远程连接 3. 配置固定TCP端口地址3.1 保留一个固定的公网TCP端口地址3.2 配置固定公网TCP端口地址3.3 测试固定地址公网远程访问 前言 MongoDB是一个基于分布式文件存储的数…

JDK环境变量配置-jre\bin、rt.jar、dt.jar、tools.jar

我们主要看下rt.jar、dt.jar、tools.jar的作用,rt.jar在​%JAVA_HOME%\jre\lib,dt.jar和tools.jar在%JAVA_HOME%\lib下。 rt.jar:Java基础类库,也就是Java doc里面看到的所有的类的class文件。 tools.jar:是系统用来编…

星星魔方

星星魔方 1,魔方三要素 (1)组成部件 6个中心块和8个角块和三阶魔方同构,另外每个面还有构成五角星的十个块。 (2)可执行操作 一共12种操作,其中6种是每个层顺时针旋转90度,另外6…

Gateway(路由映射)

1.SpringCloud Gateway Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。 Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息,从而加…

用Vision Pro来控制机器人

【技术框架概述】 - visionOS App + Python Library用于从Vision Pro将头部/手腕/手指跟踪数据流式传输到任何机器人。 【定位】 - 该框架旨在利用Vision Pro控制机器人,并记录用户在环境中导航和操作的方式,以训练机器人。 【核心功能】 1. 提供visionOS应用程序和Py…

TEASEL: A transformer-based speech-prefixed language model

文章目录 TEASEL:一种基于Transformer的语音前缀语言模型文章信息研究目的研究内容研究方法1.总体框图2.BERT-style Language Models(基准模型)3.Speech Module3.1Speech Temporal Encoder3.2Lightweight Attentive Aggregation (LAA) 4.训练…

大语言模型系列-中文开源大模型

文章目录 前言一、主流开源大模型二、中文开源大模型排行榜 前言 近期,OpenAI 的主要竞争者 Anthropic 推出了他们的新一代大型语言模型 Claude 3,该系列涵盖了三个不同规模的模型:Opus、Sonnet 和 Haiku。 Claude 3声称已经全面超越GPT-4。…

软考71-上午题-【面向对象技术2-UML】-UML中的图2

一、用例图 上午题,考的少;下午题,考的多。 1-1、用例图的定义 用例图展现了一组用例、参与者以及它们之间的关系。 用例图用于对系统的静态用例图进行建模。 可以用下列两种方式来使用用例图: 1、对系统的语境建模&#xff1b…

人口性别年龄分布数据、不同年龄结构、性别结构人口分布数据、乡镇街道人口分布数据

人口分布是指人口在一定时间内的空间存在形式、分布状况,包括各类地区总人口的分布,以及某些特定人口(如城市人口、、特定的人口过程和构成(如迁移、性别等)的分布等。 人口分布的最大特征是不平衡性。就全世界而言&am…

【工具】软件工具分享哪家强?安卓apk安装软件分享新方法,弃用QQ启用企业微信使用方法...

微信关注公众号 “DLGG创客DIY” 设为“星标”,重磅干货,第一时间送达。 前言 又又来聊软件工具分享 先简单回顾一下之前的内容: 按时间先后顺序: 1.从网盘到QQ群文件及群文件分类 【工具】软件工具分享哪家强?群文件使…

Mac电脑搭建前端项目环境,并适配老项目

1.上一篇文章中,我说到了,node.js中文网下载node 包,根据系统进行选择,然后安装包node即可,对于比较新的项目确实也是适用的,但是老项目就不行了会报错,node版本过高,导致环境不匹配…

Java线程的基本操作

线程的基本操作 Java线程的常用操作都定义在Thread类中,包括一些重要的静态方法 和线程的实例方法 。下面我们来学习一下,线程的常用基本操作 1.线程名称的设置和获取 线程名称可以通过构造Thread的时候进行设置,也可以通过实例的方法setName…

科技云报道:两会热议的数据要素,如何拥抱新技术?

科技云报道原创。 今年全国两会上,“数字经济”再次成为的热点话题。 2024年政府工作报告提到:要健全数据基础制度,大力推动数据开发开放和流通使用;适度超前建设数字基础设施,加快形成全国一体化算力体系&#xff1…

【Flutter】报错Target of URI doesn‘t exist ‘package:flutter/material.dart‘

运行别人项目 包无法导入报错:Target of URI doesn’t exist ‘package:flutter/material.dart’ 解决方法 flutter packages get成功 不会报错

Centos本地、公网邮件发送配置

目录 本地邮件发送 发送邮件的三种方式 接受邮件 配置公网发送邮件 发送文件 本地邮件发送 安装服务 # yum -y install postfix # yum -y install mailx 启动服务 # systemctl start postfix 发送邮件的三种方式 一. # mail-s“邮件主题” 收件人 ​ 邮件内容…