基于cornerstone3D的dicom影像浏览器 第二十二章 mpr + vr

news2025/5/25 9:05:45

系列文章目录

第一章 下载源码 运行cornerstone3D example
第二章 修改示例crosshairs的图像源
第三章 vite+vue3+cornerstonejs项目创建
第四章 加载本地文件夹中的dicom文件并归档
第五章 dicom文件生成png,显示检查栏,序列栏
第六章 stack viewport 显示dicom序列
第七章 在Displayer四个角落显示文字
第八章 在Displayer中显示图像方位
第九章 自动加载、清空显示、修改布局
第十章 显示标尺
第十一章 测量工具
第十二章 镜像、负像、旋转、伪彩、复位
第十三章 自定义垂直滚动条
第十四章 参考线、同步调窗、同步缩放、同步移动
第十五章 预设窗值
第十六章 工具栏svg按钮
第十七章 同步滚动
第十八章 自定义序列自动播放条
第十九章 显示overlay
第二十章 显示多帧图
第二十一章 显示DICOM TAGS


文章目录

  • 系列文章目录
  • 前言
  • 一、程序结构
    • 1. view3d.vue
    • 2. DisplayerArea3D.vue
    • 3. mprvr.js
  • 二、代码
    • 1. view3d.vue
    • 2. DisplayerArea3D.vue
    • 3. mprvr.js
      • 1)init函数
      • 2)loadImages函数
      • 3)getSliceCount, getSliceIndex 函数
      • 4) resize函数
      • 5) bindRenderEvent, bindCameraEvent
      • 6)destory函数
      • getDicomInfo函数
  • 总结


前言

实现一个简单的mpr + vr 功能。
cornerstone3D已经全都实现了,修改为自己的界面。
查看官方示例-cursor3d
cornerstonejs源码位置packages\tools\examples\cursor3D\index.ts
效果如下:
在这里插入图片描述


一、程序结构

1. view3d.vue

  • 包含Toolbar3d.vue组件和DispalyerArea3D.vue组件
  • Toolbar3d.vue与前面章节中的Toolbar.vue组件一样,主要放置一些操作按钮

2. DisplayerArea3D.vue

  • 包含四个窗口div: axial, sagittal, coronal, vr
  • cornerstone3D相关的函数全写到mprvr.js中,由DisplayerArea3D.vue调用

3. mprvr.js

  • 实现MPR类,整理了下官方示例-curso3d中的代码,方便调用

二、代码

1. view3d.vue

<script setup name="View3d">
import { ref, onMounted, computed, watch } from "vue";
import { useRoute } from "vue-router";
import DisplayerArea3D from "../components/DisplayerArea3D.vue";
import Toolbar3D from "../components/Toolbar3D.vue";
import { useArchiveStore } from "../stores/archive";
import { useAppStore } from "../stores/appStore";
import { storeToRefs } from "pinia";

const archiveStore = useArchiveStore();
const appStore = useAppStore();

const route = useRoute();
const view3d = ref(null);
const displayArea = ref(null);
const toolbar = ref(null);
const toolbarHW = ref(300);

const { toolbarPos, navbarPos } = storeToRefs(appStore);

const mainClass = computed(() => {
	const vorh = toolbarPos.value === "top" ? "vertical" : "horizontal-reverse";
	return "flex-" + vorh;
});

const toolbarClass = computed(() => {
	return "toolbar-" + toolbarPos.value;
});

const toolbarStyle = computed(() => {
	if (toolbarPos.value === "top") {
		return {
			height: toolbarHW.value + "px"
		};
	} else {
		return {
			width: toolbarHW.value + "px"
		};
	}
});

function resizeToolbar(e) {
	e.stopPropagation();
	view3d.value.style.cursor = "ew-resize";
	view3d.value.onmousemove = function (e) {
		let delta, minVal, maxVal;
		if (toolbarPos.value === "top") {
			delta = e.movementY;
			minVal = 146;
			maxVal = 220;
		} else {
			delta = -e.movementX;
			minVal = 110;
			maxVal = 520;
		}

		const value = toolbarHW.value + delta;
		if (value < minVal || value > maxVal) return;
		toolbarHW.value = value;
	};

	document.onmouseup = function () {
		view3d.value.onmousemove = null;
		document.onmouseup = null;
		view3d.value.style.cursor = "";
	};
}

async function OnToolbarAction(action) {
	switch (action.name) {
		case "layoutMpr":
			displayArea.value.setLayout("layoutmpr");
			break;
		case "layoutMprVr":
			displayArea.value.setLayout("layoutmprvr");
			break;
		default:
			break;
	}
}

</script>

<template>
	<div class="container flex-vertical" ref="view3d" @contextmenu.prevent>
		<div class="main" :class="mainClass">
			<div :class="toolbarClass" :style="toolbarStyle">
				<Toolbar3D ref="toolbar" @action="OnToolbarAction" />
			</div>
			<div class="resizer-vertical" @mousedown="resizeToolbar"></div>
			<div class="display-area">
				<DisplayerArea3D ref="displayArea" />
			</div>
		</div>
		<div class="footer">Statusbar</div>
	</div>
</template>

<style scoped lang="scss">
$bg-color: #333;
$main-color: lightblue;
$header-color: #45b7ec;
$footer-color: #45b7ec;
$resizer-width: 2px;
$header-height: 100px;
$footer-height: 34px;

