feat: integration version

This commit is contained in:
mozzie 2024-09-18 10:12:47 +08:00
parent 3226b47790
commit 039325b768
7 changed files with 288 additions and 346 deletions

View File

@ -0,0 +1,3 @@
export const Model3DViewer = () => {
return <div>3d model</div>;
};

View File

@ -1,246 +0,0 @@
import { useEffect, useRef } from "react";
import * as cornerstoneTools from "@cornerstonejs/tools";
import { PublicViewportInput } from "@cornerstonejs/core/dist/types/types/IViewport.js";
import setCtTransferFunctionForVolumeActor from "./CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor";
import {
RenderingEngine,
setVolumesForViewports,
Enums as CoreEnums,
} from "@cornerstonejs/core";
import {
viewportColors,
viewportReferenceLineControllable,
viewportReferenceLineDraggableRotatable,
viewportReferenceLineSlabThicknessControlsOn,
viewportId1,
viewportId2,
viewportId3,
ViewportId,
} from "./Crosshair.config";
const {
ToolGroupManager,
CrosshairsTool,
StackScrollMouseWheelTool,
WindowLevelTool,
ZoomTool,
Enums: csToolsEnums,
} = cornerstoneTools;
const { MouseBindings } = csToolsEnums;
const { ViewportType } = CoreEnums;
function getReferenceLineColor(vpId: ViewportId) {
return viewportColors[vpId];
}
function getReferenceLineControllable(vpId: ViewportId) {
const index = viewportReferenceLineControllable.indexOf(vpId);
return index !== -1;
}
function getReferenceLineDraggableRotatable(vpId: ViewportId) {
const index = viewportReferenceLineDraggableRotatable.indexOf(vpId);
return index !== -1;
}
function getReferenceLineSlabThicknessControlsOn(vpId: ViewportId) {
const index = viewportReferenceLineSlabThicknessControlsOn.indexOf(vpId);
return index !== -1;
}
interface CrosshairMprProps {
wwwl: {
windowCenter: number;
windowWidth: number;
};
volumeId: string;
}
export const CrosshairMpr = (props: CrosshairMprProps) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const viewportRef_AXIAL = useRef<HTMLDivElement | null>(null);
const viewportRef_SAGITTAL = useRef<HTMLDivElement | null>(null);
const viewportRef_CORONAL = useRef<HTMLDivElement | null>(null);
const renderingEngine = useRef<RenderingEngine>();
const toolGroupRef = useRef<cornerstoneTools.Types.IToolGroup | undefined>(
undefined
);
const ts = "-" + Date.now();
const toolGroupId = "mprToolGroup" + ts;
const renderingEngineId = "mprRenderingEngine" + ts;
useEffect(() => {
cornerstoneTools.addTool(StackScrollMouseWheelTool);
cornerstoneTools.addTool(CrosshairsTool);
cornerstoneTools.addTool(WindowLevelTool);
cornerstoneTools.addTool(ZoomTool);
toolGroupRef.current = ToolGroupManager.createToolGroup(toolGroupId);
const run = async () => {
if (
!viewportRef_AXIAL.current ||
!viewportRef_SAGITTAL.current ||
!viewportRef_CORONAL.current
)
return;
renderingEngine.current = new RenderingEngine(renderingEngineId);
// Create the viewports
const viewportInputArray: PublicViewportInput[] = [
{
viewportId: viewportId1,
type: ViewportType.ORTHOGRAPHIC,
element: viewportRef_AXIAL.current,
defaultOptions: {
orientation: CoreEnums.OrientationAxis.AXIAL,
background: [0, 0, 0],
},
},
{
viewportId: viewportId2,
type: ViewportType.ORTHOGRAPHIC,
element: viewportRef_SAGITTAL.current,
defaultOptions: {
orientation: CoreEnums.OrientationAxis.SAGITTAL,
background: [0, 0, 0],
},
},
{
viewportId: viewportId3,
type: ViewportType.ORTHOGRAPHIC,
element: viewportRef_CORONAL.current,
defaultOptions: {
orientation: CoreEnums.OrientationAxis.CORONAL,
background: [0, 0, 0],
},
},
];
renderingEngine.current.setViewports(viewportInputArray);
// Set volumes on the viewports
await setVolumesForViewports(
renderingEngine.current,
[
{
volumeId: props.volumeId,
callback: ({ volumeActor }) =>
setCtTransferFunctionForVolumeActor({
volumeActor,
defaultWindowCenter: props.wwwl.windowCenter,
defaultWindowWidth: props.wwwl.windowWidth,
}),
},
],
[viewportId1, viewportId2, viewportId3]
);
if (toolGroupRef.current) {
// For the crosshairs to operate, the viewports must currently be
// added ahead of setting the tool active. This will be improved in the future.
toolGroupRef.current.addViewport(viewportId1, renderingEngineId);
toolGroupRef.current.addViewport(viewportId2, renderingEngineId);
toolGroupRef.current.addViewport(viewportId3, renderingEngineId);
/**
* zoom影像
*/
toolGroupRef.current.addTool(ZoomTool.toolName);
toolGroupRef.current.setToolActive(ZoomTool.toolName, {
bindings: [
{
mouseButton: MouseBindings.Secondary, // 鼠标中键
},
],
});
// Manipulation Tools
toolGroupRef.current.addTool(StackScrollMouseWheelTool.toolName);
// Add Crosshairs tool and configure it to link the three viewports
// These viewports could use different tool groups. See the PET-CT example
// for a more complicated used case.
toolGroupRef.current.addTool(CrosshairsTool.toolName, {
getReferenceLineColor,
getReferenceLineControllable,
getReferenceLineDraggableRotatable,
getReferenceLineSlabThicknessControlsOn,
});
toolGroupRef.current.setToolActive(CrosshairsTool.toolName, {
bindings: [{ mouseButton: 1 }],
});
// As the Stack Scroll mouse wheel is a tool using the `mouseWheelCallback`
// hook instead of mouse buttons, it does not need to assign any mouse button.
toolGroupRef.current.setToolActive(StackScrollMouseWheelTool.toolName);
toolGroupRef.current.addTool(WindowLevelTool.toolName);
toolGroupRef.current.setToolActive(WindowLevelTool.toolName, {
bindings: [
{
mouseButton: MouseBindings.Auxiliary,
},
],
});
}
renderingEngine.current.renderViewports([
viewportId1,
viewportId2,
viewportId3,
]);
};
run();
return () => {
// 禁用视口
renderingEngine.current?.disableElement(viewportId1);
renderingEngine.current?.disableElement(viewportId2);
renderingEngine.current?.disableElement(viewportId3);
// 销毁渲染引擎
renderingEngine.current?.destroy();
// 从 ToolGroupManager 中移除工具组
ToolGroupManager.destroyToolGroup(toolGroupId);
// 移出工具注册
cornerstoneTools.removeTool(StackScrollMouseWheelTool);
cornerstoneTools.removeTool(CrosshairsTool);
cornerstoneTools.removeTool(WindowLevelTool);
cornerstoneTools.removeTool(ZoomTool);
};
}, [props, renderingEngineId, toolGroupId]);
/**
* mpr resize
*/
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let resizeTimeout: NodeJS.Timeout | null = null;
const resizeObserver = new ResizeObserver(() => {
if (resizeTimeout) clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => renderingEngine.current?.resize(), 100);
});
resizeObserver.observe(container);
return () => {
if (resizeTimeout) clearTimeout(resizeTimeout);
resizeObserver.unobserve(container);
resizeObserver.disconnect();
};
}, []);
return (
<div ref={containerRef} className="w-full h-full flex flex-col">
<div className="w-full h-1/3" ref={viewportRef_AXIAL} />
<div className="w-full h-1/3" ref={viewportRef_SAGITTAL} />
<div className="w-full h-1/3" ref={viewportRef_CORONAL} />
</div>
);
};

