From 43f1de9d94c3e1b4a0ab76218039cc780bfc0bb3 Mon Sep 17 00:00:00 2001 From: mozzie Date: Thu, 12 Sep 2024 15:52:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0mpr=E9=98=85=E7=89=87?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/package.json | 1 + .../components/base/SideBarLeft/constant.tsx | 10 +- .../src/pages/Datasource/CarouselSeries.tsx | 126 --------- .../pages/Datasource/MprViewer/Crosshair.tsx | 246 ------------------ .../src/pages/Datasource/MprViewer/index.tsx | 20 -- .../util/convertMultiframeImageIds.js | 65 ----- .../util/createImageIdsAndCacheMetaData.js | 101 ------- .../util/getPixelSpacingInformation.js | 136 ---------- .../util/initCornerstoneDICOMImageLoader.js | 41 --- .../Datasource/MprViewer/util/initDemo.js | 13 - .../MprViewer/util/initProviders.js | 17 -- .../MprViewer/util/initVolumeLoader.js | 19 -- .../util/ptScalingMetaDataProvider.js | 17 -- .../MprViewer/util/removeInvalidTags.js | 33 --- apps/desktop/src/pages/Datasource/index.tsx | 135 ++++++++-- .../convertMultiframeImageIds.ts | 0 .../createImageIdsAndCacheMetaData.ts | 0 .../getPTImageIdInstanceMetadata.ts | 0 .../getPixelSpacingInformation.ts | 0 .../MprViewer/CornerstoneDicomLoader/init.ts | 4 + .../initCornerstoneDicomImageLoader.ts | 7 +- .../CornerstoneDicomLoader/initProviders.ts | 0 .../initVolumeLoader.ts | 0 .../ptScalingMetaDataProvider.ts | 0 .../removeInvalidTags.ts | 0 .../setCtTransferFunctionForVolumeActor.ts} | 0 .../Viewer/MprViewer/Crosshair.config.tsx | 35 +++ .../src/pages/Viewer/MprViewer/Crosshair.tsx | 201 ++++++++++++++ apps/desktop/src/pages/Viewer/index.tsx | 18 +- pnpm-lock.yaml | 9 + 30 files changed, 397 insertions(+), 857 deletions(-) delete mode 100644 apps/desktop/src/pages/Datasource/CarouselSeries.tsx delete mode 100644 apps/desktop/src/pages/Datasource/MprViewer/Crosshair.tsx delete mode 100644 apps/desktop/src/pages/Datasource/MprViewer/index.tsx delete mode 100644 apps/desktop/src/pages/Datasource/MprViewer/util/convertMultiframeImageIds.js delete mode 100644 apps/desktop/src/pages/Datasource/MprViewer/util/createImageIdsAndCacheMetaData.js delete mode 100644 apps/desktop/src/pages/Datasource/MprViewer/util/getPixelSpacingInformation.js delete mode 100644 apps/desktop/src/pages/Datasource/MprViewer/util/initCornerstoneDICOMImageLoader.js delete mode 100644 apps/desktop/src/pages/Datasource/MprViewer/util/initDemo.js delete mode 100644 apps/desktop/src/pages/Datasource/MprViewer/util/initProviders.js delete mode 100644 apps/desktop/src/pages/Datasource/MprViewer/util/initVolumeLoader.js delete mode 100644 apps/desktop/src/pages/Datasource/MprViewer/util/ptScalingMetaDataProvider.js delete mode 100644 apps/desktop/src/pages/Datasource/MprViewer/util/removeInvalidTags.js rename apps/desktop/src/pages/{Datasource => Viewer}/MprViewer/CornerstoneDicomLoader/convertMultiframeImageIds.ts (100%) rename apps/desktop/src/pages/{Datasource => Viewer}/MprViewer/CornerstoneDicomLoader/createImageIdsAndCacheMetaData.ts (100%) rename apps/desktop/src/pages/{Datasource => Viewer}/MprViewer/CornerstoneDicomLoader/getPTImageIdInstanceMetadata.ts (100%) rename apps/desktop/src/pages/{Datasource => Viewer}/MprViewer/CornerstoneDicomLoader/getPixelSpacingInformation.ts (100%) rename apps/desktop/src/pages/{Datasource => Viewer}/MprViewer/CornerstoneDicomLoader/init.ts (69%) rename apps/desktop/src/pages/{Datasource => Viewer}/MprViewer/CornerstoneDicomLoader/initCornerstoneDicomImageLoader.ts (88%) rename apps/desktop/src/pages/{Datasource => Viewer}/MprViewer/CornerstoneDicomLoader/initProviders.ts (100%) rename apps/desktop/src/pages/{Datasource => Viewer}/MprViewer/CornerstoneDicomLoader/initVolumeLoader.ts (100%) rename apps/desktop/src/pages/{Datasource => Viewer}/MprViewer/CornerstoneDicomLoader/ptScalingMetaDataProvider.ts (100%) rename apps/desktop/src/pages/{Datasource => Viewer}/MprViewer/CornerstoneDicomLoader/removeInvalidTags.ts (100%) rename apps/desktop/src/pages/{Datasource/MprViewer/util/setCtTransferFunctionForVolumeActor.js => Viewer/MprViewer/CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor.ts} (100%) create mode 100644 apps/desktop/src/pages/Viewer/MprViewer/Crosshair.config.tsx create mode 100644 apps/desktop/src/pages/Viewer/MprViewer/Crosshair.tsx diff --git a/apps/desktop/package.json b/apps/desktop/package.json index d034c33..166ca56 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -71,6 +71,7 @@ "@cornerstonejs/dicom-image-loader": "1.84.4", "@cornerstonejs/streaming-image-volume-loader": "1.84.4", "dicomweb-client": "0.10.4", + "@cornerstonejs/calculate-suv": "1.1.0", "dcmjs": "0.34.1" }, "devDependencies": { diff --git a/apps/desktop/src/components/base/SideBarLeft/constant.tsx b/apps/desktop/src/components/base/SideBarLeft/constant.tsx index 3f334be..abaebfc 100644 --- a/apps/desktop/src/components/base/SideBarLeft/constant.tsx +++ b/apps/desktop/src/components/base/SideBarLeft/constant.tsx @@ -1,11 +1,17 @@ import { MenuItem } from "./type"; -import { BrainCircuit, Package, HardDrive, Wrench } from "lucide-react"; +import { + BrainCircuit, + Package, + HardDrive, + Wrench, + Rotate3DIcon, +} from "lucide-react"; export const menuItems: MenuItem[] = [ { to: "/", name: "自动分析", icon: }, { to: "/datasource", name: "数据列表", icon: }, + { to: "/viewer", name: "MPR阅片", icon: }, { to: "/models", name: "模型管理", icon: }, { to: "/tools", name: "小工具", icon: }, - // { to: "/help", name: "帮助", icon: }, // { to: "/setting", name: "设置", icon: }, ]; diff --git a/apps/desktop/src/pages/Datasource/CarouselSeries.tsx b/apps/desktop/src/pages/Datasource/CarouselSeries.tsx deleted file mode 100644 index 2bfba56..0000000 --- a/apps/desktop/src/pages/Datasource/CarouselSeries.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { Card } from "@/components/ui/card"; -import { - Carousel, - CarouselContent, - CarouselItem, -} from "@/components/ui/carousel"; -import { - EllipsisIcon, - FileDown, - Rotate3DIcon, - Sparkles, - Torus, -} from "lucide-react"; -import { SeriesInfo } from "./type"; -import { Badge } from "@/components/ui/badge"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuShortcut, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -export type CarouselAction = - | "viewMpr" - | "view3D" - | "exportMeaurement" - | "viewReport"; - -interface CarouselSeriesProps { - seriesList: SeriesInfo[]; - onClickItem?: (series: SeriesInfo, action: CarouselAction) => void; -} - -export function CarouselSeries(props: CarouselSeriesProps) { - return ( -
- - - {props.seriesList.map((series) => ( - - -
-
-
-
- 序号 {series.MainDicomTags.SeriesNumber} -
-
-
- - - - - - - - props.onClickItem?.(series, "viewMpr") - } - > - MPR 阅片 - - - - - - props.onClickItem?.(series, "view3D") - } - > - 3D 重建 - - - - - - props.onClickItem?.(series, "exportMeaurement") - } - > - 导出测量 - - - - - - props.onClickItem?.(series, "viewReport") - } - > - 查看报告 - - - - - - - -
-
-
- {series.MainDicomTags.SeriesDescription} -
-
-
-
- {series.Instances.length} - {series.MainDicomTags.BodyPartExamined && ( - - {series.MainDicomTags.BodyPartExamined} - - )} -
-
-
-
- ))} -
-
-
- ); -} diff --git a/apps/desktop/src/pages/Datasource/MprViewer/Crosshair.tsx b/apps/desktop/src/pages/Datasource/MprViewer/Crosshair.tsx deleted file mode 100644 index 7771914..0000000 --- a/apps/desktop/src/pages/Datasource/MprViewer/Crosshair.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import { useEffect, useRef } from "react"; -import initDemo from "./util/initDemo.js"; -import { - RenderingEngine, - Types, - Enums, - setVolumesForViewports, - volumeLoader, - getRenderingEngine, -} from "@cornerstonejs/core"; -import * as cornerstoneTools from "@cornerstonejs/tools"; -import createImageIdsAndCacheMetaData from "./util/createImageIdsAndCacheMetaData"; -import { PublicViewportInput } from "@cornerstonejs/core/dist/types/types/IViewport.js"; -import setCtTransferFunctionForVolumeActor from "./util/setCtTransferFunctionForVolumeActor"; -import * as cornerstone from "@cornerstonejs/core"; - -const { - ToolGroupManager, - Enums: csToolsEnums, - CrosshairsTool, - StackScrollMouseWheelTool, -} = cornerstoneTools; - -const { ViewportType } = Enums; - -interface CrosshairMprProps { - children?: JSX.Element; -} - -const viewportId1 = "CT_AXIAL"; -const viewportId2 = "CT_SAGITTAL"; -const viewportId3 = "CT_CORONAL"; -const toolGroupId = "group"; - -const viewportColors = { - [viewportId1]: "rgb(200, 0, 0)", - [viewportId2]: "rgb(200, 200, 0)", - [viewportId3]: "rgb(0, 200, 0)", -}; - -const viewportReferenceLineControllable = [ - viewportId1, - viewportId2, - viewportId3, -]; - -const viewportReferenceLineDraggableRotatable = [ - viewportId1, - viewportId2, - viewportId3, -]; - -const viewportReferenceLineSlabThicknessControlsOn = [ - viewportId1, - viewportId2, - viewportId3, -]; - -// Define a unique id for the volume -const volumeName = "CT_VOLUME_ID"; // Id of the volume less loader prefix -const volumeLoaderScheme = "cornerstoneStreamingImageVolume"; // Loader id which defines which volume loader to use -const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id - -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; -} - -const renderingEngineId = "myRenderingEngine"; - -interface CrosshairMprProps { - // StudyInstanceUID: string; - // SeriesInstanceUID: string; -} - -export const CrosshairMpr = (props: CrosshairMprProps) => { - const viewportId1Ref = useRef(null); - const viewportId2Ref = useRef(null); - const viewportId3Ref = useRef(null); - const renderingEngine = useRef(); - const imageIds = useRef(); - - const run = async () => { - initDemo(); - - // Get Cornerstone imageIds for the source data and fetch metadata into RAM - imageIds.current = await createImageIdsAndCacheMetaData({ - StudyInstanceUID: - "1.2.840.113564.118796721496052.50228.637325565454648183.8", - SeriesInstanceUID: - "1.3.12.2.1107.5.1.4.73399.30000020080900171669200001479", - wadoRsRoot: "http://localhost:8042/dicom-web", - }); - - // Define a volume in memory - const volume = await volumeLoader.createAndCacheVolume(volumeId, { - imageIds: imageIds.current ?? [], - }); - - // Instantiate a rendering engine - renderingEngine.current = new RenderingEngine(renderingEngineId); - - // Create the viewports - const viewportInputArray: PublicViewportInput[] = [ - { - viewportId: viewportId1, - type: ViewportType.ORTHOGRAPHIC, - element: viewportId1Ref.current!, - defaultOptions: { - orientation: Enums.OrientationAxis.AXIAL, - background: [0, 0, 0], - }, - }, - { - viewportId: viewportId2, - type: ViewportType.ORTHOGRAPHIC, - element: viewportId2Ref.current!, - defaultOptions: { - orientation: Enums.OrientationAxis.SAGITTAL, - background: [0, 0, 0], - }, - }, - { - viewportId: viewportId3, - type: ViewportType.ORTHOGRAPHIC, - element: viewportId3Ref.current!, - defaultOptions: { - orientation: Enums.OrientationAxis.CORONAL, - background: [0, 0, 0], - }, - }, - ]; - - renderingEngine.current.setViewports(viewportInputArray); - - console.log(volume); - - // Set the volume to load - volume.load(); - - // Set volumes on the viewports - await setVolumesForViewports( - renderingEngine.current, - [ - { - volumeId, - callback: setCtTransferFunctionForVolumeActor, - }, - ], - [viewportId1, viewportId2, viewportId3] - ); - - // Define tool groups to add the segmentation display tool to - const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); - - if (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. - toolGroup.addViewport(viewportId1, renderingEngineId); - toolGroup.addViewport(viewportId2, renderingEngineId); - toolGroup.addViewport(viewportId3, renderingEngineId); - - // Manipulation Tools - toolGroup.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. - - toolGroup.addTool(CrosshairsTool.toolName, { - getReferenceLineColor, - getReferenceLineControllable, - getReferenceLineDraggableRotatable, - getReferenceLineSlabThicknessControlsOn, - }); - - toolGroup.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. - toolGroup.setToolActive(StackScrollMouseWheelTool.toolName); - } - - renderingEngine.current.renderViewports([ - viewportId1, - viewportId2, - viewportId3, - ]); - }; - - useEffect(() => { - run(); - cornerstoneTools.addTool(StackScrollMouseWheelTool); - cornerstoneTools.addTool(CrosshairsTool); - - return () => { - // TODO: 是否需要写在组件,根据页面需要 - console.log("卸载tool2"); - cornerstoneTools.removeTool(StackScrollMouseWheelTool); - cornerstoneTools.removeTool(CrosshairsTool); - }; - }, []); - - useEffect(() => { - window.addEventListener("resize", () => { - renderingEngine.current?.resize(); - }); - }, []); - - const test = () => { - // console.log(volumeLoader) - console.log(cornerstone.cache.getVolume(volumeId).getScalarData()); - }; - - useEffect(() => { - document.addEventListener(Enums.Events.VOLUME_CACHE_VOLUME_ADDED, () => { - console.log(111); - }); - }, []); - - return ( -
-
-
-
-
-
- -
- ); -}; diff --git a/apps/desktop/src/pages/Datasource/MprViewer/index.tsx b/apps/desktop/src/pages/Datasource/MprViewer/index.tsx deleted file mode 100644 index b23ee89..0000000 --- a/apps/desktop/src/pages/Datasource/MprViewer/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useEffect } from "react"; -import { SeriesInfo } from "../type"; -import { CrosshairMpr } from "./Crosshair"; - -interface MprViewerProps { - series?: SeriesInfo; -} - -function MprViewer(props: MprViewerProps) { - useEffect(() => { - if (props.series) console.log(props.series); - }, [props.series]); - return ( -
- -
- ); -} - -export default MprViewer; diff --git a/apps/desktop/src/pages/Datasource/MprViewer/util/convertMultiframeImageIds.js b/apps/desktop/src/pages/Datasource/MprViewer/util/convertMultiframeImageIds.js deleted file mode 100644 index 8f2c333..0000000 --- a/apps/desktop/src/pages/Datasource/MprViewer/util/convertMultiframeImageIds.js +++ /dev/null @@ -1,65 +0,0 @@ -import { metaData } from '@cornerstonejs/core'; -import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'; -/** - * preloads imageIds metadata in memory - **/ -async function prefetchMetadataInformation(imageIdsToPrefetch) { - for (let i = 0; i < imageIdsToPrefetch.length; i++) { - await cornerstoneDICOMImageLoader.wadouri.loadImage(imageIdsToPrefetch[i]) - .promise; - } -} - -function getFrameInformation(imageId) { - if (imageId.includes('wadors:')) { - const frameIndex = imageId.indexOf('/frames/'); - const imageIdFrameless = - frameIndex > 0 ? imageId.slice(0, frameIndex + 8) : imageId; - return { - frameIndex, - imageIdFrameless, - }; - } else { - const frameIndex = imageId.indexOf('&frame='); - let imageIdFrameless = - frameIndex > 0 ? imageId.slice(0, frameIndex + 7) : imageId; - if (!imageIdFrameless.includes('&frame=')) { - imageIdFrameless = imageIdFrameless + '&frame='; - } - return { - frameIndex, - imageIdFrameless, - }; - } -} -/** - * Receives a list of imageids possibly referring to multiframe dicom images - * and returns a list of imageid where each imageid referes to one frame. - * For each imageId representing a multiframe image with n frames, - * it will create n new imageids, one for each frame, and returns the new list of imageids - * If a particular imageid no refer to a mutiframe image data, it will be just copied into the new list - * @returns new list of imageids where each imageid represents a frame - */ -function convertMultiframeImageIds(imageIds) { - const newImageIds = []; - imageIds.forEach((imageId) => { - const { imageIdFrameless } = getFrameInformation(imageId); - const instanceMetaData = metaData.get('multiframeModule', imageId); - if ( - instanceMetaData && - instanceMetaData.NumberOfFrames && - instanceMetaData.NumberOfFrames > 1 - ) { - const NumberOfFrames = instanceMetaData.NumberOfFrames; - for (let i = 0; i < NumberOfFrames; i++) { - const newImageId = imageIdFrameless + (i + 1); - newImageIds.push(newImageId); - } - } else { - newImageIds.push(imageId); - } - }); - return newImageIds; -} - -export { convertMultiframeImageIds, prefetchMetadataInformation }; diff --git a/apps/desktop/src/pages/Datasource/MprViewer/util/createImageIdsAndCacheMetaData.js b/apps/desktop/src/pages/Datasource/MprViewer/util/createImageIdsAndCacheMetaData.js deleted file mode 100644 index 4c7852c..0000000 --- a/apps/desktop/src/pages/Datasource/MprViewer/util/createImageIdsAndCacheMetaData.js +++ /dev/null @@ -1,101 +0,0 @@ -import { api } from 'dicomweb-client'; -import dcmjs from 'dcmjs'; -import { utilities } from '@cornerstonejs/core'; -import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'; -import getPixelSpacingInformation from './getPixelSpacingInformation'; -import { convertMultiframeImageIds } from './convertMultiframeImageIds'; -import removeInvalidTags from './removeInvalidTags'; - -const { DicomMetaDictionary } = dcmjs.data; -const { calibratedPixelSpacingMetadataProvider } = utilities; - -/** -/** - * Uses dicomweb-client to fetch metadata of a study, cache it in cornerstone, - * and return a list of imageIds for the frames. - * - * Uses the app config to choose which study to fetch, and which - * dicom-web server to fetch it from. - * - * @returns {string[]} An array of imageIds for instances in the study. - */ - -export default async function createImageIdsAndCacheMetaData({ - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID = null, - wadoRsRoot, - client = null, -}) { - const SOP_INSTANCE_UID = '00080018'; - const SERIES_INSTANCE_UID = '0020000E'; - const MODALITY = '00080060'; - - const studySearchOptions = { - studyInstanceUID: StudyInstanceUID, - seriesInstanceUID: SeriesInstanceUID, - }; - - client = client || new api.DICOMwebClient({ url: wadoRsRoot }); - let instances = await client.retrieveSeriesMetadata(studySearchOptions); - - // if sop instance is provided we should filter the instances to only include the one we want - if (SOPInstanceUID) { - instances = instances.filter((instance) => { - return instance[SOP_INSTANCE_UID].Value[0] === SOPInstanceUID; - }); - } - - const modality = instances[0][MODALITY].Value[0]; - let imageIds = instances.map((instanceMetaData) => { - const SeriesInstanceUID = instanceMetaData[SERIES_INSTANCE_UID].Value[0]; - const SOPInstanceUIDToUse = - SOPInstanceUID || instanceMetaData[SOP_INSTANCE_UID].Value[0]; - - const prefix = 'wadors:'; - - const imageId = - prefix + - wadoRsRoot + - '/studies/' + - StudyInstanceUID + - '/series/' + - SeriesInstanceUID + - '/instances/' + - SOPInstanceUIDToUse + - '/frames/1'; - - cornerstoneDICOMImageLoader.wadors.metaDataManager.add( - imageId, - instanceMetaData - ); - return imageId; - }); - - // if the image ids represent multiframe information, creates a new list with one image id per frame - // if not multiframe data available, just returns the same list given - imageIds = convertMultiframeImageIds(imageIds); - - imageIds.forEach((imageId) => { - let instanceMetaData = - cornerstoneDICOMImageLoader.wadors.metaDataManager.get(imageId); - - // It was using JSON.parse(JSON.stringify(...)) before but it is 8x slower - instanceMetaData = removeInvalidTags(instanceMetaData); - - if (instanceMetaData) { - // Add calibrated pixel spacing - const metadata = DicomMetaDictionary.naturalizeDataset(instanceMetaData); - const pixelSpacing = getPixelSpacingInformation(metadata); - - if (pixelSpacing) { - calibratedPixelSpacingMetadataProvider.add(imageId, { - rowPixelSpacing: parseFloat(pixelSpacing[0]), - columnPixelSpacing: parseFloat(pixelSpacing[1]), - }); - } - } - }); - - return imageIds; -} diff --git a/apps/desktop/src/pages/Datasource/MprViewer/util/getPixelSpacingInformation.js b/apps/desktop/src/pages/Datasource/MprViewer/util/getPixelSpacingInformation.js deleted file mode 100644 index 5ddcde5..0000000 --- a/apps/desktop/src/pages/Datasource/MprViewer/util/getPixelSpacingInformation.js +++ /dev/null @@ -1,136 +0,0 @@ -// See https://github.com/OHIF/Viewers/blob/94a9067fe3d291d30e25a1bda5913511388edea2/platform/core/src/utils/metadataProvider/getPixelSpacingInformation.js - -export default function getPixelSpacingInformation(instance) { - // See http://gdcm.sourceforge.net/wiki/index.php/Imager_Pixel_Spacing - - // TODO: Add Ultrasound region spacing - // TODO: Add manual calibration - - // TODO: Use ENUMS from dcmjs - const projectionRadiographSOPClassUIDs = [ - '1.2.840.10008.5.1.4.1.1.1', // CR Image Storage - '1.2.840.10008.5.1.4.1.1.1.1', // Digital X-Ray Image Storage – for Presentation - '1.2.840.10008.5.1.4.1.1.1.1.1', // Digital X-Ray Image Storage – for Processing - '1.2.840.10008.5.1.4.1.1.1.2', // Digital Mammography X-Ray Image Storage – for Presentation - '1.2.840.10008.5.1.4.1.1.1.2.1', // Digital Mammography X-Ray Image Storage – for Processing - '1.2.840.10008.5.1.4.1.1.1.3', // Digital Intra – oral X-Ray Image Storage – for Presentation - '1.2.840.10008.5.1.4.1.1.1.3.1', // Digital Intra – oral X-Ray Image Storage – for Processing - '1.2.840.10008.5.1.4.1.1.12.1', // X-Ray Angiographic Image Storage - '1.2.840.10008.5.1.4.1.1.12.1.1', // Enhanced XA Image Storage - '1.2.840.10008.5.1.4.1.1.12.2', // X-Ray Radiofluoroscopic Image Storage - '1.2.840.10008.5.1.4.1.1.12.2.1', // Enhanced XRF Image Storage - '1.2.840.10008.5.1.4.1.1.12.3', // X-Ray Angiographic Bi-plane Image Storage Retired - ]; - - const { - PixelSpacing, - ImagerPixelSpacing, - SOPClassUID, - PixelSpacingCalibrationType, - PixelSpacingCalibrationDescription, - EstimatedRadiographicMagnificationFactor, - SequenceOfUltrasoundRegions, - } = instance; - - const isProjection = projectionRadiographSOPClassUIDs.includes(SOPClassUID); - - const TYPES = { - NOT_APPLICABLE: 'NOT_APPLICABLE', - UNKNOWN: 'UNKNOWN', - CALIBRATED: 'CALIBRATED', - DETECTOR: 'DETECTOR', - }; - - if (!isProjection) { - return PixelSpacing; - } - - if (isProjection && !ImagerPixelSpacing) { - // If only Pixel Spacing is present, and this is a projection radiograph, - // PixelSpacing should be used, but the user should be informed that - // what it means is unknown - return { - PixelSpacing, - type: TYPES.UNKNOWN, - isProjection, - }; - } else if ( - PixelSpacing && - ImagerPixelSpacing && - PixelSpacing === ImagerPixelSpacing - ) { - // If Imager Pixel Spacing and Pixel Spacing are present and they have the same values, - // then the user should be informed that the measurements are at the detector plane - return { - PixelSpacing, - type: TYPES.DETECTOR, - isProjection, - }; - } else if ( - PixelSpacing && - ImagerPixelSpacing && - PixelSpacing !== ImagerPixelSpacing - ) { - // If Imager Pixel Spacing and Pixel Spacing are present and they have different values, - // then the user should be informed that these are "calibrated" - // (in some unknown manner if Pixel Spacing Calibration Type and/or - // Pixel Spacing Calibration Description are absent) - return { - PixelSpacing, - type: TYPES.CALIBRATED, - isProjection, - PixelSpacingCalibrationType, - PixelSpacingCalibrationDescription, - }; - } else if (!PixelSpacing && ImagerPixelSpacing) { - let CorrectedImagerPixelSpacing = ImagerPixelSpacing; - if (EstimatedRadiographicMagnificationFactor) { - // Note that in IHE Mammo profile compliant displays, the value of Imager Pixel Spacing is required to be corrected by - // Estimated Radiographic Magnification Factor and the user informed of that. - // TODO: should this correction be done before all of this logic? - CorrectedImagerPixelSpacing = ImagerPixelSpacing.map( - (pixelSpacing) => - pixelSpacing / EstimatedRadiographicMagnificationFactor - ); - } else { - console.warn( - 'EstimatedRadiographicMagnificationFactor was not present. Unable to correct ImagerPixelSpacing.' - ); - } - - return { - PixelSpacing: CorrectedImagerPixelSpacing, - isProjection, - }; - } else if ( - SequenceOfUltrasoundRegions && - typeof SequenceOfUltrasoundRegions === 'object' - ) { - const { PhysicalDeltaX, PhysicalDeltaY } = SequenceOfUltrasoundRegions; - const USPixelSpacing = [PhysicalDeltaX * 10, PhysicalDeltaY * 10]; - - return { - PixelSpacing: USPixelSpacing, - }; - } else if ( - SequenceOfUltrasoundRegions && - Array.isArray(SequenceOfUltrasoundRegions) && - SequenceOfUltrasoundRegions.length > 1 - ) { - console.warn( - 'Sequence of Ultrasound Regions > one entry. This is not yet implemented, all measurements will be shown in pixels.' - ); - } else if (isProjection === false && !ImagerPixelSpacing) { - // If only Pixel Spacing is present, and this is not a projection radiograph, - // we can stop here - return { - PixelSpacing, - type: TYPES.NOT_APPLICABLE, - isProjection, - }; - } - - console.warn( - 'Unknown combination of PixelSpacing and ImagerPixelSpacing identified. Unable to determine spacing.' - ); -} diff --git a/apps/desktop/src/pages/Datasource/MprViewer/util/initCornerstoneDICOMImageLoader.js b/apps/desktop/src/pages/Datasource/MprViewer/util/initCornerstoneDICOMImageLoader.js deleted file mode 100644 index d6ef6b8..0000000 --- a/apps/desktop/src/pages/Datasource/MprViewer/util/initCornerstoneDICOMImageLoader.js +++ /dev/null @@ -1,41 +0,0 @@ -import dicomParser from 'dicom-parser'; -import * as cornerstone from '@cornerstonejs/core'; -import * as cornerstoneTools from '@cornerstonejs/tools'; -import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'; - -window.cornerstone = cornerstone; -window.cornerstoneTools = cornerstoneTools; -const { preferSizeOverAccuracy, useNorm16Texture } = - cornerstone.getConfiguration().rendering; - -export default function initCornerstoneDICOMImageLoader() { - // TODO: 此处非常的蛇皮 - cornerstone.setUseSharedArrayBuffer(false) - cornerstone.setConfiguration({ - detectGPUConfig: { - // benchmarksURL: "http://localhost:9000" - } - }) - cornerstoneDICOMImageLoader.external.cornerstone = cornerstone; - cornerstoneDICOMImageLoader.external.dicomParser = dicomParser; - cornerstoneDICOMImageLoader.configure({ - useWebWorkers: true, - decodeConfig: { - convertFloatPixelDataToInt: false, - use16BitDataType: preferSizeOverAccuracy || useNorm16Texture, - }, - }); - - const config = { - maxWebWorkers: navigator.hardwareConcurrency ? Math.min(navigator.hardwareConcurrency, 7) : 1, - startWebWorkersOnDemand: false, - taskConfiguration: { - decodeTask: { - initializeCodecsOnStartup: false, - strict: false, - }, - }, - }; - - cornerstoneDICOMImageLoader.webWorkerManager.initialize(config); -} diff --git a/apps/desktop/src/pages/Datasource/MprViewer/util/initDemo.js b/apps/desktop/src/pages/Datasource/MprViewer/util/initDemo.js deleted file mode 100644 index ec5eb4e..0000000 --- a/apps/desktop/src/pages/Datasource/MprViewer/util/initDemo.js +++ /dev/null @@ -1,13 +0,0 @@ -import initProviders from './initProviders.js'; -import initCornerstoneDICOMImageLoader from './initCornerstoneDICOMImageLoader'; -import initVolumeLoader from './initVolumeLoader'; -import { init as csRenderInit } from '@cornerstonejs/core'; -import { init as csToolsInit } from '@cornerstonejs/tools'; - -export default async function initDemo() { - initProviders(); - initCornerstoneDICOMImageLoader(); - initVolumeLoader(); - await csRenderInit(); - await csToolsInit(); -} \ No newline at end of file diff --git a/apps/desktop/src/pages/Datasource/MprViewer/util/initProviders.js b/apps/desktop/src/pages/Datasource/MprViewer/util/initProviders.js deleted file mode 100644 index 3d12f3f..0000000 --- a/apps/desktop/src/pages/Datasource/MprViewer/util/initProviders.js +++ /dev/null @@ -1,17 +0,0 @@ -import * as cornerstone from '@cornerstonejs/core'; -import ptScalingMetaDataProvider from './ptScalingMetaDataProvider'; - -const { calibratedPixelSpacingMetadataProvider } = cornerstone.utilities; - -export default function initProviders() { - cornerstone.metaData.addProvider( - ptScalingMetaDataProvider.get.bind(ptScalingMetaDataProvider), - 10000 - ); - cornerstone.metaData.addProvider( - calibratedPixelSpacingMetadataProvider.get.bind( - calibratedPixelSpacingMetadataProvider - ), - 11000 - ); -} diff --git a/apps/desktop/src/pages/Datasource/MprViewer/util/initVolumeLoader.js b/apps/desktop/src/pages/Datasource/MprViewer/util/initVolumeLoader.js deleted file mode 100644 index 22a81d6..0000000 --- a/apps/desktop/src/pages/Datasource/MprViewer/util/initVolumeLoader.js +++ /dev/null @@ -1,19 +0,0 @@ -import { volumeLoader } from '@cornerstonejs/core'; -import { - cornerstoneStreamingImageVolumeLoader, - cornerstoneStreamingDynamicImageVolumeLoader, -} from '@cornerstonejs/streaming-image-volume-loader'; - -export default function initVolumeLoader() { - volumeLoader.registerUnknownVolumeLoader( - cornerstoneStreamingImageVolumeLoader - ); - volumeLoader.registerVolumeLoader( - 'cornerstoneStreamingImageVolume', - cornerstoneStreamingImageVolumeLoader - ); - volumeLoader.registerVolumeLoader( - 'cornerstoneStreamingDynamicImageVolume', - cornerstoneStreamingDynamicImageVolumeLoader - ); -} diff --git a/apps/desktop/src/pages/Datasource/MprViewer/util/ptScalingMetaDataProvider.js b/apps/desktop/src/pages/Datasource/MprViewer/util/ptScalingMetaDataProvider.js deleted file mode 100644 index 52fa9c8..0000000 --- a/apps/desktop/src/pages/Datasource/MprViewer/util/ptScalingMetaDataProvider.js +++ /dev/null @@ -1,17 +0,0 @@ -import { utilities as csUtils } from '@cornerstonejs/core'; - -const scalingPerImageId = {}; - -function addInstance(imageId, scalingMetaData) { - const imageURI = csUtils.imageIdToURI(imageId); - scalingPerImageId[imageURI] = scalingMetaData; -} - -function get(type, imageId) { - if (type === 'scalingModule') { - const imageURI = csUtils.imageIdToURI(imageId); - return scalingPerImageId[imageURI]; - } -} - -export default { addInstance, get }; diff --git a/apps/desktop/src/pages/Datasource/MprViewer/util/removeInvalidTags.js b/apps/desktop/src/pages/Datasource/MprViewer/util/removeInvalidTags.js deleted file mode 100644 index abf7652..0000000 --- a/apps/desktop/src/pages/Datasource/MprViewer/util/removeInvalidTags.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Remove invalid tags from a metadata and return a new object. - * - * At this time it is only removing tags that has `null` or `undefined` values - * which is our main goal because that breaks when `naturalizeDataset(...)` is - * called. - * - * Validating the tag id using regex like /^[a-fA-F0-9]{8}$/ make it run - * +50% slower and looping through all characteres (split+every+Set or simple - * FOR+Set) double the time it takes to run. It is currently taking +12ms/1k - * images on average which can change depending on the machine. - * - * @param srcMetadata - source metadata - * @returns new metadata object without invalid tags - */ -function removeInvalidTags(srcMetadata) { - // Object.create(null) make it ~9% faster - const dstMetadata = Object.create(null); - const tagIds = Object.keys(srcMetadata); - let tagValue; - - tagIds.forEach((tagId) => { - tagValue = srcMetadata[tagId]; - - if (tagValue !== undefined && tagValue !== null) { - dstMetadata[tagId] = tagValue; - } - }); - - return dstMetadata; -} - -export { removeInvalidTags as default, removeInvalidTags }; diff --git a/apps/desktop/src/pages/Datasource/index.tsx b/apps/desktop/src/pages/Datasource/index.tsx index 8b562bc..e9303ca 100644 --- a/apps/desktop/src/pages/Datasource/index.tsx +++ b/apps/desktop/src/pages/Datasource/index.tsx @@ -7,16 +7,36 @@ import { } from "@/components/ui/resizable"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Card } from "@/components/ui/card"; -import { PatientInfo, SeriesInfo } from "./type"; -import { CarouselAction, CarouselSeries } from "./CarouselSeries"; +import { PatientInfo, SeriesInfo, StudyInfo } from "./type"; import { Input } from "@/components/ui/input"; import { Search } from "lucide-react"; -import MprViewer from "./MprViewer"; +import { + EllipsisIcon, + FileDown, + Rotate3DIcon, + Sparkles, + Torus, +} from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useNavigate } from "react-router-dom"; + +interface SeriesClickItem { + action: "viewMpr" | "view3D" | "exportMeaurement" | "viewReport"; + series: SeriesInfo; +} export const Datasource = () => { const rawPatientsRef = useRef([]); const [patients, setPatients] = useState([]); - const [activeSeries, setActiveSeries] = useState(); + const navigate = useNavigate(); useEffect(() => { window.ipcRenderer @@ -34,15 +54,22 @@ export const Datasource = () => { }); }, []); - const onClickSeriesItem = (series: SeriesInfo, action: CarouselAction) => { - console.log(action, series); - switch (action) { + const onClickItem = (p: SeriesClickItem) => { + const activeStudy = patients + .find((p) => p.active) + ?.children.find((s) => s.active); + const StudyInstanceUID = activeStudy?.MainDicomTags.StudyInstanceUID; + const SeriesInstanceUID = p.series.MainDicomTags.SeriesInstanceUID; + const query = `StudyInstanceUID=${StudyInstanceUID}&SeriesInstanceUID=${SeriesInstanceUID}`; + switch (p.action) { case "viewMpr": - setActiveSeries(series); + navigate(`/viewer?${query}`, { replace: true }); break; + default: break; } + console.log(StudyInstanceUID, p.action, p.series); }; const handlePatientSearch = (filterValue: string) => { @@ -158,12 +185,6 @@ export const Datasource = () => {
{study.MainDicomTags.InstitutionName}
-
- -
))} @@ -172,7 +193,91 @@ export const Datasource = () => { - +
+ +
+ {patients.find((p) => p.active) && + patients + .find((p) => p.active) + ?.children.find((study) => study.active) + ?.children.map((series) => ( + +
+
+
+
+ 序号 {series.MainDicomTags.SeriesNumber} +
+
+
+ + + + + + + + onClickItem({ + series, + action: "viewMpr", + }) + } + > + MPR 阅片 + + + + + + 3D 重建 + + + + + + 导出测量 + + + + + + 查看报告 + + + + + + + +
+
+
+ + {series.MainDicomTags.SeriesDescription} + +
+
+
+
+ + {series.Instances.length} + + {series.MainDicomTags.BodyPartExamined && ( + + {series.MainDicomTags.BodyPartExamined} + + )} +
+
+
+ ))} +
+
+
diff --git a/apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/convertMultiframeImageIds.ts b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/convertMultiframeImageIds.ts similarity index 100% rename from apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/convertMultiframeImageIds.ts rename to apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/convertMultiframeImageIds.ts diff --git a/apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/createImageIdsAndCacheMetaData.ts b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/createImageIdsAndCacheMetaData.ts similarity index 100% rename from apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/createImageIdsAndCacheMetaData.ts rename to apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/createImageIdsAndCacheMetaData.ts diff --git a/apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/getPTImageIdInstanceMetadata.ts b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/getPTImageIdInstanceMetadata.ts similarity index 100% rename from apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/getPTImageIdInstanceMetadata.ts rename to apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/getPTImageIdInstanceMetadata.ts diff --git a/apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/getPixelSpacingInformation.ts b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/getPixelSpacingInformation.ts similarity index 100% rename from apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/getPixelSpacingInformation.ts rename to apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/getPixelSpacingInformation.ts diff --git a/apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/init.ts b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/init.ts similarity index 69% rename from apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/init.ts rename to apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/init.ts index ac5fbcd..8e1d19e 100644 --- a/apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/init.ts +++ b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/init.ts @@ -1,10 +1,14 @@ +import initProviders from "./initProviders.js"; import { initCornerstoneDICOMImageLoader } from "./initCornerstoneDicomImageLoader"; import initVolumeLoader from "./initVolumeLoader"; import { init as csRenderInit } from "@cornerstonejs/core"; +import { init as csToolsInit } from "@cornerstonejs/tools"; // 入口 export const initCornerstone = async () => { + initProviders(); initCornerstoneDICOMImageLoader(); initVolumeLoader(); await csRenderInit(); + await csToolsInit(); }; diff --git a/apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/initCornerstoneDicomImageLoader.ts b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/initCornerstoneDicomImageLoader.ts similarity index 88% rename from apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/initCornerstoneDicomImageLoader.ts rename to apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/initCornerstoneDicomImageLoader.ts index da17d5d..ae1e79f 100644 --- a/apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/initCornerstoneDicomImageLoader.ts +++ b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/initCornerstoneDicomImageLoader.ts @@ -2,17 +2,18 @@ import dicomParser from "dicom-parser"; import * as cornerstone from "@cornerstonejs/core"; import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"; -window.cornerstone = cornerstone; - export const initCornerstoneDICOMImageLoader = () => { const { preferSizeOverAccuracy, useNorm16Texture } = cornerstone.getConfiguration().rendering; + cornerstone.setUseSharedArrayBuffer(false); cornerstone.setConfiguration({ detectGPUConfig: { - benchmarksURL: "http://localhost:9000", + // benchmarksURL: "http://localhost:9000", }, rendering: cornerstone.getConfiguration().rendering, + isMobile: false, + enableCacheOptimization: false, }); cornerstoneDICOMImageLoader.external.cornerstone = cornerstone; diff --git a/apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/initProviders.ts b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/initProviders.ts similarity index 100% rename from apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/initProviders.ts rename to apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/initProviders.ts diff --git a/apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/initVolumeLoader.ts b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/initVolumeLoader.ts similarity index 100% rename from apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/initVolumeLoader.ts rename to apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/initVolumeLoader.ts diff --git a/apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/ptScalingMetaDataProvider.ts b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/ptScalingMetaDataProvider.ts similarity index 100% rename from apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/ptScalingMetaDataProvider.ts rename to apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/ptScalingMetaDataProvider.ts diff --git a/apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/removeInvalidTags.ts b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/removeInvalidTags.ts similarity index 100% rename from apps/desktop/src/pages/Datasource/MprViewer/CornerstoneDicomLoader/removeInvalidTags.ts rename to apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/removeInvalidTags.ts diff --git a/apps/desktop/src/pages/Datasource/MprViewer/util/setCtTransferFunctionForVolumeActor.js b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor.ts similarity index 100% rename from apps/desktop/src/pages/Datasource/MprViewer/util/setCtTransferFunctionForVolumeActor.js rename to apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor.ts diff --git a/apps/desktop/src/pages/Viewer/MprViewer/Crosshair.config.tsx b/apps/desktop/src/pages/Viewer/MprViewer/Crosshair.config.tsx new file mode 100644 index 0000000..d109039 --- /dev/null +++ b/apps/desktop/src/pages/Viewer/MprViewer/Crosshair.config.tsx @@ -0,0 +1,35 @@ +export const renderingEngineId = "mprRenderingEngine"; + +export const viewportId1 = "CT_AXIAL"; +export const viewportId2 = "CT_SAGITTAL"; +export const viewportId3 = "CT_CORONAL"; +export const toolGroupId = "group"; + +export const viewportColors = { + [viewportId1]: "rgb(200, 0, 0)", + [viewportId2]: "rgb(200, 200, 0)", + [viewportId3]: "rgb(0, 200, 0)", +}; + +export const viewportReferenceLineControllable = [ + viewportId1, + viewportId2, + viewportId3, +]; + +export const viewportReferenceLineDraggableRotatable = [ + viewportId1, + viewportId2, + viewportId3, +]; + +export const viewportReferenceLineSlabThicknessControlsOn = [ + viewportId1, + viewportId2, + viewportId3, +]; + +// Define a unique id for the volume +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 diff --git a/apps/desktop/src/pages/Viewer/MprViewer/Crosshair.tsx b/apps/desktop/src/pages/Viewer/MprViewer/Crosshair.tsx new file mode 100644 index 0000000..32dc909 --- /dev/null +++ b/apps/desktop/src/pages/Viewer/MprViewer/Crosshair.tsx @@ -0,0 +1,201 @@ +import { useEffect, useRef } from "react"; +import * as cornerstoneTools from "@cornerstonejs/tools"; +import { PublicViewportInput } from "@cornerstonejs/core/dist/types/types/IViewport.js"; +import { createImageIdsAndCacheMetaData } from "./CornerstoneDicomLoader/createImageIdsAndCacheMetaData"; +import setCtTransferFunctionForVolumeActor from "./CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor"; +import { + RenderingEngine, + setVolumesForViewports, + volumeLoader, + Enums as CoreEnums, +} from "@cornerstonejs/core"; +import { + viewportColors, + viewportReferenceLineControllable, + viewportReferenceLineDraggableRotatable, + viewportReferenceLineSlabThicknessControlsOn, + viewportId1, + viewportId2, + viewportId3, + toolGroupId, + volumeId, + renderingEngineId, +} from "./Crosshair.config"; +import { initCornerstone } from "./CornerstoneDicomLoader/init"; + +const { ToolGroupManager, CrosshairsTool, StackScrollMouseWheelTool } = + cornerstoneTools; + +const { ViewportType } = CoreEnums; + +interface CrosshairMprProps { + children?: JSX.Element; +} + +function getReferenceLineColor(viewportId: string | number) { + return viewportColors[viewportId]; +} + +function getReferenceLineControllable(viewportId: string) { + const index = viewportReferenceLineControllable.indexOf(viewportId); + return index !== -1; +} + +function getReferenceLineDraggableRotatable(viewportId: string) { + const index = viewportReferenceLineDraggableRotatable.indexOf(viewportId); + return index !== -1; +} + +function getReferenceLineSlabThicknessControlsOn(viewportId: string) { + const index = + viewportReferenceLineSlabThicknessControlsOn.indexOf(viewportId); + return index !== -1; +} + +interface CrosshairMprProps { + StudyInstanceUID: string; + SeriesInstanceUID: string; +} + +export const CrosshairMpr = (props: CrosshairMprProps) => { + const viewportId1Ref = useRef(null); + const viewportId2Ref = useRef(null); + const viewportId3Ref = useRef(null); + const renderingEngine = useRef(); + const imageIds = useRef(); + + useEffect(() => { + const run = async () => { + imageIds.current = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: props.StudyInstanceUID, + SeriesInstanceUID: props.SeriesInstanceUID, + wadoRsRoot: "http://localhost:8042/dicom-web", + }); + + renderingEngine.current = new RenderingEngine(props.SeriesInstanceUID); + + // Define a volume in memory + const volume = await volumeLoader.createAndCacheVolume( + props.SeriesInstanceUID, + { + imageIds: imageIds.current ?? [], + } + ); + + // Create the viewports + const viewportInputArray: PublicViewportInput[] = [ + { + viewportId: viewportId1, + type: ViewportType.ORTHOGRAPHIC, + element: viewportId1Ref.current!, + defaultOptions: { + orientation: CoreEnums.OrientationAxis.AXIAL, + background: [0, 0, 0], + }, + }, + { + viewportId: viewportId2, + type: ViewportType.ORTHOGRAPHIC, + element: viewportId2Ref.current!, + defaultOptions: { + orientation: CoreEnums.OrientationAxis.SAGITTAL, + background: [0, 0, 0], + }, + }, + { + viewportId: viewportId3, + type: ViewportType.ORTHOGRAPHIC, + element: viewportId3Ref.current!, + defaultOptions: { + orientation: CoreEnums.OrientationAxis.CORONAL, + background: [0, 0, 0], + }, + }, + ]; + + renderingEngine.current.setViewports(viewportInputArray); + + // Set the volume to load + volume.load(); + + // Set volumes on the viewports + await setVolumesForViewports( + renderingEngine.current, + [ + { + volumeId: props.SeriesInstanceUID, + callback: setCtTransferFunctionForVolumeActor, + }, + ], + [viewportId1, viewportId2, viewportId3] + ); + + const toolGroup = ToolGroupManager.createToolGroup( + props.SeriesInstanceUID + ); + + if (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. + toolGroup.addViewport(viewportId1, props.SeriesInstanceUID); + toolGroup.addViewport(viewportId2, props.SeriesInstanceUID); + toolGroup.addViewport(viewportId3, props.SeriesInstanceUID); + + // Manipulation Tools + toolGroup.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. + + toolGroup.addTool(CrosshairsTool.toolName, { + getReferenceLineColor, + getReferenceLineControllable, + getReferenceLineDraggableRotatable, + getReferenceLineSlabThicknessControlsOn, + }); + + toolGroup.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. + toolGroup.setToolActive(StackScrollMouseWheelTool.toolName); + } + + renderingEngine.current.renderViewports([ + viewportId1, + viewportId2, + viewportId3, + ]); + }; + + if (!props.SeriesInstanceUID && !props.StudyInstanceUID) return; + run(); + cornerstoneTools.addTool(StackScrollMouseWheelTool); + cornerstoneTools.addTool(CrosshairsTool); + + return () => { + cornerstoneTools.removeTool(StackScrollMouseWheelTool); + cornerstoneTools.removeTool(CrosshairsTool); + ToolGroupManager.destroyToolGroup(props.SeriesInstanceUID); + }; + }, [props.StudyInstanceUID, props.SeriesInstanceUID]); + + useEffect(() => { + const resize = () => renderingEngine.current?.resize(); + window.addEventListener("resize", resize); + return () => { + window.removeEventListener("resize", resize); + }; + }, []); + + return ( +
+
+
+
+
+
+
+ ); +}; diff --git a/apps/desktop/src/pages/Viewer/index.tsx b/apps/desktop/src/pages/Viewer/index.tsx index 02778df..4a39471 100644 --- a/apps/desktop/src/pages/Viewer/index.tsx +++ b/apps/desktop/src/pages/Viewer/index.tsx @@ -1,15 +1,27 @@ import { useLocation } from "react-router-dom"; +import { initCornerstone } from "./MprViewer/CornerstoneDicomLoader/init"; +import { CrosshairMpr } from "./MprViewer/Crosshair"; +import { useEffect, useState } from "react"; export const Viewer = () => { + const [cornerstoneLoaded, setCornerstoneLoaded] = useState(false); const location = useLocation(); const queryParams = new URLSearchParams(location.search); const SeriesInstanceUID = queryParams.get("SeriesInstanceUID"); // 获取URL参数 + const StudyInstanceUID = queryParams.get("StudyInstanceUID"); // 获取URL参数 + + useEffect(() => { + initCornerstone().then(() => setCornerstoneLoaded(true)); + }, []); return (
- {SeriesInstanceUID} -

找到一个本地dicom的mpr方案

- {/* */} + {cornerstoneLoaded && StudyInstanceUID && SeriesInstanceUID && ( + + )}
); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 710000c..1123cd5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,9 @@ importers: '@ant-design/icons': specifier: ^5.4.0 version: 5.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@cornerstonejs/calculate-suv': + specifier: 1.1.0 + version: 1.1.0 '@cornerstonejs/core': specifier: 1.84.4 version: 1.84.4(@babel/preset-env@7.25.4(@babel/core@7.25.2))(autoprefixer@10.4.20(postcss@8.4.41))(webpack@5.94.0)(wslink@2.1.3) @@ -897,6 +900,10 @@ packages: resolution: {integrity: sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==} engines: {node: '>=6.9.0'} + '@cornerstonejs/calculate-suv@1.1.0': + resolution: {integrity: sha512-Q9XraiDJif9aMFArD2iEuDO/HXbcRVCqB7KfaHgDrdTTjgDFovS91Psbdim7crypRSvE6dh/+HKeFNHdvNkA6w==} + engines: {node: '>=10'} + '@cornerstonejs/codec-charls@1.2.3': resolution: {integrity: sha512-qKUe6DN0dnGzhhfZLYhH9UZacMcudjxcaLXCrpxJImT/M/PQvZCT2rllu6VGJbWKJWG+dMVV2zmmleZcdJ7/cA==} engines: {node: '>=0.14'} @@ -6298,6 +6305,8 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + '@cornerstonejs/calculate-suv@1.1.0': {} + '@cornerstonejs/codec-charls@1.2.3': {} '@cornerstonejs/codec-libjpeg-turbo-8bit@1.2.2': {}