系列文章目录
第一章 下载源码 运行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