View File

@ -34,3 +34,23 @@ export const viewportReferenceLineSlabThicknessControlsOn = [
export const volumeName = "CT_VOLUME_ID"; // Id of the volume less loader prefix
export const volumeLoaderScheme = "cornerstoneStreamingImageVolume"; // Loader id which defines which volume loader to use
export const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id
export const toolGroupMprId = "toolMprNo1";
export function getReferenceLineColor(vpId: ViewportId) {
return viewportColors[vpId];
}
export function getReferenceLineControllable(vpId: ViewportId) {
const index = viewportReferenceLineControllable.indexOf(vpId);
return index !== -1;
}
export function getReferenceLineDraggableRotatable(vpId: ViewportId) {
const index = viewportReferenceLineDraggableRotatable.indexOf(vpId);
return index !== -1;
}
export function getReferenceLineSlabThicknessControlsOn(vpId: ViewportId) {
const index = viewportReferenceLineSlabThicknessControlsOn.indexOf(vpId);
return index !== -1;
}

View File

@ -0,0 +1,2 @@
export const stackViewportId = "stackNo1";
export const toolGroupStackId = "toolStackNo1";

View File

@ -1,79 +0,0 @@
import { useEffect, useRef, useState } from "react";
import { PublicViewportInput } from "@cornerstonejs/core/dist/types/types/IViewport.js";
import { Enums as CoreEnums, RenderingEngine } from "@cornerstonejs/core";
import { IStackViewport } from "@cornerstonejs/core/dist/types/types";
import { Slider } from "@/components/ui/slider";
export interface StackViewerProps {
imageIds: string[];
}
export const StackViewer = (props: StackViewerProps) => {
const viewportStackRef = useRef<HTMLDivElement | null>(null);
const renderingEngineRef = useRef<RenderingEngine>();
const containerRef = useRef<HTMLDivElement | null>(null);
const viewportId = "stackViewport";
const renderingEngineId = "stackRenderingEngine";
useEffect(() => {
if (!viewportStackRef.current) return;
renderingEngineRef.current = new RenderingEngine(renderingEngineId);
const viewportInput: PublicViewportInput = {
viewportId,
type: CoreEnums.ViewportType.STACK, // 用于 Stack 视图
element: viewportStackRef.current,
defaultOptions: {
background: [0, 0, 0],
},
};
renderingEngineRef.current.enableElement(viewportInput);
const viewport = renderingEngineRef.current.getViewport(
viewportId
) as IStackViewport;
viewport.setStack(props.imageIds);
return () => {
renderingEngineRef.current?.disableElement(viewportId);
renderingEngineRef.current?.destroy();
};
}, [props.imageIds, renderingEngineId, viewportId]);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let resizeTimeout: NodeJS.Timeout | null = null;
const resizeObserver = new ResizeObserver(() => {
if (resizeTimeout) clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(
() => renderingEngineRef.current?.resize(),
100
);
});
resizeObserver.observe(container);
return () => {
if (resizeTimeout) clearTimeout(resizeTimeout);
resizeObserver.unobserve(container);
resizeObserver.disconnect();
};
}, []);
const onChangeIndex = (value: number[]) => {
if (renderingEngineRef.current) {
const viewport = renderingEngineRef.current.getViewport(viewportId);
(viewport as IStackViewport).setImageIdIndex(value[0]);
}
};
return (
<div ref={containerRef} className="w-full h-full flex flex-col pb-4">
<div className="w-full h-full" ref={viewportStackRef} />
<Slider
defaultValue={[0]}
max={props.imageIds.length - 1}
step={1}
onValueChange={onChangeIndex}
/>
</div>
);
};