.container {
	background-color: $bg-color;
	height: 100vh;
	width: 100vw;
	max-width: 100%;
	max-height: 100%;
	justify-content: center;
	color: black;
	user-select: none;
}

.flex-vertical {
	display: flex;
	flex-direction: column;
}

.flex-vertical-reverse {
	display: flex;
	flex-direction: column-reverse;
}

.flex-horizontal {
	display: flex;
	flex-direction: row;
}

.flex-horizontal-reverse {
	display: flex;
	flex-direction: row-reverse;
}

.resizer-vertical {
	cursor: ew-resize;
	width: $resizer-width;
	background-color: gray;
	flex-shrink: 0;
	z-index: 10;
}

.resizer-horizontal {
	cursor: ns-resize;
	height: $resizer-width;
	background-color: gray;
	width: 100%;
	flex-shrink: 0;
	z-index: 10;
}

.main {
	width: 100%;
	height: 100%;
	background-color: $main-color;

	.toolbar-top {
		height: 200px;
		width: 100%;
	}

	.toolbar-right {
		width: 200px;
		height: 100%;
		flex-shrink: 0;
	}

	.display-area {
		flex: 1;
		flex-shrink: 0;
	}
}

.footer {
	background-color: $footer-color;
	height: $footer-height;
	border-top: gray solid 1px;
}
</style>

2. DisplayerArea3D.vue

  • 包含四个窗口div: axial, sagittal, coronal, vr
  • cornerstone3D相关的函数全写到mprvr.js中,由DisplayerArea3D.vue调用
<template>
	<div
		class="container3d"
		ref="elContainer"
		v-loading="loading"
		element-loading-text="正在处理..."
		element-loading-background="rgba(0, 0, 0, 0.8)"
		@mousedown.prevent="OnSelectView"
	>
		<div class="axialparent" :style="axialStyle" v-show="showAxial">
			<div ref="elAxial" class="sliceview" @contextmenu.prevent>
				<h4 class="desc">Axial</h4>
				<span class="text_studyinfo">{{ studyInfo }}</span>
				<span class="text_wwwc">{{ axialText.wwwc }}</span>
				<span class="text_slice">{{ axialText.slice }}</span>
				<span class="orient_top">{{ axialText.orient.top }}</span>
				<span class="orient_bottom">{{ axialText.orient.bottom }}</span>
				<span class="orient_left">{{ axialText.orient.left }}</span>
				<span class="orient_right">{{ axialText.orient.right }}</span>
			</div>
		</div>
		<div class="vrcprparent" v-show="showVR" @dblclick="OnDbClick">
			<div ref="elVR" class="sliceview" @contextmenu.prevent>
				<h4 class="desc">VR</h4>
				<span class="text_studyinfo">{{ studyInfo }}</span>
			</div>
		</div>
		<div class="sagittalparent" v-show="showSagittal">
			<div ref="elSagittal" class="sliceview" @contextmenu.prevent>
				<h4 class="desc">Sagittal</h4>
				<span class="text_studyinfo">{{ studyInfo }}</span>
				<span class="text_wwwc">{{ sagittalText.wwwc }}</span>
				<span class="text_slice">{{ sagittalText.slice }}</span>
				<span class="orient_top">{{ sagittalText.orient.top }}</span>
				<span class="orient_bottom">{{ sagittalText.orient.bottom }}</span>
				<span class="orient_left">{{ sagittalText.orient.left }}</span>
				<span class="orient_right">{{ sagittalText.orient.right }}</span>
			</div>
		</div>
		<div class="coronalparent" v-show="showCoronal">
			<div ref="elCoronal" class="sliceview" @contextmenu.prevent>
				<h4 class="desc">Coronal</h4>
				<span class="text_studyinfo">{{ studyInfo }}</span>
				<span class="text_wwwc">{{ coronalText.wwwc }}</span>
				<span class="text_slice">{{ coronalText.slice }}</span>
				<span class="orient_top">{{ coronalText.orient.top }}</span>
				<span class="orient_bottom">{{ coronalText.orient.bottom }}</span>
				<span class="orient_left">{{ coronalText.orient.left }}</span>
				<span class="orient_right">{{ coronalText.orient.right }}</span>
			</div>
		</div>
	</div>
</template>

<script setup name="DisplayerArea3D">
import { ref, onMounted, onBeforeUnmount, computed, reactive, watch, onUnmounted } from "vue";
import MPR from "../cornerstone3D/mprvr.js";
import { ViewportId, getDicomInfo } from "../cornerstone3D/mprvr.js";
import { useAppStore } from "../stores/appStore";
import { storeToRefs } from "pinia";

const appStore = useAppStore();
const { currentDisplayer } = storeToRefs(appStore);

let theMPR = null;
const loading = ref(false);
const layout = reactive({
	type: "layoutmprvr",
	viewIds: [],
	maxViewId: null
});

let resizeObserver = null;

const studyInfo = ref("");
const defWL = ref(40);
const defWW = ref(400);
const showAxial = ref(true);
const showSagittal = ref(true);
const showCoronal = ref(true);
const showVR = ref(true);
const currentViewportId = ref("");

