基于cornerstone3D的dicom影像浏览器 第十四章 参考线、同步调窗、同步缩放、同步移动

发布于:2025-05-19 ⋅ 阅读:(21) ⋅ 点赞:(0)

系列文章目录

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



前言

本章介绍cornerstonjs ReferenceLinesTool实现参考线显示,以及在viewport响应VOI_MODIFIED事件实现同步调窗,响应CAMERA_MODIFIED实现同步缩放、同步移动。
效果如下:
在这里插入图片描述


一、参考线

1. initTool.js中添加ReferenceLinesTool

function initTools() {
	// 操作
	cornerstoneTools.addTool(WindowLevelTool);
	cornerstoneTools.addTool(PanTool);
	cornerstoneTools.addTool(ZoomTool);
	cornerstoneTools.addTool(StackScrollTool);

	// 旋转
	cornerstoneTools.addTool(PlanarRotateTool);

	// 测量工具
	cornerstoneTools.addTool(ProbeTool);
	cornerstoneTools.addTool(MyProbeTool);
	cornerstoneTools.addTool(LengthTool);
	...
	// 参考线
	cornerstoneTools.addTool(ReferenceLinesTool);

	// Add tools to the tool group
    ...

	// 参考线
	toolGroup.addTool(ReferenceLinesTool.toolName, {
		sourceViewportId: ""
	});

	toolGroup.setToolEnabled(ReferenceLinesTool.toolName);
	...

}

2. 鼠标点击Displayer时切换ReferenceLinesTool sourceViewportId

1) initTool中增加函数setReferenceSource

function setReferenceSource(viewportId) {
	const renderingEngine = getRenderingEngine(renderingEngineId);
	const viewport = renderingEngine.getViewport(viewportId);
	if (!viewport) {
		console.warn("setReferenceSource: viewport not found", viewportId);
		return;
	}

	const data = viewport.getImageData();
	if (!data) {
		console.warn("setReferenceSource: image data not found", viewportId);
		return;
	}

	const r = toolGroup.setToolConfiguration(ReferenceLinesTool.toolName, {
		sourceViewportId: viewportId,
		showFullDimension: true
	});
	toolGroup.setToolEnabled(ReferenceLinesTool.toolName);
}

2) Displayer中导出getViewportId函数

const getViewportId = () => {
    return viewportId;
};

defineExpose({
    ...
    getViewportId,
});

3) DisplayerArea, onSelected函数中调用setReferenceSource

import { setReferenceSource } from "../cornerstone3D/initTools";
const onSelected = ({ pos }) => {
    ...

    if (dispRefs[pos].isLoaded()) {
        setReferenceSource(dispRefs[pos].getViewportId());
    }
};

二、同步操作的界面与数据绑定

1. appStore.js 中添加状态数据

import { ref, reactive, computed } from "vue";
import { defineStore } from "pinia";

export const useAppStore = defineStore("app", () => {
	const layout = reactive({
		row: 1,
		col: 1
	});
	...
	const syncOperation = reactive({
		syncWindow: false,
		syncPan: false,
		syncZoom: false
	});

	return {
		layout,
		...
		syncOperation
	};
});

2. 工具栏Toolbar添加checkbox,并与syncOperation绑定

<script lang="js" setup name="Toolbar">
import { useAppStore } from "../stores/appStore";
import { storeToRefs } from "pinia";

const appStore = useAppStore();
const { layout, syncOperation } = storeToRefs(appStore);
...
</script>

<template>
	<div class="toolbar">
		...
		<div class="toolbar-row">
			<el-checkbox
				v-model="syncOperation.syncWindow"
				label="同步调窗"
				size="large"
			/>
			<el-checkbox
				v-model="syncOperation.syncZoom"
				label="同步缩放"
				size="large"
			/>
			<el-checkbox
				v-model="syncOperation.syncPan"
				label="同步移动"
				size="large"
			/>
		</div>
	</div>
</template>

三、同步调窗

1. Displayer 响应 VOI_MODIFIED 事件

const { IMAGE_RENDERED, CAMERA_MODIFIED, VOI_MODIFIED } = Enums.Events;
onMounted(() => {
    ...
    displayer.value.addEventListener(VOI_MODIFIED, onVoiModified);
});

onBeforeUnmount(() => {
	...
    displayer.value.removeEventListener(VOI_MODIFIED, onVoiModified);

});

