基于cornerstone3D的dicom影像浏览器 第四章 鼠标实现翻页、放大、移动、窗宽窗位调节
先看效果
dicom鼠标调节
1.定义utils/initTools.js工具函数
import { Enums, Settings } from "@cornerstonejs/core";
import * as cornerstoneTools from "@cornerstonejs/tools";
import MyProbeTool from "./MyProbeTool.js";
const {annotation
} = cornerstoneTools;
cornerstoneTools.init()
const {ToolGroupManager,Enums: csToolsEnums,StackScrollTool,WindowLevelTool,PanTool,ZoomTool,
} = cornerstoneTools;
cornerstoneTools.addTool(MyProbeTool);cornerstoneTools.addTool(StackScrollTool);
cornerstoneTools.addTool(WindowLevelTool);
cornerstoneTools.addTool(PanTool);
cornerstoneTools.addTool(ZoomTool);const { MouseBindings } = csToolsEnums;
const toolGroupId = "tpid_2d";
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId);function initTools() {// Add tools to the tool grouptoolGroup.addTool(WindowLevelTool.toolName);toolGroup.addTool(PanTool.toolName);toolGroup.addTool(MyProbeTool.toolName);toolGroup.addTool(ZoomTool.toolName, {zoomToCenter: true,invert: true,minZoomScale: 0.1,maxZoomScale: 20,preventDefault: true});toolGroup.addTool(StackScrollTool.toolName);toolGroup.setToolActive(WindowLevelTool.toolName, {bindings: [{mouseButton: MouseBindings.Primary // Left Click}]});toolGroup.setToolActive(PanTool.toolName, {bindings: [{mouseButton: MouseBindings.Auxiliary // Middle Click}]});toolGroup.setToolActive(ZoomTool.toolName, {bindings: [{mouseButton: MouseBindings.Secondary, // Right Click}]});toolGroup.setToolActive(StackScrollTool.toolName, {bindings: [{mouseButton: MouseBindings.Wheel // Wheel Mouse}]})
}export { initTools, toolGroup };
2.MyProbeTool.js
import * as cornerstoneTools from "@cornerstonejs/tools";
import { VolumeViewport, utilities as csUtils } from "@cornerstonejs/core";
import drawHandlesSvg from "./drawHandlesSvg.js";const {Enums: csToolsEnums,ProbeTool,annotation,drawing
} = cornerstoneTools;const { ChangeTypes } = csToolsEnums;
const { getAnnotations } = annotation.state;
const { drawTextBox: drawTextBoxSvg } = drawing;class MyProbeTool extends ProbeTool {static toolName = "MyProbe";constructor(options = {}) {super(options);}/*** it is used to draw the probe annotation in each* request animation frame. It calculates the updated cached statistics if* data is invalidated and cache it.** @param enabledElement - The Cornerstone's enabledElement.* @param svgDrawingHelper - The svgDrawingHelper providing the context for drawing.*/renderAnnotation = (enabledElement, svgDrawingHelper) => {let renderStatus = false;const { viewport } = enabledElement;const { element } = viewport;let annotations = getAnnotations(this.getToolName(), element);if (!annotations?.length) {return renderStatus;}annotations = this.filterInteractableAnnotationsForElement(element,annotations);if (!annotations?.length) {return renderStatus;}const targetId = this.getTargetId(viewport);const renderingEngine = viewport.getRenderingEngine();const styleSpecifier = {toolGroupId: this.toolGroupId,toolName: this.getToolName(),viewportId: enabledElement.viewport.id};for (let i = 0; i < annotations.length; i++) {const annotation = annotations[i];const annotationUID = annotation.annotationUID;const data = annotation.data;const point = data.handles.points[0];const canvasCoordinates = viewport.worldToCanvas(point);styleSpecifier.annotationUID = annotationUID;const { color, lineWidth } = this.getAnnotationStyle({annotation,styleSpecifier});if (!data.cachedStats) {data.cachedStats = {};}if (!data.cachedStats[targetId] ||data.cachedStats[targetId].value === null) {data.cachedStats[targetId] = {Modality: null,index: null,value: null};this._calculateCachedStats(annotation,renderingEngine,enabledElement,ChangeTypes.StatsUpdated);} else if (annotation.invalidated) {this._calculateCachedStats(annotation,renderingEngine,enabledElement);// If the invalidated data is as a result of volumeViewport manipulation// of the tools, we need to invalidate the related stackViewports data if// they are not at the referencedImageId, so that// when scrolling to the related slice in which the tool were manipulated// we re-render the correct tool position. This is due to stackViewport// which doesn't have the full volume at each time, and we are only working// on one slice at a time.if (viewport instanceof VolumeViewport) {const { referencedImageId } = annotation.metadata;// invalidate all the relevant stackViewports if they are not// at the referencedImageIdfor (const targetId in data.cachedStats) {if (targetId.startsWith("imageId")) {const viewports =renderingEngine.getStackViewports();const invalidatedStack = viewports.find(vp => {// The stack viewport that contains the imageId but is not// showing it currentlyconst referencedImageURI =csUtils.imageIdToURI(referencedImageId);const hasImageURI =vp.hasImageURI(referencedImageURI);const currentImageURI = csUtils.imageIdToURI(vp.getCurrentImageId());return (hasImageURI &¤tImageURI !== referencedImageURI);});if (invalidatedStack) {delete data.cachedStats[targetId];}}}}}// If rendering engine has been destroyed while renderingif (!viewport.getRenderingEngine()) {console.warn("Rendering Engine has been destroyed");return renderStatus;}const handleGroupUID = "0";// 重写此函数drawHandlesSvg(svgDrawingHelper,annotationUID,handleGroupUID,[canvasCoordinates],{color,lineWidth,handleRadius: this.configuration.handleRadius,type: "path"});renderStatus = true;const options = this.getLinkedTextBoxStyle(styleSpecifier,annotation);if (!options.visibility) {continue;}const textLines = this.configuration.getTextLines(data, targetId);if (textLines) {const textCanvasCoordinates = [canvasCoordinates[0] + 6,canvasCoordinates[1] - 6];const textUID = "0";drawTextBoxSvg(svgDrawingHelper,annotationUID,textUID,textLines,[textCanvasCoordinates[0], textCanvasCoordinates[1]],options);}}return renderStatus;};
}export default MyProbeTool;
3.drawHandlesSvg.js
// import _getHash from "./_getHash";
// import setNewAttributesIfValid from './setNewAttributesIfValid';
// import setAttributesIfNecessary from "./setAttributesIfNecessary";function _getHash(annotationUID, drawingElementType, nodeUID) {return `${annotationUID}::${drawingElementType}::${nodeUID}`;
}function setNewAttributesIfValid(attributes, svgNode) {Object.keys(attributes).forEach(key => {const newValue = attributes[key];if (newValue !== undefined && newValue !== "") {svgNode.setAttribute(key, newValue);}});
}function setAttributesIfNecessary(attributes, svgNode) {Object.keys(attributes).forEach(key => {const currentValue = svgNode.getAttribute(key);const newValue = attributes[key];if (newValue === undefined || newValue === "") {svgNode.removeAttribute(key);} else if (currentValue !== newValue) {svgNode.setAttribute(key, newValue);}});
}function drawHandlesSvg(svgDrawingHelper,annotationUID,handleGroupUID,handlePoints,options = {}
) {handlePoints.forEach((handle, i) => {drawHandle(svgDrawingHelper,annotationUID,handleGroupUID,handle,options,i);});
}function drawHandle(svgDrawingHelper,annotationUID,handleGroupUID,handle,options = {},uniqueIndex
) {const { color, handleRadius, width, lineWidth, fill, type, opacity } =Object.assign({color: "rgb(0, 255, 0)",handleRadius: "6",width: "2",lineWidth: undefined,fill: "transparent",type: "circle", //type: 'circle|rect|path',opacity: 1},options);// for supporting both lineWidth and width optionsconst strokeWidth = lineWidth || width;// variable for the namespaceconst svgns = "http://www.w3.org/2000/svg";const svgNodeHash = _getHash(annotationUID,"handle",`hg-${handleGroupUID}-index-${uniqueIndex}`);let attributes;if (type === "circle") {attributes = {cx: `${handle[0]}`,cy: `${handle[1]}`,r: handleRadius,stroke: color,fill,"stroke-width": strokeWidth,opacity: opacity};} else if (type === "rect") {const handleRadiusFloat = parseFloat(handleRadius);const side = handleRadiusFloat * 1.5;const x = handle[0] - side * 0.5;const y = handle[1] - side * 0.5;attributes = {x: `${x}`,y: `${y}`,width: `${side}`,height: `${side}`,stroke: color,fill,"stroke-width": strokeWidth,rx: `${side * 0.1}`,opacity: opacity};} else if (type === "path") {const handleRadiusFloat = parseFloat(handleRadius);const side = handleRadiusFloat * 1.5;const x = handle[0] - side * 0.5;const y = handle[1] - side * 0.5;const d = `M ${x} ${handle[1]} L ${x + side} ${handle[1]} M ${handle[0]} ${y} L ${handle[0]} ${y + side}`;attributes = {d,stroke: color,fill,"stroke-width": strokeWidth,opacity: opacity};} else {throw new Error(`Unsupported handle type: ${type}`);}const existingHandleElement = svgDrawingHelper.getSvgNode(svgNodeHash);if (existingHandleElement) {setAttributesIfNecessary(attributes, existingHandleElement);svgDrawingHelper.setNodeTouched(svgNodeHash);} else {const newHandleElement = document.createElementNS(svgns, type);setNewAttributesIfValid(attributes, newHandleElement);svgDrawingHelper.appendNode(newHandleElement, svgNodeHash);}
}export default drawHandlesSvg;
4.在displayerArea.vue调用
import { initTools } from "@/utils/initTools";onMounted(() => {initTools()
});