基于cornerstone3D的dicom影像浏览器 第二十五章 自定义VR调窗工具

文章目录


前言

从cornerstoneTools BaseTool派生VolumeShiftColorTool,实现鼠标键按下并移动时,对3D窗口的preset进行偏移,达到三维调窗的目的。

演示视频中绑定鼠标右键进行调窗,和其他工具一样,也可以绑定左键,中键。

效果如下:


一、三维调窗原理

观察cornerstonejs中 viewport preset 数据结构:

源码位置:packages\core\src\constants\viewportPresets.ts

javascript 复制代码
const presets: ViewportPreset[] = [
{
    name: 'CT-AAA',
    gradientOpacity: '4 0 1 255 1',
    specularPower: '10',
    scalarOpacity:
      '12 -3024 0 143.556 0 166.222 0.686275 214.389 0.696078 419.736 0.833333 3071 0.803922',
    specular: '0.2',
    shade: '1',
    ambient: '0.1',
    colorTransfer:
      '24 -3024 0 0 0 143.556 0.615686 0.356863 0.184314 166.222 0.882353 0.603922 0.290196 214.389 1 1 1 419.736 1 0.937033 0.954531 3071 0.827451 0.658824 1',
    diffuse: '0.9',
    interpolation: '1',
  },
  ...
]

我们关心两个数据

  1. scalarOpacity:图像灰度值映射不透明度
    值为字符串,以空格为分隔符:
    第一个数据(12)表示有效映射数据个数,即第二个数据(-3024)到最后一个数据(0.803922)的个数。从第二个数据~最后一个数据,每两个数据为一组映射,如第一组[-3024 0],表示像素值为-3024时对应的不透明度为0。第一个数据为12,计算可知有6组映射。
  2. colorTransfer:图像灰度值映射RGB值
    值为字符串,以空格为分隔符:
    第一个数据(24)表示有效映射数据个数,即第二个数据(-3024)到最后一个数据(1)的个数。从第二个数据~最后一个数据,每四个数据为一组映射,如第一组:[-3024 0 0 0],表示像素值为-3024时对应的RGB值 为(0,0,0)。此处为归一化的RGB值,乘255可得RGB值。第一个数据为24,计算可知有6组映射。

实现三维调窗的方法:在鼠标按下并移动时,修改preset中scalarOpacity和colorTransfer中每组映射中的第一个值,即图像灰度值,再把修改后的preset值设置到vieport即可实现。

javascript 复制代码
_shiftVRColor(viewport, preset, pos) {
		const volumeActor = viewport.getDefaultActor().actor;
		// 字符串以空格为分割符转为数值数组
		const color = preset.colorTransfer.split(" ").map(Number);
		// 偏移数据
		for (let i = 1; i < color.length; i += 4) {
			color[i] = color[i] + pos;
		}

		// 字符串以空格为分割符转为数值数组
		const opacity = preset.scalarOpacity.split(" ").map(Number);
		// 偏移数据
		for (let i = 1; i < opacity.length; i += 2) {
			opacity[i] = opacity[i] + pos;
		}
		
		// 生成新的preset
		const newPreset = { ...preset };
		newPreset.colorTransfer = color.join(" ");
		newPreset.scalarOpacity = opacity.join(" ");

		// 记录偏移
		preset.shiftPos = pos;

		// 应用新的preset
		csUtils.applyPreset(volumeActor, newPreset);
		viewport.render();
	}

二、自定义三维调窗工具

新建VolumeShiftColorTool.js

参考WindowLevelTool,从BaseTool派生,重写mouseDragCallback。在mouseDragCallback中调用上一节的_shiftVRColor函数

源码位置:packages\tools\src\tools\WindowLevelTool.ts

javascript 复制代码
import {
	getEnabledElement,
	utilities as csUtils,
	VolumeViewport3D
} from "@cornerstonejs/core";
import * as cornerstoneTools from "@cornerstonejs/tools";
const { BaseTool } = cornerstoneTools;

export default class VolumeShiftColorTool extends BaseTool {
	static toolName;
	constructor(
		toolProps = {},
		defaultToolProps = {
			supportedInteractionTypes: ["Mouse", "Touch"]
		}
	) {
		super(toolProps, defaultToolProps);
	}

	touchDragCallback(evt) {
		this.mouseDragCallback(evt);
	}

	mouseDragCallback(evt) {
		const { element, deltaPoints } = evt.detail;
		const enabledElement = getEnabledElement(element);
		const { viewport } = enabledElement;
		
		if (viewport instanceof VolumeViewport3D) {
			const { preset } = viewport.getProperties();
			let shiftPos = preset.shiftPos || 0;
			// 鼠标上下移动
			const yDelta = deltaPoints.canvas[1];
			shiftPos += yDelta;

			this._shiftVRColor(viewport, preset, shiftPos);
		}
	}

