import queryString from 'query-string'; import dicomParser from 'dicom-parser'; import { imageIdToURI } from '../utils'; import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation'; import DicomMetadataStore from '../services/DicomMetadataStore'; import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData'; import toNumber from '../utils/toNumber'; import combineFrameInstance from '../utils/combineFrameInstance'; class MetadataProvider { constructor() { // Define the main "metadataLookup" private property as an immutable property. Object.defineProperty(this, 'studies', { configurable: false, enumerable: false, writable: false, value: new Map(), }); Object.defineProperty(this, 'imageURIToUIDs', { configurable: false, enumerable: false, writable: false, value: new Map(), }); // Can be used to store custom metadata for a specific type. // For instance, the scaling metadata for PET can be stored here // as type "scalingModule" // Object.defineProperty(this, 'customMetadata', { configurable: false, enumerable: false, writable: false, value: new Map(), }); } addImageIdToUIDs(imageId, uids) { // This method is a fallback for when you don't have WADO-URI or WADO-RS. // You can add instances fetched by any method by calling addInstance, and hook an imageId to point at it here. // An example would be dicom hosted at some random site. const imageURI = imageIdToURI(imageId); this.imageURIToUIDs.set(imageURI, uids); } addCustomMetadata(imageId, type, metadata) { const imageURI = imageIdToURI(imageId); if (!this.customMetadata.has(type)) { this.customMetadata.set(type, {}); } this.customMetadata.get(type)[imageURI] = metadata; } _getInstance(imageId) { const uids = this.getUIDsFromImageID(imageId); if (!uids) { return; } const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, frameNumber } = uids; const instance = DicomMetadataStore.getInstance( StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID ); if (!instance) { return; } return (frameNumber && combineFrameInstance(frameNumber, instance)) || instance; } get(query, imageId, options = { fallback: false }) { if (Array.isArray(imageId)) { return; } const instance = this._getInstance(imageId); if (query === INSTANCE) { return instance; } // check inside custom metadata if (this.customMetadata.has(query)) { const customMetadata = this.customMetadata.get(query); const imageURI = imageIdToURI(imageId); if (customMetadata[imageURI]) { return customMetadata[imageURI]; } } return this.getTagFromInstance(query, instance, options); } getTag(query, imageId, options) { return this.get(query, imageId, options); } getInstance(imageId) { return this.get(INSTANCE, imageId); } getTagFromInstance(naturalizedTagOrWADOImageLoaderTag, instance, options = { fallback: false }) { if (!instance) { return; } // If its a naturalized dcmjs tag present on the instance, return. if (instance[naturalizedTagOrWADOImageLoaderTag]) { return instance[naturalizedTagOrWADOImageLoaderTag]; } // Maybe its a legacy dicomImageLoader tag then: return this._getCornerstoneDICOMImageLoaderTag(naturalizedTagOrWADOImageLoaderTag, instance); } /** * Adds a new handler for the given tag. The handler will be provided an * instance object that it can read values from. */ public addHandler(wadoImageLoaderTag: string, handler) { WADO_IMAGE_LOADER[wadoImageLoaderTag] = handler; } _getCornerstoneDICOMImageLoaderTag(wadoImageLoaderTag, instance) { let metadata = WADO_IMAGE_LOADER[wadoImageLoaderTag]?.(instance); if (metadata) { return metadata; } switch (wadoImageLoaderTag) { case WADO_IMAGE_LOADER_TAGS.GENERAL_SERIES_MODULE: const { SeriesDate, SeriesTime } = instance; let seriesDate; let seriesTime; if (SeriesDate) { seriesDate = dicomParser.parseDA(SeriesDate); } if (SeriesTime) { seriesTime = dicomParser.parseTM(SeriesTime); } metadata = { modality: instance.Modality, seriesInstanceUID: instance.SeriesInstanceUID, seriesNumber: toNumber(instance.SeriesNumber), studyInstanceUID: instance.StudyInstanceUID, seriesDate, seriesTime, }; break; case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE: metadata = { patientAge: toNumber(instance.PatientAge), patientSize: toNumber(instance.PatientSize), patientWeight: toNumber(instance.PatientWeight), }; break; case WADO_IMAGE_LOADER_TAGS.PATIENT_DEMOGRAPHIC_MODULE: metadata = { patientSex: instance.PatientSex, }; break; case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE: metadata = { samplesPerPixel: toNumber(instance.SamplesPerPixel), photometricInterpretation: instance.PhotometricInterpretation, rows: toNumber(instance.Rows), columns: toNumber(instance.Columns), bitsAllocated: toNumber(instance.BitsAllocated), bitsStored: toNumber(instance.BitsStored), highBit: toNumber(instance.HighBit), pixelRepresentation: toNumber(instance.PixelRepresentation), planarConfiguration: toNumber(instance.PlanarConfiguration), pixelAspectRatio: toNumber(instance.PixelAspectRatio), smallestPixelValue: toNumber(instance.SmallestPixelValue), largestPixelValue: toNumber(instance.LargestPixelValue), redPaletteColorLookupTableDescriptor: toNumber( instance.RedPaletteColorLookupTableDescriptor ), greenPaletteColorLookupTableDescriptor: toNumber( instance.GreenPaletteColorLookupTableDescriptor ), bluePaletteColorLookupTableDescriptor: toNumber( instance.BluePaletteColorLookupTableDescriptor ), redPaletteColorLookupTableData: fetchPaletteColorLookupTableData( instance, 'RedPaletteColorLookupTableData', 'RedPaletteColorLookupTableDescriptor' ), greenPaletteColorLookupTableData: fetchPaletteColorLookupTableData( instance, 'GreenPaletteColorLookupTableData', 'GreenPaletteColorLookupTableDescriptor' ), bluePaletteColorLookupTableData: fetchPaletteColorLookupTableData( instance, 'BluePaletteColorLookupTableData', 'BluePaletteColorLookupTableDescriptor' ), }; break; case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE: const { WindowCenter, WindowWidth, VOILUTFunction } = instance; if (WindowCenter === undefined || WindowWidth === undefined) { return; } const windowCenter = Array.isArray(WindowCenter) ? WindowCenter : [WindowCenter]; const windowWidth = Array.isArray(WindowWidth) ? WindowWidth : [WindowWidth]; metadata = { windowCenter: toNumber(windowCenter), windowWidth: toNumber(windowWidth), voiLUTFunction: VOILUTFunction, }; break; case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE: const { RescaleIntercept, RescaleSlope } = instance; if (RescaleIntercept === undefined || RescaleSlope === undefined) { return; } metadata = { rescaleIntercept: toNumber(instance.RescaleIntercept), rescaleSlope: toNumber(instance.RescaleSlope), rescaleType: instance.RescaleType, }; break; case WADO_IMAGE_LOADER_TAGS.SOP_COMMON_MODULE: metadata = { sopClassUID: instance.SOPClassUID, sopInstanceUID: instance.SOPInstanceUID, }; break; case WADO_IMAGE_LOADER_TAGS.PET_IMAGE_MODULE: metadata = { frameReferenceTime: instance.FrameReferenceTime, actualFrameDuration: instance.ActualFrameDuration, }; break; case WADO_IMAGE_LOADER_TAGS.PET_ISOTOPE_MODULE: const { RadiopharmaceuticalInformationSequence } = instance; if (RadiopharmaceuticalInformationSequence) { const RadiopharmaceuticalInformation = Array.isArray( RadiopharmaceuticalInformationSequence ) ? RadiopharmaceuticalInformationSequence[0] : RadiopharmaceuticalInformationSequence; const { RadiopharmaceuticalStartTime, RadionuclideTotalDose, RadionuclideHalfLife } = RadiopharmaceuticalInformation; const radiopharmaceuticalInfo = { radiopharmaceuticalStartTime: dicomParser.parseTM(RadiopharmaceuticalStartTime), radionuclideTotalDose: RadionuclideTotalDose, radionuclideHalfLife: RadionuclideHalfLife, }; metadata = { radiopharmaceuticalInfo, }; } break; case WADO_IMAGE_LOADER_TAGS.OVERLAY_PLANE_MODULE: const overlays = []; for (let overlayGroup = 0x00; overlayGroup <= 0x1e; overlayGroup += 0x02) { let groupStr = `60${overlayGroup.toString(16)}`; if (groupStr.length === 3) { groupStr = `600${overlayGroup.toString(16)}`; } const OverlayDataTag = `${groupStr}3000`; const OverlayData = instance[OverlayDataTag]; if (!OverlayData) { continue; } const OverlayRowsTag = `${groupStr}0010`; const OverlayColumnsTag = `${groupStr}0011`; const OverlayType = `${groupStr}0040`; const OverlayOriginTag = `${groupStr}0050`; const OverlayDescriptionTag = `${groupStr}0022`; const OverlayLabelTag = `${groupStr}1500`; const ROIAreaTag = `${groupStr}1301`; const ROIMeanTag = `${groupStr}1302`; const ROIStandardDeviationTag = `${groupStr}1303`; const OverlayOrigin = instance[OverlayOriginTag]; let rows = 0; if (instance[OverlayRowsTag] instanceof Array) { // The DICOM VR for overlay rows is US (unsigned short). const rowsInt16Array = new Uint16Array(instance[OverlayRowsTag][0]); rows = rowsInt16Array[0]; } else { rows = instance[OverlayRowsTag]; } let columns = 0; if (instance[OverlayColumnsTag] instanceof Array) { // The DICOM VR for overlay columns is US (unsigned short). const columnsInt16Array = new Uint16Array(instance[OverlayColumnsTag][0]); columns = columnsInt16Array[0]; } else { columns = instance[OverlayColumnsTag]; } let x = 0; let y = 0; if (OverlayOrigin.length === 1) { // The DICOM VR for overlay origin is SS (signed short) with a multiplicity of 2. const originInt16Array = new Int16Array(OverlayOrigin[0]); x = originInt16Array[0]; y = originInt16Array[1]; } else { x = OverlayOrigin[0]; y = OverlayOrigin[1]; } const overlay = { rows: rows, columns: columns, type: instance[OverlayType], x, y, pixelData: OverlayData, description: instance[OverlayDescriptionTag], label: instance[OverlayLabelTag], roiArea: instance[ROIAreaTag], roiMean: instance[ROIMeanTag], roiStandardDeviation: instance[ROIStandardDeviationTag], }; overlays.push(overlay); } metadata = { overlays, }; break; case WADO_IMAGE_LOADER_TAGS.PATIENT_MODULE: const { PatientName } = instance; let patientName; if (PatientName) { patientName = PatientName.Alphabetic; } metadata = { patientName, patientId: instance.PatientID, }; break; case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE: metadata = { sopInstanceUID: instance.SOPInstanceUID, instanceNumber: toNumber(instance.InstanceNumber), lossyImageCompression: instance.LossyImageCompression, lossyImageCompressionRatio: instance.LossyImageCompressionRatio, lossyImageCompressionMethod: instance.LossyImageCompressionMethod, }; break; case WADO_IMAGE_LOADER_TAGS.GENERAL_STUDY_MODULE: metadata = { studyDescription: instance.StudyDescription, studyDate: instance.StudyDate, studyTime: instance.StudyTime, accessionNumber: instance.AccessionNumber, }; break; case WADO_IMAGE_LOADER_TAGS.CINE_MODULE: metadata = { frameTime: instance.FrameTime, numberOfFrames: instance.NumberOfFrames ? Number(instance.NumberOfFrames) : 1, }; break; case WADO_IMAGE_LOADER_TAGS.PER_SERIES_MODULE: metadata = { correctedImage: instance.CorrectedImage, units: instance.Units, decayCorrection: instance.DecayCorrection, }; break; case WADO_IMAGE_LOADER_TAGS.CALIBRATION_MODULE: // map the DICOM tags to the cornerstone tags since cornerstone tags // are camelCase and instance tags are all caps metadata = { sequenceOfUltrasoundRegions: instance.SequenceOfUltrasoundRegions?.map(region => { return { regionSpatialFormat: region.RegionSpatialFormat, regionDataType: region.RegionDataType, regionFlags: region.RegionFlags, regionLocationMinX0: region.RegionLocationMinX0, regionLocationMinY0: region.RegionLocationMinY0, regionLocationMaxX1: region.RegionLocationMaxX1, regionLocationMaxY1: region.RegionLocationMaxY1, referencePixelX0: region.ReferencePixelX0, referencePixelY0: region.ReferencePixelY0, referencePixelPhysicalValueX: region.ReferencePixelPhysicalValueX, referencePixelPhysicalValueY: region.ReferencePixelPhysicalValueY, physicalUnitsXDirection: region.PhysicalUnitsXDirection, physicalUnitsYDirection: region.PhysicalUnitsYDirection, physicalDeltaX: region.PhysicalDeltaX, physicalDeltaY: region.PhysicalDeltaY, }; }), }; break; /** * Below are the tags and not the modules since they are not really * consistent with the modules above */ case 'temporalPositionIdentifier': metadata = { temporalPositionIdentifier: instance.TemporalPositionIdentifier, }; break; default: return; } return metadata; } /** * Retrieves the frameNumber information, depending on the url style * wadors /frames/1 * wadouri &frame=1 * @param {*} imageId * @returns */ getFrameInformationFromURL(imageId) { function getInformationFromURL(informationString, separator) { let result = ''; const splittedStr = imageId.split(informationString)[1]; if (splittedStr.includes(separator)) { result = splittedStr.split(separator)[0]; } else { result = splittedStr; } return result; } if (imageId.includes('/frames')) { return getInformationFromURL('/frames', '/'); } if (imageId.includes('&frame=')) { return getInformationFromURL('&frame=', '&'); } return; } getUIDsFromImageID(imageId) { if (!imageId) { throw new Error('MetadataProvider::Empty imageId'); } // TODO: adding csiv here is not really correct. Probably need to use // metadataProvider.addImageIdToUIDs(imageId, { // StudyInstanceUID, // SeriesInstanceUID, // SOPInstanceUID, // }) // somewhere else if (imageId.startsWith('wadors:')) { const strippedImageId = imageId.split('/studies/')[1]; const splitImageId = strippedImageId.split('/'); return { StudyInstanceUID: splitImageId[0], // Note: splitImageId[1] === 'series' SeriesInstanceUID: splitImageId[2], // Note: splitImageId[3] === 'instances' SOPInstanceUID: splitImageId[4], frameNumber: splitImageId[6], }; } else if (imageId.includes('?requestType=WADO')) { const qs = queryString.parse(imageId); return { StudyInstanceUID: qs.studyUID, SeriesInstanceUID: qs.seriesUID, SOPInstanceUID: qs.objectUID, frameNumber: qs.frameNumber, }; } // Maybe its a non-standard imageId // check if the imageId starts with http:// or https:// using regex // Todo: handle non http imageIds let imageURI; const urlRegex = /^(http|https|dicomfile):\/\//; if (urlRegex.test(imageId)) { imageURI = imageId; } else { imageURI = imageIdToURI(imageId); } // remove &frame=number from imageId imageURI = imageURI.split('&frame=')[0]; const uids = this.imageURIToUIDs.get(imageURI); const frameNumber = this.getFrameInformationFromURL(imageId) || '1'; if (uids && frameNumber !== undefined) { return { ...uids, frameNumber }; } return uids; } } const metadataProvider = new MetadataProvider(); export default metadataProvider; const WADO_IMAGE_LOADER = { imagePlaneModule: instance => { const { ImageOrientationPatient } = instance; // Fallback for DX images. // TODO: We should use the rest of the results of this function // to update the UI somehow const { PixelSpacing } = getPixelSpacingInformation(instance); let rowPixelSpacing; let columnPixelSpacing; let rowCosines; let columnCosines; if (PixelSpacing) { rowPixelSpacing = PixelSpacing[0]; columnPixelSpacing = PixelSpacing[1]; } if (ImageOrientationPatient) { rowCosines = ImageOrientationPatient.slice(0, 3); columnCosines = ImageOrientationPatient.slice(3, 6); } return { frameOfReferenceUID: instance.FrameOfReferenceUID, rows: toNumber(instance.Rows), columns: toNumber(instance.Columns), imageOrientationPatient: toNumber(ImageOrientationPatient), rowCosines: toNumber(rowCosines || [0, 1, 0]), isDefaultValueSetForRowCosine: toNumber(rowCosines) ? false : true, columnCosines: toNumber(columnCosines || [0, 0, -1]), isDefaultValueSetForColumnCosine: toNumber(columnCosines) ? false : true, imagePositionPatient: toNumber(instance.ImagePositionPatient || [0, 0, 0]), sliceThickness: toNumber(instance.SliceThickness), sliceLocation: toNumber(instance.SliceLocation), pixelSpacing: toNumber(PixelSpacing || 1), rowPixelSpacing: rowPixelSpacing ? toNumber(rowPixelSpacing) : null, columnPixelSpacing: columnPixelSpacing ? toNumber(columnPixelSpacing) : null, }; }, }; const WADO_IMAGE_LOADER_TAGS = { // dicomImageLoader specific GENERAL_SERIES_MODULE: 'generalSeriesModule', PATIENT_STUDY_MODULE: 'patientStudyModule', IMAGE_PIXEL_MODULE: 'imagePixelModule', VOI_LUT_MODULE: 'voiLutModule', MODALITY_LUT_MODULE: 'modalityLutModule', SOP_COMMON_MODULE: 'sopCommonModule', PET_IMAGE_MODULE: 'petImageModule', PET_ISOTOPE_MODULE: 'petIsotopeModule', PER_SERIES_MODULE: 'petSeriesModule', OVERLAY_PLANE_MODULE: 'overlayPlaneModule', PATIENT_DEMOGRAPHIC_MODULE: 'patientDemographicModule', // react-cornerstone-viewport specific PATIENT_MODULE: 'patientModule', GENERAL_IMAGE_MODULE: 'generalImageModule', GENERAL_STUDY_MODULE: 'generalStudyModule', CINE_MODULE: 'cineModule', CALIBRATION_MODULE: 'calibrationModule', }; const INSTANCE = 'instance';