ohif-viewer/platform/core/src/classes/MetadataProvider.js

515 lines
15 KiB
JavaScript

import dcmjs from 'dcmjs';
import queryString from 'query-string';
import dicomParser from 'dicom-parser';
import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation';
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
import fetchOverlayData from '../utils/metadataProvider/fetchOverlayData';
import validNumber from '../utils/metadataProvider/validNumber';
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, 'imageIdToUIDs', {
configurable: false,
enumerable: false,
writable: false,
value: new Map(),
});
this.datasets = {};
}
async addInstance(dicomJSONDatasetOrP10ArrayBuffer, options = {}) {
let dicomJSONDataset;
// If Arraybuffer, parse to DICOMJSON before naturalizing.
if (dicomJSONDatasetOrP10ArrayBuffer instanceof ArrayBuffer) {
const dicomData = DicomMessage.readFile(dicomJSONDatasetOrP10ArrayBuffer);
dicomJSONDataset = dicomData.dict;
} else {
dicomJSONDataset = dicomJSONDatasetOrP10ArrayBuffer;
}
// Check if dataset is already naturalized.
let naturalizedDataset;
if (dicomJSONDataset['SeriesInstanceUID'] === undefined) {
naturalizedDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomJSONDataset
);
} else {
naturalizedDataset = dicomJSONDataset;
}
const {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
} = naturalizedDataset;
this._getAndCacheStudyDataset(StudyInstanceUID, dicomJSONDataset);
const study = this._getAndCacheStudy(StudyInstanceUID);
const series = this._getAndCacheSeriesFromStudy(study, SeriesInstanceUID);
const instance = this._getAndCacheInstanceFromStudy(series, SOPInstanceUID);
Object.assign(instance, naturalizedDataset);
await this._checkBulkDataAndInlineBinaries(instance, options.server);
return instance;
}
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.
this.imageIdToUIDs.set(imageId, uids);
}
_getAndCacheStudyDataset(StudyInstanceUID, dataset) {
if (!this.datasets[StudyInstanceUID]) {
this.datasets[StudyInstanceUID] = dataset;
}
}
getStudyDataset(StudyInstanceUID) {
return this.datasets[StudyInstanceUID];
}
_getAndCacheStudy(StudyInstanceUID) {
const studies = this.studies;
let study = studies.get(StudyInstanceUID);
if (!study) {
study = { series: new Map() };
studies.set(StudyInstanceUID, study);
}
return study;
}
_getAndCacheSeriesFromStudy(study, SeriesInstanceUID) {
let series = study.series.get(SeriesInstanceUID);
if (!series) {
series = { instances: new Map() };
study.series.set(SeriesInstanceUID, series);
}
return series;
}
_getAndCacheInstanceFromStudy(series, SOPInstanceUID) {
let instance = series.instances.get(SOPInstanceUID);
if (!instance) {
instance = {};
series.instances.set(SOPInstanceUID, instance);
}
return instance;
}
async _checkBulkDataAndInlineBinaries(instance, server) {
await fetchOverlayData(instance, server);
if (instance.PhotometricInterpretation === 'PALETTE COLOR') {
await fetchPaletteColorLookupTableData(instance, server);
}
}
_getInstance(imageId) {
const uids = this._getUIDsFromImageID(imageId);
if (!uids) {
return;
}
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = uids;
return this._getInstanceData(
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID
);
}
get(query, imageId, options = { fallback: false }) {
const instance = this._getInstance(imageId);
if (query === INSTANCE) {
return instance;
}
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 CornerstoneWADOImageLoader tag then:
return this._getCornerstoneWADOImageLoaderTag(
naturalizedTagOrWADOImageLoaderTag,
instance
);
}
_getCornerstoneWADOImageLoaderTag(wadoImageLoaderTag, instance) {
let 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: instance.SeriesNumber,
studyInstanceUID: instance.StudyInstanceUID,
seriesDate,
seriesTime,
};
break;
case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE:
metadata = {
patientAge: instance.PatientAge,
patientSize: instance.PatientSize,
patientWeight: instance.PatientWeight,
};
break;
case WADO_IMAGE_LOADER_TAGS.IMAGE_PLANE_MODULE:
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);
}
metadata = {
frameOfReferenceUID: instance.FrameOfReferenceUID,
rows: instance.Rows,
columns: instance.Columns,
imageOrientationPatient: ImageOrientationPatient,
rowCosines,
columnCosines,
imagePositionPatient: instance.ImagePositionPatient,
sliceThickness: instance.SliceThickness,
sliceLocation: instance.SliceLocation,
pixelSpacing: PixelSpacing,
rowPixelSpacing,
columnPixelSpacing,
};
break;
case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE:
metadata = {
samplesPerPixel: instance.SamplesPerPixel,
photometricInterpretation: instance.PhotometricInterpretation,
rows: instance.Rows,
columns: instance.Columns,
bitsAllocated: instance.BitsAllocated,
bitsStored: instance.BitsStored,
highBit: instance.HighBit,
pixelRepresentation: instance.PixelRepresentation,
planarConfiguration: instance.PlanarConfiguration,
pixelAspectRatio: instance.PixelAspectRatio,
smallestPixelValue: instance.SmallestPixelValue,
largestPixelValue: instance.LargestPixelValue,
redPaletteColorLookupTableDescriptor:
instance.RedPaletteColorLookupTableDescriptor,
greenPaletteColorLookupTableDescriptor:
instance.GreenPaletteColorLookupTableDescriptor,
bluePaletteColorLookupTableDescriptor:
instance.BluePaletteColorLookupTableDescriptor,
redPaletteColorLookupTableData:
instance.RedPaletteColorLookupTableData,
greenPaletteColorLookupTableData:
instance.GreenPaletteColorLookupTableData,
bluePaletteColorLookupTableData:
instance.BluePaletteColorLookupTableData,
};
break;
case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE:
let { WindowCenter, WindowWidth } = instance;
const windowCenter = Array.isArray(WindowCenter)
? WindowCenter
: [WindowCenter];
const windowWidth = Array.isArray(WindowWidth)
? WindowWidth
: [WindowWidth];
metadata = {
windowCenter: validNumber(windowCenter),
windowWidth: validNumber(windowWidth),
};
break;
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
const rescaleSlope = validNumber(instance.RescaleSlope);
const rescaleIntercept = validNumber(instance.RescaleIntercept);
metadata = {
rescaleIntercept,
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_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];
const overlay = {
rows: instance[OverlayRowsTag],
columns: instance[OverlayColumnsTag],
type: instance[OverlayType],
x: OverlayOrigin[0],
y: OverlayOrigin[1],
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: 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,
};
break;
}
return metadata;
}
_getInstanceData(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
const study = this.studies.get(StudyInstanceUID);
if (!study) {
return;
}
const series = study.series.get(SeriesInstanceUID);
if (!series) {
return;
}
const instance = series.instances.get(SOPInstanceUID);
return instance;
}
_getUIDsFromImageID(imageId) {
if (imageId.includes('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],
};
}
if (imageId.includes('wado?requestType=WADO')) {
const qs = queryString.parse(imageId);
return {
StudyInstanceUID: qs.studyUID,
SeriesInstanceUID: qs.seriesUID,
SOPInstanceUID: qs.objectUID,
};
} else {
// Maybe its a non-standard imageId
return this.imageIdToUIDs.get(imageId);
}
}
}
const metadataProvider = new MetadataProvider();
export default metadataProvider;
const WADO_IMAGE_LOADER_TAGS = {
// CornerstoneWADOImageLoader specific
GENERAL_SERIES_MODULE: 'generalSeriesModule',
PATIENT_STUDY_MODULE: 'patientStudyModule',
IMAGE_PLANE_MODULE: 'imagePlaneModule',
IMAGE_PIXEL_MODULE: 'imagePixelModule',
VOI_LUT_MODULE: 'voiLutModule',
MODALITY_LUT_MODULE: 'modalityLutModule',
SOP_COMMON_MODULE: 'sopCommonModule',
PET_ISOTOPE_MODULE: 'petIsotopeModule',
OVERLAY_PLANE_MODULE: 'overlayPlaneModule',
// react-cornerstone-viewport specifc
PATIENT_MODULE: 'patientModule',
GENERAL_IMAGE_MODULE: 'generalImageModule',
GENERAL_STUDY_MODULE: 'generalStudyModule',
CINE_MODULE: 'cineModule',
};
const INSTANCE = 'instance';