	_shiftVRColor(viewport, preset, pos) {
		const volumeActor = viewport.getDefaultActor().actor;
		const color = preset.colorTransfer.split(" ").map(Number);
		for (let i = 1; i < color.length; i += 4) {
			color[i] = color[i] + pos;
		}
		
		const opacity = preset.scalarOpacity.split(" ").map(Number);
		for (let i = 1; i < opacity.length; i += 2) {
			opacity[i] = opacity[i] + pos;
		}

		const newPreset = { ...preset };
		newPreset.colorTransfer = color.join(" ");
		newPreset.scalarOpacity = opacity.join(" ");

		preset.shiftPos = pos;

		csUtils.applyPreset(volumeActor, newPreset);
		viewport.render();
	}
}

VolumeShiftColorTool.toolName = "VolumeShiftColor";

三、调用流程

1. 修改mprvr.js

  • 导入、添加VolumeShiftColorTool
  • 添加函数enableVolumeShiftColor,切换ZoomTool和VolumeShiftColorTool绑定鼠标右键
javascript 复制代码
import VolumeShiftColorTool from "./VolumeShiftColorTool";

export default class MPR {
	constructor(params) {
		this.toolGroup = null;
		this.vrToolGroup = null;
		this.renderingEngine = null;
		this.registered = false;
		...
		this.init(params);
	}

	init(config = {}) {
	 	const { elAxial, elSagittal, elCoronal, elVR } = config;

		cornerstoneTools.addTool(CrosshairsTool);
		...
		this.vrToolGroup = ToolGroupManager.getToolGroup(vrToolGroupId);
		cornerstoneTools.addTool(VolumeShiftColorTool);
		if (!this.vrToolGroup) {
			...
			this.vrToolGroup.addTool(VolumeShiftColorTool.toolName);
		}
	}
	enableVolumeShiftColor(enable) {
		if (enable) {
			this.vrToolGroup.setToolDisabled(ZoomTool.toolName);
			this.vrToolGroup.setToolActive(VolumeShiftColorTool.toolName, {
				bindings: [
					{
						mouseButton: MouseBindings.Secondary // Right Click
					}
				]
			});
		} else {
			this.vrToolGroup.setToolDisabled(VolumeShiftColorTool.toolName);
			this.vrToolGroup.setToolActive(ZoomTool.toolName, {
				bindings: [
					{
						mouseButton: MouseBindings.Secondary // Right Click
					}
				]
			});
		}
	}
}

2. 修改DispalyerArea3D.vue

javascript 复制代码
const enableVRShiftColor = enable => {
	theMPR.enableVolumeShiftColor(enable);
};

defineExpose({
	...
	enableVRShiftColor
});

3. view3d.vue

响应工具栏enableVRShift事件

javascript 复制代码
async function OnToolbarAction(action) {
	switch (action.name) {
		...
		case "enableVRShift":
			displayArea.value.enableVRShiftColor(action.value);
			break;
		default:
			break;
	}
}

4. Toolbar3D.vue

添加"VR调窗" el-checkbox, 绑定变量enableVRShift

javascript 复制代码
const enableVRShift = ref(false);
watch(enableVRShift, (newValue) => {
	emit("action", { name: "enableVRShift", value: newValue });
});

<template>
	<div class="toolbar">
		...
		
		<div class="toolbar-row">
			<el-checkbox v-model="enableVRShift" label="VR调窗" size="large" />
		</div>
       ...

	</div>
</template>

总结

  1. 讲解三维调窗原理
  2. 自定义工具流程
相关推荐
炫饭第一名2 小时前
速通Canvas指北🦮——基础入门篇
前端·javascript·程序员
进击的尘埃4 小时前
Vue3 响应式原理:从 Proxy 到依赖收集,手撸一个迷你 reactivity
javascript
willow4 小时前
JavaScript数据类型整理1
javascript
LeeYaMaster4 小时前
20个例子掌握RxJS——第十一章实现 WebSocket 消息节流
javascript·angular.js
UIUV5 小时前
RAG技术学习笔记(含实操解析)
javascript·langchain·llm
颜酱7 小时前
理解二叉树最近公共祖先(LCA):从基础到变种解析
javascript·后端·算法
FansUnion7 小时前
我如何用 Next.js + Supabase + Cloudflare R2 搭建壁纸销售平台——月成本接近 $0
javascript
左夕8 小时前
分不清apply,bind,call?看这篇文章就够了
前端·javascript
滕青山9 小时前
文本行过滤/筛选 在线工具核心JS实现
前端·javascript·vue.js
时光不负努力9 小时前
编程常用模式集合
前端·javascript·typescript