View File

@ -1,31 +1,87 @@
import { useLocation } from "react-router-dom";
import { initCornerstone } from "./MprViewer/CornerstoneDicomLoader/init";
import { CrosshairMpr } from "./MprViewer/Crosshair";
import * as cornerstoneTools from "@cornerstonejs/tools";
import { useEffect, useRef, useState } from "react";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { StackViewer } from "./StackViewer";
import { createImageIdsAndCacheMetaData } from "./MprViewer/CornerstoneDicomLoader/createImageIdsAndCacheMetaData";
import { volumeLoader } from "@cornerstonejs/core";
import {
RenderingEngine,
setVolumesForViewports,
volumeLoader,
Enums as CoreEnums,
} from "@cornerstonejs/core";
import {
IStackViewport,
PublicViewportInput,
} from "@cornerstonejs/core/dist/types/types";
import {
getReferenceLineColor,
getReferenceLineControllable,
getReferenceLineDraggableRotatable,
getReferenceLineSlabThicknessControlsOn,
toolGroupMprId,
viewportId1,
viewportId2,
viewportId3,
volumeId,
} from "./MprViewer/index.config";
import setCtTransferFunctionForVolumeActor from "./MprViewer/CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor";
import { Slider } from "@/components/ui/slider";
import { stackViewportId, toolGroupStackId } from "./StackViewer/index.config";
import { ToolBarMenu } from "./ToolBarMenu";
import { Model3DViewer } from "./ModelViewer";
const {
ToolGroupManager,
CrosshairsTool,
StackScrollMouseWheelTool,
WindowLevelTool,
ZoomTool,
Enums: csToolsEnums,
} = cornerstoneTools;
const { MouseBindings } = csToolsEnums;
const wadoRsRoot = "http://localhost:8042/dicom-web";
const { ViewportType, OrientationAxis } = CoreEnums;
const wwwl = { windowCenter: 50, windowWidth: 850 };
export const Viewer = () => {
const [cornerstoneLoaded, setCornerstoneLoaded] = useState(false);
const volumeViewport1Ref = useRef<HTMLDivElement | null>(null);
const volumeViewport2Ref = useRef<HTMLDivElement | null>(null);
const volumeViewport3Ref = useRef<HTMLDivElement | null>(null);
const stackViewportRef = useRef<HTMLDivElement | null>(null);
/**
* index
*/
const [index, setIndex] = useState<number>(0);
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const SeriesInstanceUID = queryParams.get("SeriesInstanceUID");
const StudyInstanceUID = queryParams.get("StudyInstanceUID");
const imageIdsRef = useRef<string[]>();
const volumeId = "volume";
const wwwl = { windowCenter: 50, windowWidth: 850 };
const [imageIds, setImageIds] = useState<string[]>();
const renderingEngineId = "renderEngineNo1";
const renderingEngineRef = useRef<RenderingEngine>();
useEffect(() => {
const setImageOrderCache = async () => {
cornerstoneTools.addTool(StackScrollMouseWheelTool);
cornerstoneTools.addTool(CrosshairsTool);
cornerstoneTools.addTool(WindowLevelTool);
cornerstoneTools.addTool(ZoomTool);
const renderViewport = async () => {
if (!StudyInstanceUID || !SeriesInstanceUID) return;
if (!volumeViewport1Ref.current) return;
if (!volumeViewport2Ref.current) return;
if (!volumeViewport3Ref.current) return;
if (!stackViewportRef.current) return;
renderingEngineRef.current = new RenderingEngine(renderingEngineId);
// imageIds此时由于流式加载for mpr是错乱的图片顺序
const imageIds = await createImageIdsAndCacheMetaData({
StudyInstanceUID,
@ -37,26 +93,198 @@ export const Viewer = () => {
imageIds,
});
volume.load();
imageIdsRef.current = volume.imageIds;
const volumeViewportInput: PublicViewportInput[] = [
{
viewportId: viewportId1,
type: ViewportType.ORTHOGRAPHIC,
element: volumeViewport1Ref.current,
defaultOptions: {
orientation: OrientationAxis.AXIAL,
background: [0, 0, 0],
},
},
{
viewportId: viewportId2,
type: ViewportType.ORTHOGRAPHIC,
element: volumeViewport2Ref.current,
defaultOptions: {
orientation: OrientationAxis.SAGITTAL,
background: [0, 0, 0],
},
},
{
viewportId: viewportId3,
type: ViewportType.ORTHOGRAPHIC,
element: volumeViewport3Ref.current,
defaultOptions: {
orientation: OrientationAxis.CORONAL,
background: [0, 0, 0],
},
},
];
const stackViewportInput: PublicViewportInput = {
viewportId: stackViewportId,
type: ViewportType.STACK, // 用于 Stack 视图
element: stackViewportRef.current,
defaultOptions: {
background: [0, 0, 0],
},
};
renderingEngineRef.current.setViewports([
...volumeViewportInput,
stackViewportInput,
]);
renderingEngineRef.current.enableElement(stackViewportInput);
const stackViewport = renderingEngineRef.current.getViewport(
stackViewportId
) as IStackViewport;
stackViewport.setStack(volume.imageIds);
setImageIds(volume.imageIds);
await setVolumesForViewports(
renderingEngineRef.current,
[
{
volumeId: volumeId,
callback: ({ volumeActor }) =>
setCtTransferFunctionForVolumeActor({
volumeActor,
defaultWindowCenter: wwwl.windowCenter,
defaultWindowWidth: wwwl.windowWidth,
}),
},
],
[viewportId1, viewportId2, viewportId3]
);
const toolGroupMpr = ToolGroupManager.createToolGroup(toolGroupMprId);
if (toolGroupMpr) {
toolGroupMpr.addViewport(viewportId1, renderingEngineId);
toolGroupMpr.addViewport(viewportId2, renderingEngineId);
toolGroupMpr.addViewport(viewportId3, renderingEngineId);
toolGroupMpr.addTool(ZoomTool.toolName);
toolGroupMpr.setToolActive(ZoomTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Secondary }],
});
toolGroupMpr.addTool(StackScrollMouseWheelTool.toolName);
toolGroupMpr.addTool(CrosshairsTool.toolName, {
getReferenceLineColor,
getReferenceLineControllable,
getReferenceLineDraggableRotatable,
getReferenceLineSlabThicknessControlsOn,
});
toolGroupMpr.setToolActive(CrosshairsTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Primary }],
});
toolGroupMpr.setToolActive(StackScrollMouseWheelTool.toolName);
toolGroupMpr.addTool(WindowLevelTool.toolName);
toolGroupMpr.setToolActive(WindowLevelTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Auxiliary }],
});
}
const toolGroupStack = ToolGroupManager.createToolGroup(toolGroupStackId);
if (toolGroupStack) {
toolGroupStack.addViewport(stackViewportId, renderingEngineId);
toolGroupStack.addTool(ZoomTool.toolName);
toolGroupStack.setToolActive(ZoomTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Secondary }],
});
toolGroupStack.addTool(WindowLevelTool.toolName);
toolGroupStack.setToolActive(WindowLevelTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Auxiliary }],
});
}
// 默认windowWidtth
const { windowCenter, windowWidth } = volume.cornerstoneImageMetaData;
console.log("默认窗宽/位: ", windowCenter, windowWidth);
setCornerstoneLoaded(true);
};
initCornerstone(() => {
setImageOrderCache();
renderViewport();
});
return () => {
renderingEngineRef.current?.disableElement(stackViewportId);
renderingEngineRef.current?.disableElement(viewportId1);
renderingEngineRef.current?.disableElement(viewportId2);
renderingEngineRef.current?.disableElement(viewportId3);
renderingEngineRef.current?.destroy();
ToolGroupManager.destroyToolGroup(toolGroupMprId);
ToolGroupManager.destroyToolGroup(toolGroupStackId);
cornerstoneTools.removeTool(StackScrollMouseWheelTool);
cornerstoneTools.removeTool(CrosshairsTool);
cornerstoneTools.removeTool(WindowLevelTool);
cornerstoneTools.removeTool(ZoomTool);
};
}, [SeriesInstanceUID, StudyInstanceUID]);
const onChangeIndex = (value: number[]) => {
setIndex(value[0]);
};
useEffect(
() => setIndex(imageIds ? Math.floor(imageIds.length / 2) : 0),
[imageIds]
);
/**
*
*/
useEffect(() => {
const handleWheel = (event: WheelEvent) => {
console.log("wheel");
if (!imageIds) return;
const delta = event.deltaY > 0 ? -1 : 1;
if (delta === -1 && index === 0) return;
if (delta === 1 && index === imageIds?.length - 1) return;
setIndex((p) => p + delta);
};
const stackElement = stackViewportRef.current;
stackElement?.addEventListener("wheel", handleWheel);
return () => {
stackElement?.removeEventListener("wheel", handleWheel);
};
}, [imageIds, index]);
useEffect(() => {
if (renderingEngineRef.current) {
const viewport = renderingEngineRef.current.getViewport(stackViewportId);
(viewport as IStackViewport)?.setImageIdIndex(index);
}
}, [index]);
useEffect(() => {
const container = stackViewportRef.current;
if (!container) return;
let resizeTimeout: NodeJS.Timeout | null = null;
const resizeObserver = new ResizeObserver(() => {
if (resizeTimeout) clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(
() => renderingEngineRef.current?.resize(),
100
);
});
resizeObserver.observe(container);
return () => {
if (resizeTimeout) clearTimeout(resizeTimeout);
resizeObserver.unobserve(container);
resizeObserver.disconnect();
};
}, []);
return (
<div className="w-full h-full flex flex-col">
{/* <div className="flex-shrink-0 border-b border-secondary">
<div className="flex-shrink-0 border-b border-secondary">
<ToolBarMenu />
</div> */}
</div>
<div className="flex-grow">
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
<ResizablePanel defaultSize={50}>
@ -66,24 +294,38 @@ export const Viewer = () => {
className="w-full h-full"
>
<ResizablePanel defaultSize={50}>
<div className="w-full h-full">
{cornerstoneLoaded && imageIdsRef.current && (
<StackViewer imageIds={imageIdsRef.current} />
<div className="w-full h-full flex flex-col relative">
<div ref={stackViewportRef} className="w-full flex-grow" />
{imageIds && (
<div className="absolute w-1/2 left-1/2 transform -translate-x-1/2 bottom-4">
<div>
{index + 1}/{imageIds.length}
</div>
<Slider
value={[index]}
min={0}
max={imageIds.length - 1}
step={1}
onValueChange={onChangeIndex}
/>
</div>
)}
</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={50}>
<div>bototm</div>
<div className="w-full h-full">
<Model3DViewer />
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={50}>
{cornerstoneLoaded && imageIdsRef.current && (
<CrosshairMpr wwwl={wwwl} volumeId={volumeId} />
)}
<div className="w-full h-1/3" ref={volumeViewport1Ref} />
<div className="w-full h-1/3" ref={volumeViewport2Ref} />
<div className="w-full h-1/3" ref={volumeViewport3Ref} />
</ResizablePanel>
</ResizablePanelGroup>
</div>