const elContainer = ref(null);
const elAxial = ref(null);
const elSagittal = ref(null);
const elCoronal = ref(null);
const elVR = ref(null);

const cornerText = reactive({
	[ViewportId.AXIAL]: {
		wwwc: "",
		slice: "",
		orient: {
			top: "",
			bottom: "",
			left: "",
			right: ""
		}
	},
	[ViewportId.SAGITTAL]: {
		wwwc: "",
		slice: "",
		orient: {
			top: "",
			bottom: "",
			left: "",
			right: ""
		}
	},
	[ViewportId.CORONAL]: {
		wwwc: "",
		slice: "",
		orient: {
			top: "",
			bottom: "",
			left: "",
			right: ""
		}
	}
});

const axialText = computed(() => {
	return cornerText[ViewportId.AXIAL];
});
const sagittalText = computed(() => {
	return cornerText[ViewportId.SAGITTAL];
});

const coronalText = computed(() => {
	return cornerText[ViewportId.CORONAL];
});

const axialStyle = computed(() => {
	if (layout.type == "layoutmpr") {
		return {
			gridRowStart: 1,
			gridColumnStart: 2,
			gridRowEnd: 3,
			gridColumnEnd: 3
		};
	} else if (layout.type == "layoutmprvr") {
		return {
			gridRowStart: 1,
			gridColumnStart: 1,
			gridRowEnd: 2,
			gridColumnEnd: 2
		};
	}
});

watch(
	() => layout.type,
	(newVal, oldVal) => {
		layout.viewIds.length = 0;
		if (newVal === "layoutmpr") {
			showAxial.value = true;
			showSagittal.value = true;
			showCoronal.value = true;
			showVR.value = false;
		} else if (newVal === "layoutmprvr") {
			showAxial.value = true;
			showSagittal.value = true;
			showCoronal.value = true;
			showVR.value = true;
		}

		if (showAxial.value) {
			layout.viewIds.push(ViewportId.AXIAL);
		}
		if (showSagittal.value) {
			layout.viewIds.push(ViewportId.SAGITTAL);
		}
		if (showCoronal.value) {
			layout.viewIds.push(ViewportId.CORONAL);
		}
		if (showVR.value) {
			layout.viewIds.push(ViewportId.VOLUME);
		}

		resizeViews();
	}
);

const setLayout = type => {
	layout.type = type;
};

const renderHandler = e => {
	UpdateText(e);
};

const UpdateText = e => {
	const { viewportId } = e.detail;
	const wwwl = theMPR.getWindow(viewportId);
	const text = `WL: ${wwwl.windowCenter} WW: ${wwwl.windowWidth}`;
	const sliceCount = theMPR.getSliceCount(viewportId);
	const sliceIdx = theMPR.getSliceIndex(viewportId);
	const sliceText = `Im: ${sliceIdx + 1} / ${sliceCount}`;

	cornerText[viewportId].wwwc = text;
	cornerText[viewportId].slice = sliceText;
};

const onSelectView = e => {
	const elem = e.target.closest(".sliceview");
	if (!elem) {
		return;
	}
	currentViewportId = elem.getAttribute("data-viewport-uid");
};

const load = async () => {
	const imageIds = currentDisplayer.value.getImageIds();
	loading.value = true;
	const info = await getDicomInfo(imageIds[0]);

	studyInfo.value = info.studyInfo;
	defWL.value = info.defWL;
	defWW.value = info.defWW;

	theMPR.loadImages(imageIds).then(() => {
		loading.value = false;
	});
};

const resizeViews = () => {
	const resizeTimer = setTimeout(() => {
		theMPR.resize(layout.viewIds, layout.maxViewId);
		clearTimeout(resizeTimer);
	}, 100);
};

defineExpose({
	setLayout,
});

onMounted(() => {
	theMPR = new MPR({
		elAxial: elAxial.value,
		elSagittal: elSagittal.value,
		elCoronal: elCoronal.value,
		elVR: elVR.value
	});

	load();
	theMPR.bindRenderEvent(renderHandler);

});

onBeforeUnmount(() => {
	theMPR.destroy();
	theMPR = null;
});
</script>

<style lang="scss" scoped>
$font-size: 14px;
@mixin text() {
	position: absolute;
	color: white;
	font-size: $font-size;
	font-family: Arial, Helvetica, sans-serif;
	text-align: left;
	z-index: 10;
}

@mixin orient($text-align: left) {
	position: absolute;
	color: white;
	font-size: $font-size;
	text-align: $text-align;
	z-index: 10;
}

.container3d {
	display: grid;
	grid-template-columns: 1fr 1fr;
	grid-template-rows: 1fr 1fr;
	grid-gap: 1px 1px;
	height: 100%;
	background-color: black;
	color: #fff;
}

:deep(.el-loading-spinner) {
	font-size: 80px;
	font-weight: bold;
}

:deep(.el-loading-mask .el-loading-spinner .el-loading-text) {
	font-size: 20px;
}

.desc {
	@include text();
	top: 2px;
	left: 2px;
}

.text_studyinfo {
	@include text();
	top: 24px;
	left: 2px;
}

.text_wwwc {
	@include text();
	top: 2px;
	right: 2px;
}

.text_slice {
	@include text();
	top: 24px;
	right: 2px;
}

