From c04884ffd3ab8b516fe47ce2d17931922f3e2a2c Mon Sep 17 00:00:00 2001 From: mozzie Date: Wed, 20 Dec 2023 10:24:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E4=BA=86demo=20ts?= =?UTF-8?q?=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/aorta/src/global.d.ts | 5 + .../createImageIdsAndCacheMetaData.ts | 126 ++++++++++-------- .../getPixelSpacingInformation.ts | 78 ++++++----- .../Root/Viewer/VolumeViewer/index.tsx | 2 - .../Viewer/VolumeViewer/removeInvalidTags.ts | 30 +++-- .../Root/Viewer/VolumeViewer/type.d.ts | 1 + 6 files changed, 132 insertions(+), 110 deletions(-) create mode 100644 apps/aorta/src/global.d.ts diff --git a/apps/aorta/src/global.d.ts b/apps/aorta/src/global.d.ts new file mode 100644 index 0000000..3c42f82 --- /dev/null +++ b/apps/aorta/src/global.d.ts @@ -0,0 +1,5 @@ +declare global { + interface Window { + cornerstone: any; + } +} diff --git a/apps/aorta/src/modules/Root/Viewer/VolumeViewer/createImageIdsAndCacheMetaData.ts b/apps/aorta/src/modules/Root/Viewer/VolumeViewer/createImageIdsAndCacheMetaData.ts index 0a790dc..57f7abc 100644 --- a/apps/aorta/src/modules/Root/Viewer/VolumeViewer/createImageIdsAndCacheMetaData.ts +++ b/apps/aorta/src/modules/Root/Viewer/VolumeViewer/createImageIdsAndCacheMetaData.ts @@ -9,74 +9,84 @@ 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"; +interface CreateImageIdsAndCacheMetaDataOptions { + StudyInstanceUID: string; + SeriesInstanceUID: string; + SOPInstanceUID?: string | null; + wadoRsRoot: string; + client?: api.DICOMwebClient | null; +} +const SOP_INSTANCE_UID = "00080018"; +const SERIES_INSTANCE_UID = "0020000E"; +// 数据扫描设备 CT、CTA +const MODALITY = "00080060"; + +/** + * 从 DICOMweb 服务创建图像 ID 并缓存相关元数据。 + * + * @param options - 包含图像和 DICOMweb 服务信息的对象 + * @returns 图像 ID 数组 + */ +export default async function createImageIdsAndCacheMetaData( + options: CreateImageIdsAndCacheMetaDataOptions +): Promise { const studySearchOptions = { - studyInstanceUID: StudyInstanceUID, - seriesInstanceUID: SeriesInstanceUID, + studyInstanceUID: options.StudyInstanceUID, + seriesInstanceUID: options.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 client = + options.client || + new api.DICOMwebClient({ url: options.wadoRsRoot, singlepart: true }); - const prefix = "wadors:"; + try { + // TODO: 取第一张的metadata看下扫描设备,加上限制,如果不满足条件,就别缓存了 + const instances = await client.retrieveSeriesMetadata(studySearchOptions); - const imageId = - prefix + - wadoRsRoot + - "/studies/" + - StudyInstanceUID + - "/series/" + - SeriesInstanceUID + - "/instances/" + - SOPInstanceUIDToUse + - "/frames/1"; + const modality = (instances[0] as unknown as any)[MODALITY].Value[0]; + console.log("modality", modality); + let imageIds = instances.map((instanceMetaData: any) => { + const SeriesInstanceUID = instanceMetaData[SERIES_INSTANCE_UID].Value[0]; + const SOPInstanceUIDToUse = + options.SOPInstanceUID || instanceMetaData[SOP_INSTANCE_UID].Value[0]; - cornerstoneDICOMImageLoader.wadors.metaDataManager.add( - imageId, - instanceMetaData - ); - return imageId; - }); + const prefix = "wadors:"; + const imageId = `${prefix}${options.wadoRsRoot}/studies/${options.StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUIDToUse}/frames/1`; - // 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); + cornerstoneDICOMImageLoader.wadors.metaDataManager.add( + imageId, + instanceMetaData + ); + return imageId; + }); - imageIds.forEach((imageId) => { - let instanceMetaData = - cornerstoneDICOMImageLoader.wadors.metaDataManager.get(imageId); + // 如果图像 ID 代表多帧信息,则为每个帧创建一个新的图像 ID 列表 + imageIds = convertMultiframeImageIds(imageIds); - // It was using JSON.parse(JSON.stringify(...)) before but it is 8x slower - instanceMetaData = removeInvalidTags(instanceMetaData); + imageIds.forEach((imageId) => { + let instanceMetaData = + cornerstoneDICOMImageLoader.wadors.metaDataManager.get(imageId); + 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]), - }); + if (instanceMetaData) { + // 添加校准的像素间距 + const metadata = + DicomMetaDictionary.naturalizeDataset(instanceMetaData); + const pixelSpacing = getPixelSpacingInformation(metadata) as Number[]; + if (pixelSpacing) { + const payload = { + rowPixelSpacing: pixelSpacing[0], + columnPixelSpacing: pixelSpacing[1], + }; + // FIXME: 这个地方cornerstone ts类型约束洗的有问题 + // @ts-ignore + calibratedPixelSpacingMetadataProvider.add(imageId, payload); + } } - } - }); - - return imageIds; + }); + return imageIds; + } catch (error) { + throw new Error("pacs中数据不存在"); + } } diff --git a/apps/aorta/src/modules/Root/Viewer/VolumeViewer/getPixelSpacingInformation.ts b/apps/aorta/src/modules/Root/Viewer/VolumeViewer/getPixelSpacingInformation.ts index 74646d5..081c02e 100644 --- a/apps/aorta/src/modules/Root/Viewer/VolumeViewer/getPixelSpacingInformation.ts +++ b/apps/aorta/src/modules/Root/Viewer/VolumeViewer/getPixelSpacingInformation.ts @@ -1,13 +1,35 @@ -// See https://github.com/OHIF/Viewers/blob/94a9067fe3d291d30e25a1bda5913511388edea2/platform/core/src/utils/metadataProvider/getPixelSpacingInformation.js +interface DICOMInstance { + PixelSpacing?: number[]; + ImagerPixelSpacing?: number[]; + SOPClassUID?: string; + PixelSpacingCalibrationType?: string; + PixelSpacingCalibrationDescription?: string; + EstimatedRadiographicMagnificationFactor?: number; + SequenceOfUltrasoundRegions?: { + PhysicalDeltaX: number; + PhysicalDeltaY: number; + }[]; // 根据实际需要调整类型 +} -export default function getPixelSpacingInformation(instance) { - // See http://gdcm.sourceforge.net/wiki/index.php/Imager_Pixel_Spacing +interface PixelSpacingResult { + PixelSpacing?: number[]; + type?: string; + isProjection?: boolean; + PixelSpacingCalibrationType?: string; + PixelSpacingCalibrationDescription?: string; +} - // TODO: Add Ultrasound region spacing - // TODO: Add manual calibration - - // TODO: Use ENUMS from dcmjs - const projectionRadiographSOPClassUIDs = [ +/** + * 获取像素间距信息。 + * 此函数根据 DICOM 实例的不同属性来确定适当的像素间距。 + * + * @param instance - DICOM 实例对象 + * @returns 像素间距信息 + */ +export default function getPixelSpacingInformation( + instance: DICOMInstance +): PixelSpacingResult | number[] | undefined { + const projectionRadiographSOPClassUIDs: string[] = [ "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 @@ -32,7 +54,9 @@ export default function getPixelSpacingInformation(instance) { SequenceOfUltrasoundRegions, } = instance; - const isProjection = projectionRadiographSOPClassUIDs.includes(SOPClassUID); + const isProjection = SOPClassUID + ? projectionRadiographSOPClassUIDs.includes(SOPClassUID) + : false; const TYPES = { NOT_APPLICABLE: "NOT_APPLICABLE", @@ -46,9 +70,6 @@ export default function getPixelSpacingInformation(instance) { } 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, @@ -57,10 +78,8 @@ export default function getPixelSpacingInformation(instance) { } else if ( PixelSpacing && ImagerPixelSpacing && - PixelSpacing === ImagerPixelSpacing + PixelSpacing.join() === ImagerPixelSpacing.join() ) { - // 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, @@ -69,12 +88,8 @@ export default function getPixelSpacingInformation(instance) { } else if ( PixelSpacing && ImagerPixelSpacing && - PixelSpacing !== ImagerPixelSpacing + PixelSpacing.join() !== ImagerPixelSpacing.join() ) { - // 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, @@ -85,9 +100,6 @@ export default function getPixelSpacingInformation(instance) { } 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 @@ -104,25 +116,16 @@ export default function getPixelSpacingInformation(instance) { }; } else if ( SequenceOfUltrasoundRegions && - typeof SequenceOfUltrasoundRegions === "object" + Array.isArray(SequenceOfUltrasoundRegions) && + SequenceOfUltrasoundRegions.length > 0 ) { - const { PhysicalDeltaX, PhysicalDeltaY } = SequenceOfUltrasoundRegions; + const { PhysicalDeltaX, PhysicalDeltaY } = SequenceOfUltrasoundRegions[0]; 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 + } else if (isProjection && !ImagerPixelSpacing) { return { PixelSpacing, type: TYPES.NOT_APPLICABLE, @@ -134,3 +137,6 @@ export default function getPixelSpacingInformation(instance) { "Unknown combination of PixelSpacing and ImagerPixelSpacing identified. Unable to determine spacing." ); } + +// 可选的,如果需要单独导出函数 +export { getPixelSpacingInformation }; diff --git a/apps/aorta/src/modules/Root/Viewer/VolumeViewer/index.tsx b/apps/aorta/src/modules/Root/Viewer/VolumeViewer/index.tsx index 18d536c..390a35b 100644 --- a/apps/aorta/src/modules/Root/Viewer/VolumeViewer/index.tsx +++ b/apps/aorta/src/modules/Root/Viewer/VolumeViewer/index.tsx @@ -33,8 +33,6 @@ export const VolumeViewer = (props: VolumeViewerProps) => { wadoRsRoot: "/dicom-web", }); - console.log(imageIds) - const renderingEngine = new RenderingEngine(renderingEngineId); const viewportInput = { diff --git a/apps/aorta/src/modules/Root/Viewer/VolumeViewer/removeInvalidTags.ts b/apps/aorta/src/modules/Root/Viewer/VolumeViewer/removeInvalidTags.ts index abf7652..0a96a0f 100644 --- a/apps/aorta/src/modules/Root/Viewer/VolumeViewer/removeInvalidTags.ts +++ b/apps/aorta/src/modules/Root/Viewer/VolumeViewer/removeInvalidTags.ts @@ -1,23 +1,25 @@ /** - * Remove invalid tags from a metadata and return a new object. + * Removes invalid tags from a metadata object and returns 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. + * This function currently removes tags that have `null` or `undefined` values. + * Removing these values is important to prevent errors when processing + * the metadata, such as when calling `naturalizeDataset(...)`. * - * 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. + * The performance of this function has been considered. Validating each + * tag ID with a regex significantly reduces the function's speed, so this + * approach is avoided. The function is designed to be efficient, with an + * average runtime of around +12ms per 1000 images, though this can vary + * depending on the machine. * - * @param srcMetadata - source metadata - * @returns new metadata object without invalid tags + * @param srcMetadata - The source metadata object containing the tags. + * @returns A new metadata object with invalid tags removed. */ -function removeInvalidTags(srcMetadata) { - // Object.create(null) make it ~9% faster - const dstMetadata = Object.create(null); +function removeInvalidTags( + srcMetadata: Record +): Record { + const dstMetadata: Record = Object.create(null); const tagIds = Object.keys(srcMetadata); - let tagValue; + let tagValue: any; tagIds.forEach((tagId) => { tagValue = srcMetadata[tagId]; diff --git a/apps/aorta/src/modules/Root/Viewer/VolumeViewer/type.d.ts b/apps/aorta/src/modules/Root/Viewer/VolumeViewer/type.d.ts index fe718df..c4bd600 100644 --- a/apps/aorta/src/modules/Root/Viewer/VolumeViewer/type.d.ts +++ b/apps/aorta/src/modules/Root/Viewer/VolumeViewer/type.d.ts @@ -1 +1,2 @@ declare module "@cornerstonejs/dicom-image-loader"; +declare module "dcmjs"; \ No newline at end of file