2. 同步的程序流程

  • 工具栏勾选 “同步调窗” -> syncOperation.syncWindow = true
  • 鼠标点击任意Displayer,并调窗,appStore.currentDisplayer记录当前Displayer
  • Displayer onVoiModified中判断需要同步调窗,从event.detail.range获取当前窗值,mitt发送“syncWindow”到DisplayerArea
  • DisplayerArea 调用除当前Displayer以外所有Displayer的setVoiRange函数设置相同窗值

1) Displayer

import { ref, reactive, onMounted, computed, watch, onBeforeUnmount, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const onVoiModified = (event) => {
    if (!appStore.currentDisplayer) return;

    const isCurrent = (appStore.currentDisplayer.getViewportId() === viewportId);
    if (isCurrent && appStore.syncOperation.syncWindow) {
        proxy.emitter.emit("syncWindow", 
        					{ 
        						range: event.detail.range, 
        						pos: props.pos 
        					});
    }
};

const setVoiRange = (newRange) => {
    if (!state.image) return;
    state.viewport.setProperties({
        voiRange: newRange,
    });

    state.viewport.render();
};

defineExpose({
    ...
    setVoiRange,
});

2) DisplayerArea

const { proxy } = getCurrentInstance()
const syncVoiRange = (data) => {
    for (let i = 0; i < dispRefs.length; i++) {
        if (i !== data.pos) {
            const disp = dispRefs[i];
            disp.setVoiRange(data.range);
        }
    }
}

onMounted(() => {
    const { row, col } = config.defLayout;
    layout.value.col = col;
    layout.value.row = row;
    setLayout(row, col);

    proxy.emitter.on("syncWindow", syncVoiRange);
    proxy.emitter.on("syncZoom", syncZoom);
    proxy.emitter.on("syncPan", syncPan);
});

onUnmounted(() => {
    proxy.emitter.off("syncWindow", syncVoiRange);
    proxy.emitter.off("syncZoom", syncZoom);
    proxy.emitter.off("syncPan", syncPan);
});

3) 全局mitt使用

  • 安装mitt, npm install mitt
  • main.js中导入
import mitt from "mitt";
app.config.globalProperties.emitter = mitt();
  • 在组件中使用
import { getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();

// Displayer中发送
proxy.emitter.emit("syncWindow", 
        					{ 
        						range: event.detail.range, 
        						pos: props.pos 
        					});

// DispalyerArea中接受
proxy.emitter.on("syncWindow", syncVoiRange);

四、同步缩放、同步移动

同步缩放、同步移动与同步调窗流程基本一样,区别是
同步调窗从响应VOI_MODIFIED开始,同步缩放、同步移动都是从响应CAMERA_MODIFIED开始
同步缩放 state.viewport.getZoom获取当前缩放系数,mitt发送“syncZoom”到DisplayerArea
同步移动 state.viewport.getPan获取当前位置,mitt发送“syncPan”到DisplayerArea

1. Displayer

const onCameraModified = (event) => {
    if (!state.series) return;
    const { camera, previousCamera } = event.detail;
    ...

    if (!appStore.currentDisplayer) return;
    const isCurrent = (appStore.currentDisplayer.getViewportId() === viewportId);

    // 需要同步缩放
    if (isCurrent && appStore.syncOperation.syncZoom) {
        const zoom = state.viewport.getZoom();
        proxy.emitter.emit("syncZoom", { zoom, pos: props.pos });
    }

    // 需要同步移动
    if (isCurrent && appStore.syncOperation.syncPan) {
        const pan = state.viewport.getPan();
        proxy.emitter.emit("syncPan", { pan, pos: props.pos });
    }

};

const setZoom = (zoom) => {
    if (!state.image) return;
    state.viewport.setZoom(zoom);
    state.viewport.render();
};

const setPan = (pan) => {
    if (!state.image) return;
    state.viewport.setPan(pan);
    state.viewport.render();
};

defineExpose({
    ...
    setVoiRange,
    setZoom,
    setPan,
});

2. DisplayerArea

const syncZoom = (data) => {
    for (let i = 0; i < dispRefs.length; i++) {
        if (i !== data.pos) {
            const disp = dispRefs[i];
            disp.setZoom(data.zoom);
        }
    }
}

const syncPan = (data) => {
    for (let i = 0; i < dispRefs.length; i++) {
        if (i !== data.pos) {
            const disp = dispRefs[i];
            disp.setPan(data.pan);
        }
    }
}


总结

  • 参考线 添加ReferenceLinesTool
  • 同步调窗 VOI_MODIFIED,viewport.setProperties
  • 同步缩放 CAMERA_MODIFIED, viewport.getZoom,viewport.setZoom
  • 同步移动 CAMERA_MODIFIED, viewport.getPan,viewport.setPan

网站公告

今日签到

点亮在社区的每一天
去签到