.orient_top {
	@include orient(center);
	top: 2px;
	left: calc(50% - 30px);
	width: 60px;
}

.orient_bottom {
	@include orient(center);
	bottom: 2px;
	left: calc(50% - 30px);
	width: 60px;
}

.orient_left {
	@include orient();
	top: calc(50% - 20px);
	left: 2px;
}

.orient_right {
	@include orient();
	top: calc(50% - 20px);
	right: 2px;
}

.axialparent {
	padding: 1px;
	border: 1px solid rgb(135, 206, 250);
	background-color: black;
}

.sagittalparent {
	padding: 1px;
	border: 1px solid red;
	background-color: black;
}

.coronalparent {
	padding: 1px;
	border: 1px solid green;
	background-color: black;
}

.vrcprparent {
	padding: 1px;
	border: 1px solid white;
	background-color: black;
}

.sliceview {
	position: relative;
	width: 100%;
	height: 100%;
}

</style>

3. mprvr.js

把官方示例中的代码整理成一个类, MPR

1)init函数

  • 创建toolGroup,管理mpr三个窗口的操作工具,添加工具WindowLevelTool、CrosshairsTool、PanTool、ZoomTool、StackScrollTool
  • 创建vrToolGroup,管理vr窗口操作工具,添加工具TrackballRotateTool、ZoomTool、PanTool
  • 创建renderingEngine
  • 定义mpr三个 ViewportType.ORTHOGRAPHIC类型窗口, 方向分别为:Enums.OrientationAxis.AXIAL,Enums.OrientationAxis.CORONAL,Enums.OrientationAxis.SAGITTAL
  • 定义vr 一个ViewportType.VOLUME_3D类型窗口,方向指定为Enums.OrientationAxis.CORONAL

2)loadImages函数

  • 加载imageIds, 生成volume数据
  • 四个窗口添加到renderingEngine
  • 激活窗口操作工具
  • 绘制窗口,设置vr窗口配色方案

3)getSliceCount, getSliceIndex 函数

  • 获取mpr窗口slice位置,显示在右上角

4) resize函数

  • 窗口尺寸变化时,调用此函数重绘,否则显示的图像比例变形

5) bindRenderEvent, bindCameraEvent

  • 用于绑定cornerstone3D 中的IMAGE_RENDERED, CAMERA_MODIFIED事件
  • IMAGE_RENDERED发生时调用UpdateText可以绘制四个角落的文字
  • CAMERA_MODIFIED发生时可能mpr的三个窗口方向发生变化,用于以后更新方位文字

6)destory函数

删除绑定元素,清理缓存

getDicomInfo函数

  • 获取姓名,性别、年龄等信息,显示在窗口右上角
import {
	RenderingEngine,
	Enums,
	setVolumesForViewports,
	volumeLoader,
	getRenderingEngine,
	eventTarget,
	getEnabledElement,
	utilities as csUtils,
	cache,
	imageLoader
} from "@cornerstonejs/core";
import { Enums as toolsEnums, utilities } from "@cornerstonejs/tools";

import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader";
import * as cornerstoneTools from "@cornerstonejs/tools";

import { readTagAsString, decodeChinese, desensitizeSubstring } from "@/utils";

import { renderingEngineId } from "../cornerstone3D";

const {
	ToolGroupManager,
	Enums: csToolsEnums,
	TrackballRotateTool,
	StackScrollTool,
	WindowLevelTool,
	PanTool,
	ZoomTool,
} = cornerstoneTools;


const { MouseBindings, KeyboardBindings } = csToolsEnums;
const { ViewportType } = Enums;
const { IMAGE_RENDERED, CAMERA_MODIFIED, VOI_MODIFIED } = Enums.Events;

const ViewportId = Object.freeze({
	AXIAL: "DICOM3D_AXIAL",
	SAGITTAL: "DICOM3D_SAGITTAL",
	CORONAL: "DICOM3D_CORONAL",
	VOLUME: "DICOM3D_VOLUME"
});

const idAxial = ViewportId.AXIAL;
const idSagittal = ViewportId.SAGITTAL;
const idCoronal = ViewportId.CORONAL;
const idVolume = ViewportId.VOLUME;

const volumeId = "DICOM3D_VOLUME_ID";
const viewportIds = [idAxial, idSagittal, idCoronal, idVolume];
const mprViewportIds = [idAxial, idSagittal, idCoronal];

const cameraSynchronizerId = "CAMERA_SYNCHRONIZER_ID";
const voiSynchronizerId = "VOI_SYNCHRONIZER_ID";
const toolGroupId = "MPR_TOOLGROUP_ID";
const vrToolGroupId = "VR_TOOL_GROUP_ID";


const viewportColors = {
	[idAxial]: "rgb(135, 206, 250)",
	[idSagittal]: "rgb(255, 0, 0)",
	[idCoronal]: "rgb(0, 255, 0)",
	[idVolume]: "rgb(128, 128, 128)"
};


const viewportReferenceLineControllable = [idAxial, idSagittal, idCoronal, idVolume];
const viewportReferenceLineDraggableRotatable = [idAxial, idSagittal, idCoronal, idVolume];
const viewportReferenceLineSlabThicknessControlsOn = [idAxial, idSagittal, idCoronal, idVolume];

