feat: 打通了wadors
This commit is contained in:
parent
3d1afd7a18
commit
42d18e116c
|
@ -29,7 +29,10 @@
|
|||
"pako": "2.1.0",
|
||||
"@kitware/vtk.js": "29.2.0",
|
||||
"@cornerstonejs/dicom-image-loader": "1.41.0",
|
||||
"cornerstone-wado-image-loader": "4.13.2"
|
||||
"cornerstone-wado-image-loader": "4.13.2",
|
||||
"@cornerstonejs/core": "1.41.0",
|
||||
"dcmjs": "0.30.0",
|
||||
"dicomweb-client": "0.10.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.8",
|
||||
|
|
|
@ -12,6 +12,11 @@ export const proxyMap: TProxyMap = {
|
|||
changeOrigin: true,
|
||||
pathRewrite: { "^/dicom-web": "" },
|
||||
},
|
||||
'/wado':{
|
||||
target: "http://localhost:8042/wado",
|
||||
changeOrigin: true,
|
||||
pathRewrite: { "^/wado": "" },
|
||||
},
|
||||
"/cert": {
|
||||
target: "http://localhost:12144/",
|
||||
changeOrigin: true,
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
import { metaData } from "@cornerstonejs/core";
|
||||
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader";
|
||||
|
||||
/**
|
||||
* Preloads imageIds metadata in memory
|
||||
**/
|
||||
async function prefetchMetadataInformation(
|
||||
imageIdsToPrefetch: string[]
|
||||
): Promise<void> {
|
||||
for (let i = 0; i < imageIdsToPrefetch.length; i++) {
|
||||
await cornerstoneDICOMImageLoader.wadouri.loadImage(imageIdsToPrefetch[i])
|
||||
.promise;
|
||||
}
|
||||
}
|
||||
|
||||
interface FrameInformation {
|
||||
frameIndex: number;
|
||||
imageIdFrameless: string;
|
||||
}
|
||||
|
||||
function getFrameInformation(imageId: string): FrameInformation {
|
||||
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 += "&frame=";
|
||||
}
|
||||
return {
|
||||
frameIndex,
|
||||
imageIdFrameless,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of imageids possibly referring to multiframe dicom images
|
||||
* into a list of imageids where each imageid refers to one frame.
|
||||
* For each imageId representing a multiframe image with n frames,
|
||||
* it creates n new imageids, one for each frame, and returns the new list of imageids.
|
||||
* If a particular imageid does not refer to a multiframe 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: string[]): string[] {
|
||||
const newImageIds: string[] = [];
|
||||
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 };
|
|
@ -0,0 +1,82 @@
|
|||
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;
|
||||
|
||||
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 });
|
||||
const instances = await client.retrieveSeriesMetadata(studySearchOptions);
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
// 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."
|
||||
);
|
||||
}
|
|
@ -1,58 +1,61 @@
|
|||
import axios from "axios";
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as cornerstone from "cornerstone-core";
|
||||
import dicomParser from "dicom-parser";
|
||||
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader";
|
||||
import { Enums, RenderingEngine } from "@cornerstonejs/core";
|
||||
import initDemo from "./init.demo";
|
||||
import createImageIdsAndCacheMetaData from "./createImageIdsAndCacheMetaData";
|
||||
import {
|
||||
IStackViewport,
|
||||
PublicViewportInput,
|
||||
} from "@cornerstonejs/core/dist/types/types";
|
||||
|
||||
interface VolumeViewerProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const renderingEngineId = "myRenderingEngine";
|
||||
const viewportId = "CT_STACK";
|
||||
|
||||
const { ViewportType } = Enums;
|
||||
|
||||
const StudyInstanceUID =
|
||||
"1.2.840.113619.2.416.200043823236217877797891016883696407563"; // 你的 studyInstanceUID
|
||||
"1.2.840.113564.345049290535.9692.637552042156233117.433089";
|
||||
const SeriesInstanceUID =
|
||||
"1.2.840.113619.6.80.114374075765625.22940.1553237925965.1"; // 你的 seriesInstanceUID
|
||||
"1.3.12.2.1107.5.1.4.76315.30000021042706150001900118311";
|
||||
|
||||
export const VolumeViewer = (props: VolumeViewerProps) => {
|
||||
const dicomRef = useRef(null);
|
||||
const url = `/dicom-web/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances`;
|
||||
|
||||
useEffect(() => {
|
||||
cornerstoneDICOMImageLoader.external.cornerstone = cornerstone;
|
||||
cornerstoneDICOMImageLoader.external.dicomParser = dicomParser;
|
||||
const run = async () => {
|
||||
await initDemo();
|
||||
// Get Cornerstone imageIds and fetch metadata into RAM
|
||||
const imageIds = await createImageIdsAndCacheMetaData({
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
wadoRsRoot: "/dicom-web",
|
||||
});
|
||||
|
||||
// cornerstoneWADOImageLoader.configure({ useWebWorkers: true });
|
||||
console.log(imageIds)
|
||||
|
||||
const config = {
|
||||
maxWebWorkers: navigator.hardwareConcurrency || 1,
|
||||
startWebWorkersOnDemand: true,
|
||||
taskConfiguration: {
|
||||
decodeTask: {
|
||||
initializeCodecsOnStartup: false,
|
||||
},
|
||||
},
|
||||
const renderingEngine = new RenderingEngine(renderingEngineId);
|
||||
|
||||
const viewportInput = {
|
||||
viewportId,
|
||||
type: ViewportType.STACK,
|
||||
element: dicomRef.current,
|
||||
} as unknown as PublicViewportInput;
|
||||
|
||||
renderingEngine.enableElement(viewportInput);
|
||||
|
||||
const viewport = renderingEngine.getViewport(viewportId) as IStackViewport;
|
||||
|
||||
const stack = imageIds;
|
||||
|
||||
await viewport.setStack(stack);
|
||||
|
||||
viewport.render();
|
||||
};
|
||||
cornerstoneDICOMImageLoader.webWorkerManager.initialize(config);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
cornerstone.enable(dicomRef.current!);
|
||||
axios
|
||||
.get(url)
|
||||
.then((response) => {
|
||||
const objectUIDs = response.data.map(
|
||||
(i: any) => i["00080018"].Value[0]
|
||||
);
|
||||
objectUIDs.forEach((i: any) => {
|
||||
const imageId = `wadors:/dicom-web/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${i}/frames/1`;
|
||||
cornerstone.loadImage(imageId).then((image: any) => {
|
||||
cornerstone.displayImage(dicomRef.current!, image);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching instances:", error);
|
||||
});
|
||||
run();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import initCornerstoneDICOMImageLoader from "./initCornerstoneDICOMImageLoader";
|
||||
import { init as csRenderInit } from "@cornerstonejs/core";
|
||||
|
||||
export default async function initDemo() {
|
||||
initCornerstoneDICOMImageLoader();
|
||||
await csRenderInit();
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import dicomParser from "dicom-parser";
|
||||
import * as cornerstone from "@cornerstonejs/core";
|
||||
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader";
|
||||
|
||||
window.cornerstone = cornerstone;
|
||||
const { preferSizeOverAccuracy, useNorm16Texture } =
|
||||
cornerstone.getConfiguration().rendering;
|
||||
|
||||
export default function initCornerstoneDICOMImageLoader() {
|
||||
cornerstoneDICOMImageLoader.external.cornerstone = cornerstone;
|
||||
cornerstoneDICOMImageLoader.external.dicomParser = dicomParser;
|
||||
cornerstoneDICOMImageLoader.configure({
|
||||
useWebWorkers: true,
|
||||
decodeConfig: {
|
||||
convertFloatPixelDataToInt: false,
|
||||
use16BitDataType: preferSizeOverAccuracy || useNorm16Texture,
|
||||
},
|
||||
});
|
||||
|
||||
let maxWebWorkers = 1;
|
||||
|
||||
if (navigator.hardwareConcurrency) {
|
||||
maxWebWorkers = Math.min(navigator.hardwareConcurrency, 7);
|
||||
}
|
||||
|
||||
var config = {
|
||||
maxWebWorkers,
|
||||
startWebWorkersOnDemand: false,
|
||||
taskConfiguration: {
|
||||
decodeTask: {
|
||||
initializeCodecsOnStartup: false,
|
||||
strict: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
cornerstoneDICOMImageLoader.webWorkerManager.initialize(config);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* 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 };
|
1
apps/aorta/src/modules/Root/Viewer/VolumeViewer/type.d.ts
vendored
Normal file
1
apps/aorta/src/modules/Root/Viewer/VolumeViewer/type.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module "@cornerstonejs/dicom-image-loader";
|
|
@ -100,7 +100,7 @@ services:
|
|||
- POSTGRES_DB=postgres
|
||||
|
||||
pacs:
|
||||
image: osimis/orthanc:20.11.2
|
||||
image: osimis/orthanc:master-full
|
||||
container_name: tavi-orthanc
|
||||
depends_on:
|
||||
- postgres
|
||||
|
|
2398
pnpm-lock.yaml
2398
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user