feat: 打通了wadors

This commit is contained in:
mozzie 2023-12-19 16:56:25 +08:00
parent 3d1afd7a18
commit 42d18e116c
12 changed files with 920 additions and 1948 deletions

View File

@ -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",

View File

@ -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,

View File

@ -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 };

View File

@ -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;
}

View File

@ -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."
);
}

View File

@ -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`;
const run = async () => {
await initDemo();
// Get Cornerstone imageIds and fetch metadata into RAM
const imageIds = await createImageIdsAndCacheMetaData({
StudyInstanceUID,
SeriesInstanceUID,
wadoRsRoot: "/dicom-web",
});
console.log(imageIds)
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();
};
useEffect(() => {
cornerstoneDICOMImageLoader.external.cornerstone = cornerstone;
cornerstoneDICOMImageLoader.external.dicomParser = dicomParser;
// cornerstoneWADOImageLoader.configure({ useWebWorkers: true });
const config = {
maxWebWorkers: navigator.hardwareConcurrency || 1,
startWebWorkersOnDemand: true,
taskConfiguration: {
decodeTask: {
initializeCodecsOnStartup: false,
},
},
};
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 (

View File

@ -0,0 +1,7 @@
import initCornerstoneDICOMImageLoader from "./initCornerstoneDICOMImageLoader";
import { init as csRenderInit } from "@cornerstonejs/core";
export default async function initDemo() {
initCornerstoneDICOMImageLoader();
await csRenderInit();
}

View File

@ -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);
}

View File

@ -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 };

View File

@ -0,0 +1 @@
declare module "@cornerstonejs/dicom-image-loader";

View File

@ -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

File diff suppressed because it is too large Load Diff