function getReferenceLineColor(viewportId) {
	return viewportColors[viewportId];
}

function getReferenceLineControllable(viewportId) {
	const index = viewportReferenceLineControllable.indexOf(viewportId);
	return index !== -1;
}

function getReferenceLineDraggableRotatable(viewportId) {
	const index = viewportReferenceLineDraggableRotatable.indexOf(viewportId);
	return index !== -1;
}
function getReferenceLineSlabThicknessControlsOn(viewportId) {
	const index = viewportReferenceLineSlabThicknessControlsOn.indexOf(viewportId);
	return index !== -1;
}

export default class MPR {
	constructor(params) {
		this.toolGroup = null;
		this.vrToolGroup = null;
		this.renderingEngine = null;
		this.registered = false;
		this.viewportInputArray = null;
		this.crosshairsToolActive = true;
		this.loaded = false;
		this.selecteToolName = "";
		this.params = params;
		this.volume = null;
		this.init(params);
	}

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

		cornerstoneTools.addTool(CrosshairsTool);
		cornerstoneTools.addTool(TrackballRotateTool);
		cornerstoneTools.addTool(ZoomTool);
		cornerstoneTools.addTool(PanTool);
		cornerstoneTools.addTool(WindowLevelTool);

		this.vrToolGroup = ToolGroupManager.getToolGroup(vrToolGroupId);
		if (!this.vrToolGroup) {
			this.vrToolGroup = ToolGroupManager.createToolGroup(vrToolGroupId);
			this.vrToolGroup.addTool(TrackballRotateTool.toolName);
			this.vrToolGroup.addTool(ZoomTool.toolName, {
				zoomToCenter: true,
				invert: true,
				minZoomScale: 0.15,
				maxZoomScale: 20
			});

			this.vrToolGroup.addTool(PanTool.toolName);
			this.vrToolGroup.setToolActive(TrackballRotateTool.toolName, {
				bindings: [
					{
						mouseButton: MouseBindings.Primary // Left Click
					}
				]
			});

			this.vrToolGroup.setToolActive(ZoomTool.toolName, {
				bindings: [
					{
						mouseButton: MouseBindings.Secondary
					}
				]
			});

			this.vrToolGroup.setToolActive(PanTool.toolName, {
				bindings: [
					{
						mouseButton: MouseBindings.Auxiliary
					}
				]
			});
		}

		// Instantiate a rendering engine
		this.renderingEngine = new RenderingEngine(renderingEngineId);

		this.viewportInputArray = [
			{
				viewportId: idAxial,
				type: ViewportType.ORTHOGRAPHIC,
				element: elAxial,
				defaultOptions: {
					orientation: Enums.OrientationAxis.AXIAL,
					background: [0, 0, 0]
				}
			},
			{
				viewportId: idSagittal,
				type: ViewportType.ORTHOGRAPHIC,
				element: elSagittal,
				defaultOptions: {
					orientation: Enums.OrientationAxis.SAGITTAL,
					background: [0, 0, 0]
				}
			},
			{
				viewportId: idCoronal,
				type: ViewportType.ORTHOGRAPHIC,
				element: elCoronal,
				defaultOptions: {
					orientation: Enums.OrientationAxis.CORONAL,
					background: [0, 0, 0]
				}
			},
			{
				viewportId: idVolume,
				type: ViewportType.VOLUME_3D,
				element: elVR,
				defaultOptions: {
					background: [0, 0, 0],
					orientation: Enums.OrientationAxis.CORONAL
				}
			}
		];

		// Define tool groups to add the segmentation display tool to
		this.toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
		if (!this.toolGroup) {
			this.toolGroup = ToolGroupManager.createToolGroup(toolGroupId);
			this.toolGroup.addTool(WindowLevelTool.toolName);

			const isMobile = window.matchMedia("(any-pointer:coarse)").matches;

			this.toolGroup.addTool(CrosshairsTool.toolName, {
				getReferenceLineColor,
				getReferenceLineControllable,
				getReferenceLineDraggableRotatable,
				getReferenceLineSlabThicknessControlsOn,
				referenceLinesCenterGapRadius: 14,
				mobile: {
					enabled: isMobile,
					opacity: 0.8,
					handleRadius: 9
				}
			});

			this.addManipulationBindings(this.toolGroup);
		}

