* Instance metadata plus metadata provider overhaul. Fix consumption of wado-uri urls fallbacks + datatype agnosticism. WIP DICOMify things. fix various issues with naturalized variable naming migration. Remove metadata provider. Fix consumption of multiframe images and addition of CWIL metadata. Fix strange build issues. Fix CWIL style windowWidth to array from naturalized DICOM. Fix PT, CT, CR and DX issues for cornerstone + DX issues for vtkjs. Move color palette fetching down to the natuaralized JSON level. Remove unused StudyMetadataSummary Remove redundant dicom metadata dictionary. Working local + json routes. Fix SR read. Finished first round of testing + cleaned up debugging etc. * data => metadata for instance naturalizedJSON * Update dcmjs version * Correct github isssues. * Fix erroneously replaced files. * Danny's recommended changes. * Instance metadata plus metadata provider overhaul. Fix consumption of wado-uri urls fallbacks + datatype agnosticism. WIP DICOMify things. fix various issues with naturalized variable naming migration. Remove metadata provider. Fix consumption of multiframe images and addition of CWIL metadata. Fix strange build issues. Fix CWIL style windowWidth to array from naturalized DICOM. Fix PT, CT, CR and DX issues for cornerstone + DX issues for vtkjs. Move color palette fetching down to the natuaralized JSON level. Remove unused StudyMetadataSummary Remove redundant dicom metadata dictionary. Working local + json routes. Fix SR read. Finished first round of testing + cleaned up debugging etc. * data => metadata for instance naturalizedJSON * Update dcmjs version * Correct github isssues. * Fix erroneously replaced files. * Danny's recommended changes. * Update JSON CI * Update casing of import. * Fix jump for SR. * Fix unit tests for measurements service * Fix json CI test. * fix: update yarn lock * Fix local non-encapsulated pdf view * CI updated to new sucess message. Co-authored-by: Danny <danny.ri.brown@gmail.com>
509 lines
15 KiB
JavaScript
509 lines
15 KiB
JavaScript
import * as dcmjs from 'dcmjs';
|
|
import queryString from 'query-string';
|
|
import dicomParser from 'dicom-parser';
|
|
import { getFallbackTagFromInstance } from '../utils/metadataProvider/metadataProviderFallbackTags';
|
|
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
|
|
import fetchOverlayData from '../utils/metadataProvider/fetchOverlayData';
|
|
|
|
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(),
|
|
});
|
|
}
|
|
|
|
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;
|
|
|
|
const study = this._getAndCacheStudy(StudyInstanceUID);
|
|
const series = this._getAndCacheSeriesFromStudy(study, SeriesInstanceUID);
|
|
const instance = this._getAndCacheInstanceFromStudy(series, SOPInstanceUID);
|
|
|
|
Object.assign(instance, naturalizedDataset);
|
|
|
|
if (options.server) {
|
|
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);
|
|
}
|
|
|
|
_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];
|
|
}
|
|
|
|
if (options.fallback) {
|
|
// Perhaps the tag has fallbacks?
|
|
const fallbackTag = getFallbackTagFromInstance(
|
|
naturalizedTagOrWADOImageLoaderTag,
|
|
instance
|
|
);
|
|
|
|
if (fallbackTag) {
|
|
return fallbackTag;
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
const PixelSpacing = getFallbackTagFromInstance(
|
|
'PixelSpacing',
|
|
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:
|
|
const { WindowCenter, WindowWidth } = instance;
|
|
|
|
const windowCenter = Array.isArray(WindowCenter)
|
|
? WindowCenter
|
|
: [WindowCenter];
|
|
const windowWidth = Array.isArray(WindowWidth)
|
|
? WindowWidth
|
|
: [WindowWidth];
|
|
|
|
metadata = {
|
|
windowCenter,
|
|
windowWidth,
|
|
};
|
|
|
|
break;
|
|
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
|
|
metadata = {
|
|
rescaleIntercept: instance.RescaleIntercept,
|
|
rescaleSlope: 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_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:
|
|
metadata = [];
|
|
|
|
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],
|
|
};
|
|
|
|
metadata.push(overlay);
|
|
}
|
|
|
|
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 = {
|
|
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';
|