		// For the crosshairs to operate, the viewports must currently be
		// added ahead of setting the tool active. This will be improved in the future.
		this.toolGroup.addViewport(idAxial, renderingEngineId);
		this.toolGroup.addViewport(idSagittal, renderingEngineId);
		this.toolGroup.addViewport(idCoronal, renderingEngineId);
		this.vrToolGroup.addViewport(idVolume, renderingEngineId);
	}

	destroy() {
		this.renderingEngine.disableElement(idVolume);
		this.renderingEngine.disableElement(idAxial);
		this.renderingEngine.disableElement(idSagittal);
		this.renderingEngine.disableElement(idCoronal);

		this.toolGroup.removeViewports(renderingEngineId);
		this.vrToolGroup.removeViewports(renderingEngineId);
		cache.purgeCache();
	}

	addManipulationBindings(toolGroup, options = {}) {
		const zoomBindings = [
			{
				mouseButton: MouseBindings.Secondary
			}
		];

		const { is3DViewport = false, enableShiftClickZoom = false, toolMap = new Map() } = options;

		if (enableShiftClickZoom === true) {
			zoomBindings.push({
				mouseButton: MouseBindings.Primary, // Shift Left Click
				modifierKey: KeyboardBindings.Shift
			});
		}

		if (!this.registered) {
			for (const [, config] of toolMap) {
				if (config.tool) {
					cornerstoneTools.addTool(config.tool);
				}
			}
		}

		this.registered = true;

		toolGroup.addTool(PanTool.toolName);
		toolGroup.addTool(ZoomTool.toolName, {
			zoomToCenter: true,
			invert: true,
			minZoomScale: 0.1,
			maxZoomScale: 20
		});
		if (is3DViewport) {
			toolGroup.addTool(TrackballRotateTool.toolName);
		} else {
			toolGroup.addTool(StackScrollTool.toolName);
		}

		toolGroup.addTool(StackScrollTool.toolName);

		toolGroup.setToolActive(PanTool.toolName, {
			bindings: [
				{
					mouseButton: MouseBindings.Auxiliary
				},
				{
					numTouchPoints: 1,
					modifierKey: KeyboardBindings.Ctrl
				}
			]
		});
		toolGroup.setToolActive(ZoomTool.toolName, {
			bindings: zoomBindings
		});
		// Need a binding to navigate without a wheel mouse
		toolGroup.setToolActive(StackScrollTool.toolName, {
			bindings: [
				{
					mouseButton: MouseBindings.Primary,
					modifierKey: KeyboardBindings.Alt
				},
				{
					numTouchPoints: 1,
					modifierKey: KeyboardBindings.Alt
				},
				{
					mouseButton: MouseBindings.Wheel
				}
			]
		});
		
		if (is3DViewport) {
			toolGroup.setToolActive(TrackballRotateTool.toolName, {
				bindings: [
					{
						mouseButton: MouseBindings.Primary
					}
				]
			});
		} else {
			toolGroup.setToolActive(StackScrollTool.toolName);
		}

		// Add extra tools from the toolMap
		for (const [toolName, config] of toolMap) {
			if (config.baseTool) {
				if (!toolGroup.hasTool(config.baseTool)) {
					toolGroup.addTool(config.baseTool, toolMap.get(config.baseTool)?.configuration);
				}
				toolGroup.addToolInstance(toolName, config.baseTool, config.configuration);
			} else if (!toolGroup.hasTool(toolName)) {
				toolGroup.addTool(toolName, config.configuration);
			}
			if (config.passive) {
				// This can be applied during add/remove contours
				toolGroup.setToolPassive(toolName);
			}
			if (config.bindings || config.selected) {
				toolGroup.setToolActive(
					toolName,
					(config.bindings && config) || {
						bindings: [{ mouseButton: MouseBindings.Primary }]
					}
				);
			}
		}
	}

	async loadImages(imageIds) {
		let newImageIds = [...new Set(imageIds)];
		for (let i = 0; i < newImageIds.length; i++) {
			await cornerstoneDICOMImageLoader.wadouri.loadImage(newImageIds[i]).promise;
		}

		// Define a volume in memory
		this.volume = await volumeLoader.createAndCacheVolume(volumeId, {
			imageIds: newImageIds
		});

		this.renderingEngine.setViewports(this.viewportInputArray);
		this.volume.load();

		// Set volumes on the viewports
		await setVolumesForViewports(
			this.renderingEngine,
			[
				{
					volumeId,
					callback: null
				}
			],
			viewportIds
		);

		this.toolGroup.setToolActive(CrosshairsTool.toolName, {
			bindings: [{ mouseButton: MouseBindings.Primary }]
		});

		this.renderingEngine.renderViewports(viewportIds);

		const viewport = this.renderingEngine.getViewport(idVolume);
		await setVolumesForViewports(this.renderingEngine, [{ volumeId }], [idVolume]).then(() => {
			viewport.setProperties({
				preset: "CT-Coronary-Arteries-2"
			});

		});

		this.loaded = true;
	}
	
	getSliceCount(viewportId) {
		if (!this.loaded) {
			return;
		}

		const viewport = this.renderingEngine.getViewport(viewportId);
		return viewport.getNumberOfSlices();
	}
	getSliceIndex(viewportId) {
		if (!this.loaded) {
			return;
		}

		const viewport = this.renderingEngine.getViewport(viewportId);
		return viewport.getSliceIndex();
	}
	
	resize(viewIds, maxViewId) {
		if (!this.loaded) return;
		this.renderingEngine.resize(true, true);
		viewIds.forEach(viewportId => {
			const viewport = this.renderingEngine.getViewport(viewportId);
			if (viewport) {
				const presentation = viewport.getViewPresentation();
				viewport.setViewPresentation(presentation);
			}
		});

		if (maxViewId) {
			this.toolGroup.setToolDisabled(CrosshairsTool.toolName);
			this.toolGroup.setToolEnabled(WindowLevelTool.toolName);
			this.toolGroup.setToolActive(WindowLevelTool.toolName, {
				bindings: [
					{
						mouseButton: MouseBindings.Primary
					}
				]
			});
		} else {
			this.toolGroup.setToolEnabled(CrosshairsTool.toolName);
			this.toolGroup.setToolDisabled(WindowLevelTool.toolName);
			this.toolGroup.setToolActive(CrosshairsTool.toolName, {
				bindings: [{ mouseButton: MouseBindings.Primary }]
			});
		}
	}
	bindRenderEvent(callback) {
		for (let i = 0; i < this.viewportInputArray.length; i++) {
			if (this.viewportInputArray[i].viewportId !== idVolume) {
				const elem = this.viewportInputArray[i].element;
				elem.addEventListener(IMAGE_RENDERED, callback);
			}
		}
	}
	bindCameraEvent(callback) {
		for (let i = 0; i < this.viewportInputArray.length; i++) {
			if (this.viewportInputArray[i].viewportId !== idVolume) {
				const elem = this.viewportInputArray[i].element;
				elem.addEventListener(CAMERA_MODIFIED, callback);
			}
		}
	}
}

async function getDicomInfo(imageId, lang = "zh_CN") {
	const image = await imageLoader.loadAndCacheImage(imageId);
	const defWL = Math.round(image.windowCenter);
	const defWW = Math.round(image.windowWidth);

	const charset = image.data.string("x00080005") || "ISO_IR 100";
	const isUtf8 = charset.indexOf("ISO_IR 192") != -1;

	let studyInfo = "";
	let val = "";
	let tag = image.data.elements.x00100010; // 姓名
	if (tag) {
		val = readTagAsString(image.data.byteArray, tag.dataOffset, tag.length);
	}
	if (val) {
		val = decodeChinese(val, isUtf8);
		if (config.desensitize) {
			val = desensitizeSubstring(val, 1, -1);
		}
		studyInfo += val + " ";
	}

	val = image.data.string("x00101010");
	if (!val || val.length <= 0) {
		val = image.data.string("x00100030");
		if (val === undefined) val = "";
	} else {
		if (lang == "zh_CN") {
			val = val.replace(/Y|y/, "岁");
			val = val.replace(/M|m/, "月");
			val = val.replace(/D|d/, "天");
		}
	}
	val = val.replace(/^0+/g, "");
	if (val) {
		studyInfo += val + " ";
	}

	val = image.data.string("x00100040");

	if (lang == "zh_CN") {
		if (!val || val.length <= 0) tmp = "未知";
		if (val === "M" || val === "m") val = "男";
		else if (val === "F" || val === "f") val = "女";
		else val = "未知";
	}
	if (val) {
		studyInfo += val;
	}

	return {
		defWL,
		defWW,
		studyInfo
	};
}

export { presetColors, ViewportId, getDicomInfo, loadDicom };


总结

整理cornerstone3D中的官方示例 cursor3d的代码,实现mpr功能

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

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

相关文章

MySQL:游标 cursor 句柄

当我们select * from emp 可以查看所有的数据 这个数据就相当于一个数据表 游标的作用相当于一个索引 一个指针 指向每一个数据 假设说我要取出员工中薪资最高的前五名成员 就要用到limit关键字 但是这样太麻烦了 所以这里用到了游标 游标的声明&#xff1a; declare my…

二、ZooKeeper 集群部署搭建

作者&#xff1a;IvanCodes 日期&#xff1a;2025年5月24日 专栏&#xff1a;Zookeeper教程 我们这次教程将以 hadoop01 (192.168.121.131), hadoop02 (192.168.121.132), hadoop03 (192.168.121.133) 三台Linux服务器为例&#xff0c;搭建一个ZooKeeper 3.8.4集群。 一、下载…

<< C程序设计语言第2版 >> 练习1-14 打印输入中各个字符出现频度的直方图

1. 前言 本篇文章是<< C程序设计语言第2版 >> 的第1章的编程练习1-14, 个人觉得还有点意思, 所以写一篇文章来记录下. 希望可以给初学C的同学一点参考. 尤其是自学的同学, 或者觉得以前学得不好, 需要自己补充学习的同学. 和我的很多其它文章一样, 不建议自己还没实…

黑马点评双拦截器和Threadlocal实现原理

文章目录 双拦截器ThreadLocal实现原理 双拦截器 实现登录状态刷新的原因&#xff1a; ​ 防止用户会话过期&#xff1a;通过动态刷新Token有效期&#xff0c;确保活跃用户不会因固定过期时间而被强制登出 ​ 提升用户体验&#xff1a;用户无需频繁重新登录&#xff0c;只要…

港股IPO市场火爆 没有港卡如何参与港股打新?

据Wind资讯数据统计&#xff0c;今年1月1日至5月20日&#xff0c;港股共有23家企业IPO&#xff0c;较去年同期增加6家&#xff1b;IPO融资规模达600亿港元&#xff0c;较去年同期增长626.54%&#xff0c;IPO融资规模重回全球首位。 港股IPO市场持续火爆&#xff0c;不少朋友没有…

RESTful API 在前后端交互中的作用与实践

一、RESTful API 概述 RESTful&#xff08;Representational State Transfer&#xff09;API 是一种基于 HTTP 协议、面向资源的架构风格&#xff0c;旨在实现前后端的松散耦合和高效通信。它通过定义统一的资源标识、操作方法以及数据传输格式&#xff0c;为前后端提供了一种…

python打卡训练营打卡记录day35

知识点回顾&#xff1a; 三种不同的模型可视化方法&#xff1a;推荐torchinfo打印summary权重分布可视化进度条功能&#xff1a;手动和自动写法&#xff0c;让打印结果更加美观推理的写法&#xff1a;评估模式 作业&#xff1a;调整模型定义时的超参数&#xff0c;对比下效果 1…

如何评价OpenRouter这样的大模型API聚合平台?

OpenRouter通过统一接口简化多模型访问与集成的复杂性,实现一站式调用。然而,这种便利性背后暗藏三重挑战:成本控制、服务稳定性、对第三方供应商的强依赖性。 现在AI大模型火得一塌糊涂,新模型层出不穷,各有各的长处。但是对于开发者来说,挨个去对接OpenAI、谷歌、Anthr…

5.2.4 wpf中MultiBinding的使用方法

在 WPF 中,MultiBinding 允许将多个绑定(Binding)组合成一个逻辑结果,并通过一个转换器(IMultiValueConverter)处理这些值,最终影响目标属性。以下是其核心用法和示例: 核心组件: MultiBinding:定义多个绑定源的集合。 IMultiValueConverter:实现逻…

技术服务业-首套运营商网络路由5G SA测试专网搭建完成并对外提供服务

为了更好的服务蜂窝无线技术及运营商测试认证相关业务&#xff0c;搭建了技术服务业少有的5G测试专网&#xff0c;可独立灵活配置、完整端到端5G&#xff08;含RedCap、LAN&#xff09;的网络架构。 通过走真正运营商网络路由的方式&#xff0c;使终端设备的测试和运营商网络兼…

仿腾讯会议——音频服务器部分

1、中介者定义处理音频帧函数 2、 中介者实现处理音频帧函数 3、绑定函数映射 4、服务器定义音频处理函数 5、 服务器实现音频处理函数

大文件上传,对接阿里oss采用前端分片技术。完成对应需求!

最近做了一个大文件分片上传的功能&#xff0c;记录下 1. 首先是安装阿里云 oss 扩展 composer require aliyuncs/oss-sdk-php 去阿里云 oss 获取配置文件 AccessKey ID *** AccessKey Secret *** Bucket名称 *** Endpoint *** 2. 前端上传&#xff0c;对文件进行分片…

【场景分析】基于概率距离快速削减法的风光场景生成与削减方法

目录 1 主要内容 场景消减步骤 2 部分代码 3 程序结果 1 主要内容 该程序参考文献《含风光水的虚拟电厂与配电公司协调调度模型》场景消减部分模型&#xff0c;程序对风电场景进行生成并采用概率距离方法进行消减&#xff0c;程序先随机生成200个风电出力场景&#xff0c;然…

【Java Web】3.SpringBootWeb请求响应

&#x1f4d8;博客主页&#xff1a;程序员葵安 &#x1faf6;感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb; 文章目录 一、请求 1.1 postman 1.2 简单参数 1.3 实体参数 1.4 数组集合参数 1.5 日期参数 1.6 JSON参数 1.7 路径参数 二、响应 2…

单片机中断系统工作原理及定时器中断应用

文件目录 main.c #include <REGX52.H> #include "TIMER0.H" #include "KEY.H" #include "DELAY.H"//void Timer0_Init() { // TMOD 0x01; // TL0 64536 % 256; // TH0 64536 / 256; // ET0 1; // EA 1; // TR0 1; //}unsigned char…

LangGraph-agent-天气助手

用于创建agent和多代理工作流 循环&#xff08;有迭代次数&#xff09;、可控、持久 安装langgraph包 conda create --name agent python3.12 conda activate agent pip install -U langgraph pip install langchain-openai设置 windows&#xff08;>结尾&#xff09; s…

深度学习——超参数调优

第一部分&#xff1a;什么是超参数&#xff1f;为什么要调优&#xff1f; 一、参数 vs 超参数&#xff08;Parameter vs Hyperparameter&#xff09; 类型定义举例是否通过训练自动学习&#xff1f;参数&#xff08;Parameter&#xff09;是模型在训练过程中通过反向传播自动…

创建型:建造者模式

目录 1、核心思想 2、实现方式 2.1 模式结构 2.2 工作流程 2.3 实现案例 2.4 变体&#xff1a;链式建造者&#xff08;常见于多参数对象&#xff0c;无需指挥者&#xff09; 3、优缺点分析 4、适用场景 1、核心思想 目的&#xff1a;将复杂对象的构建过程与其表示分离…

UE4游戏查找本地角色数据的方法-SDK

UE4中&#xff0c;玩家的表示通常涉及以下几个类&#xff1a; APlayerController: 代表玩家的控制逻辑&#xff0c;处理输入等。 APawn: 代表玩家在世界中的实体&#xff08;比如一个角色、一辆车&#xff09;。APlayerController 控制一个 APawn。 ACharacter: APawn 的一个…

Java 连接并操作 Redis 万字详解:从 Jedis 直连到 RedisTemplate 封装,5 种方式全解析

引言 在分布式系统和高并发场景中&#xff0c;Redis 作为高性能内存数据库的地位举足轻重。对于 Java 开发者而言&#xff0c;掌握 Redis 的连接与操作是进阶必备技能。然而&#xff0c;从基础的 Jedis 原生客户端到 Spring 封装的 RedisTemplate&#xff0c;不同连接方式的原…