fix: 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases (#862)

* fix: 🐛 Local file: failing when retrieving segmentation data

Fix segmentation data retrieval issues for local file. Changed from
fecthing to use cornerstone loadAndCache method

BREAKING CHANGE: DICOM Seg

Closes: part of #838

* Switch SEG retrieval to WADO-RS

* Forgot a debugger

* refactor: 💡 Code refactor. Minor changes into methods

* fix: 🐛 Load local files: PDF

Items:
1. FileLoaderService: used for serveral operations on local files(load it,  get list of studies, group them, accepting dicom and pdf)
2. DicomLoaderService: used for loading dicom based on dataset and studies. Depending on type of dicom loader might change. WIP
3. Refactor PDF and handleSegmentationStorage to use DicomLoaderService

* fix: 🐛 Code review

* fix: 🐛 Code review. Changed:Folder organization and dicom file

Move fileLoaderService and others to a specific folder. When loading
dicom file change to only retrieve the file(not use cornerstone to cache
or anything else).

* fix: 🐛 Code review. Move dicomLoaderService to core

Moved dicomLoaderService to ohif/core and localFileLoaders to a specific
folder.

* fix: 🐛 Code review

Simplified method to get study for dicom file. Added error handling on
file loading. DicomLoaderService to be exposed on ohif/core/utils
instead.

* fix: 🐛 Reduce local load to one method only

Reduced local file load to one method only

* fix: 🐛 HTML to use dicomLoaderService. Prefer wadors than (uri)

* fix: 🐛 Code implementation for multiframe files

* fix: 🐛 Code review. Default local loader to dicom

Closes: 838

* fix: 🐛 Code review. Use relative path to require DICOMWeb

Closes: 838

* fix: 🐛 Code review. Fix unit test. Added DicomLoaderService mod

Closes: 838

* fix: 🐛 Code review. Add 'Seg' on left thumb

When getting/creating dataset get modality for file/image read

Closes: 838
This commit is contained in:
ladeirarodolfo authored and Erik Ziegler committed 2019-09-27 13:47:08 +02:00
1 parent e66c27affb
commit e7e1a8a6cd
16 files changed
+629 -272

No files matched your search

@@ -79,6 +79,9 @@ class ViewerLocalFileData extends Component {
studyMetadata.createDisplaySets(sopClassHandlerModules);
studyMetadata.setDisplaySets(study.displaySets);
studyMetadata.forEachDisplaySet(displayset => {
displayset.localFile = true;
});
// Updates WADO-RS metaDataManager
updateMetaDataManager(study);
+19 -153
View File
@@ -1,160 +1,26 @@
import cornerstone from 'cornerstone-core';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import * as dcmjs from 'dcmjs';
import FileLoaderService from './localFileLoaders/fileLoaderService';
function datasetsToStudies(datasets) {
const StudyInstanceUIDs = new Set();
datasets.forEach(ds => {
StudyInstanceUIDs.add(ds.StudyInstanceUID);
});
const processFile = async file => {
try {
const fileLoaderService = new FileLoaderService(file);
const imageId = fileLoaderService.addFile(file);
const image = await fileLoaderService.loadFile(file, imageId);
const dataset = await fileLoaderService.getDataset(image, imageId);
const studies = await fileLoaderService.getStudies(dataset, imageId);
const studies = [];
Array.from(StudyInstanceUIDs).forEach(studyInstanceUid => {
const studyDatasets = datasets.filter(
ds => ds.StudyInstanceUID === studyInstanceUid
return studies;
} catch (error) {
console.log(
error.name,
':Error when trying to load and process local files:',
error.message
);
const firstDataset = studyDatasets[0];
const study = {
studyInstanceUid: firstDataset.StudyInstanceUID,
studyDate: firstDataset.StudyDate,
studyTime: firstDataset.StudyTime,
accessionNumber: firstDataset.AccessionNumber,
referringPhysicianName: firstDataset.ReferringPhysicianName,
patientName: firstDataset.PatientName,
patientId: firstDataset.PatientID,
patientBirthdate: firstDataset.PatientBirthDate,
patientSex: firstDataset.PatientSex,
studyId: firstDataset.StudyID,
studyDescription: firstDataset.StudyDescription,
//numberOfStudyRelatedSeries: DICOMWeb.getString(study['00201206']),
//numberOfStudyRelatedInstances: DICOMWeb.getString(study['00201208']),
// modality: DICOMWeb.getString(study['00080060']),
// modalitiesInStudy: DICOMWeb.getString(study['00080061']),
//modalities:
seriesList: [],
};
const SeriesInstanceUIDs = new Set();
studyDatasets.forEach(ds => {
SeriesInstanceUIDs.add(ds.SeriesInstanceUID);
});
Array.from(SeriesInstanceUIDs).forEach(seriesInstanceUid => {
const seriesDatasets = studyDatasets.filter(
ds => ds.SeriesInstanceUID === seriesInstanceUid
);
const SOPInstanceUIDs = new Set();
seriesDatasets.forEach(ds => {
SOPInstanceUIDs.add(ds.SOPInstanceUID);
study.seriesList.push({
seriesInstanceUid: ds.SeriesInstanceUID,
seriesDescription: ds.SeriesDescription,
seriesNumber: ds.SeriesNumber,
instances: [],
});
});
Array.from(SOPInstanceUIDs).forEach(sopInstanceUid => {
const instance = seriesDatasets.find(
a => a.SOPInstanceUID === sopInstanceUid
);
const series = study.seriesList.find(
a => a.seriesInstanceUid === seriesInstanceUid
);
series.instances.push({
sopInstanceUid: instance.SOPInstanceUID,
sopClassUid: instance.SOPClassUID,
rows: instance.Rows,
columns: instance.Columns,
numberOfFrames: instance.NumberOfFrames,
instanceNumber: instance.InstanceNumber,
getImageId: () => instance.imageId, // TODO: Change getImageId to check for instance.imageId property first.
/*imageType: DICOMWeb.getString(instance['00080008']),
modality: DICOMWeb.getString(instance['00080060']),
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
rescaleType: DICOMWeb.getNumber(instance['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
frameTimeVector: parseFloatArray(
DICOMWeb.getString(instance['00181065'])
),
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),*/
});
});
});
studies.push(study);
});
console.warn(studies);
return studies;
}
}
};
export default async function filesToStudies(files) {
const imagePromises = files.map(file => {
const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(file);
return cornerstone
.loadAndCacheImage(imageId)
.catch(error => console.warn(error));
});
const processFilesPromises = files.map(processFile);
const studies = await Promise.all(processFilesPromises);
const images = await Promise.all(imagePromises);
const datasets = [];
images.forEach(image => {
if (!image || !image.data) {
return;
}
const arrayBuffer = image.data.byteArray.buffer;
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
dicomData.meta
);
dataset.imageId = image.imageId;
datasets.push(dataset);
});
return datasetsToStudies(datasets);
return FileLoaderService.groupSeries(studies.flat());
}
@@ -0,0 +1,192 @@
import * as dcmjs from 'dcmjs';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import FileLoader from './fileLoader';
const DICOMFileLoader = new (class extends FileLoader {
fileType = 'application/dicom';
loadFile(file, imageId) {
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
}
getDataset(image, imageId) {
let dataset = {};
try {
const dicomData = dcmjs.data.DicomMessage.readFile(image);
dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
dicomData.meta
);
} catch (e) {
console.log('Error on getting dicom file dataset. It defaults to empty');
}
// Set imageId on dataset to be consumed later on
dataset.imageId = imageId;
return dataset;
}
getStudies(dataset, imageId) {
return this.getStudyFromDataset(dataset);
}
getStudyFromDataset(dataset = {}) {
const {
StudyInstanceUID,
StudyDate,
StudyTime,
AccessionNumber,
ReferringPhysicianName,
PatientName,
PatientID,
PatientBirthDate,
PatientSex,
StudyID,
StudyDescription,
/*
NumberOfStudyRelatedSeries,
NumberOfStudyRelatedInstances,
Modality,
ModalitiesInStudy,
*/
SeriesInstanceUID,
SeriesDescription,
SeriesNumber,
SOPInstanceUID,
SOPClassUID,
Rows,
Columns,
NumberOfFrames,
InstanceNumber,
imageId,
Modality,
/*ImageType,
InstanceNumber,
ImagePositionPatient,
ImageOrientationPatient,
FrameOfReferenceUID,
SliceLocation,
SamplesPerPixel,
PhotometricInterpretation,
PlanarConfiguration,
PixelSpacing,
PixelAspectRatio,
BitsAllocated,
BitsStored,
HighBit,
PixelRepresentation,
SmallestPixelValue,
LargestPixelValue,
WindowCenter,
WindowWidth,
RescaleIntercept,
RescaleSlope,
RescaleType,
Laterality,
ViewPosition,
AcquisitionDateTime,
FrameIncrementPointer,
FrameTime,
FrameTimeVector,
SliceThickness,
SpacingBetweenSlices,
LossyImageCompression,
DerivationDescription,
LossyImageCompressionRatio,
LossyImageCompressionMethod,
EchoNumber,
ContrastBolusAgent,
*/
} = dataset;
const instance = {
sopInstanceUid: SOPInstanceUID,
sopClassUid: SOPClassUID,
rows: Rows,
columns: Columns,
numberOfFrames: NumberOfFrames,
instanceNumber: InstanceNumber,
url: imageId,
modality: Modality,
/*
TODO: in case necessary to uncoment this block, double check every property
imageType: ImageType || DICOMWeb.getString(dataset['00080008']),
instanceNumber: InstanceNumber || DICOMWeb.getNumber(dataset['00200013']),
imagePositionPatient: ImagePositionPatient || DICOMWeb.getString(dataset['00200032']),
imageOrientationPatient: ImageOrientationPatient || DICOMWeb.getString(dataset['00200037']),
frameOfReferenceUID: FrameOfReferenceUID || DICOMWeb.getString(dataset['00200052']),
sliceLocation: SliceLocation || DICOMWeb.getNumber(dataset['00201041']),
samplesPerPixel: SamplesPerPixel || DICOMWeb.getNumber(dataset['00280002']),
photometricInterpretation: PhotometricInterpretation || DICOMWeb.getString(dataset['00280004']),
planarConfiguration: PlanarConfiguration || DICOMWeb.getNumber(dataset['00280006']),
pixelSpacing: PixelSpacing || DICOMWeb.getString(dataset['00280030']),
pixelAspectRatio: PixelAspectRatio || DICOMWeb.getString(dataset['00280034']),
bitsAllocated: BitsAllocated || DICOMWeb.getNumber(dataset['00280100']),
bitsStored: BitsStored || DICOMWeb.getNumber(dataset['00280101']),
highBit: HighBit || DICOMWeb.getNumber(dataset['00280102']),
pixelRepresentation: PixelRepresentation || DICOMWeb.getNumber(dataset['00280103']),
smallestPixelValue: SmallestPixelValue || DICOMWeb.getNumber(dataset['00280106']),
largestPixelValue: LargestPixelValue || DICOMWeb.getNumber(dataset['00280107']),
windowCenter: WindowCenter || DICOMWeb.getString(dataset['00281050']),
windowWidth: WindowWidth || DICOMWeb.getString(dataset['00281051']),
rescaleIntercept: RescaleIntercept || DICOMWeb.getNumber(dataset['00281052']),
rescaleSlope: RescaleSlope || DICOMWeb.getNumber(dataset['00281053']),
rescaleType: RescaleType || DICOMWeb.getNumber(dataset['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(dataset),
laterality: Laterality || DICOMWeb.getString(dataset['00200062']),
viewPosition: ViewPosition || DICOMWeb.getString(dataset['00185101']),
acquisitionDateTime: AcquisitionDateTime || DICOMWeb.getString(dataset['0008002A']),
frameIncrementPointer: FrameIncrementPointer || getFrameIncrementPointer(dataset['00280009']),
frameTime: FrameTime || DICOMWeb.getNumber(dataset['00181063']),
frameTimeVector: FrameTimeVector || parseFloatArray(
DICOMWeb.getString(dataset['00181065'])
),
sliceThickness: SliceThickness || DICOMWeb.getNumber(dataset['00180050']),
spacingBetweenSlices: SpacingBetweenSlices || DICOMWeb.getString(dataset['00180088']),
lossyImageCompression: LossyImageCompression || DICOMWeb.getString(dataset['00282110']),
derivationDescription: DerivationDescription || DICOMWeb.getString(dataset['00282111']),
lossyImageCompressionRatio: LossyImageCompressionRatio || DICOMWeb.getString(dataset['00282112']),
lossyImageCompressionMethod: LossyImageCompressionMethod || DICOMWeb.getString(dataset['00282114']),
echoNumber: EchoNumber || DICOMWeb.getString(dataset['00180086']),
contrastBolusAgent: ContrastBolusAgent || DICOMWeb.getString(dataset['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(dataset),
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),*/
};
const series = {
seriesInstanceUid: SeriesInstanceUID,
seriesDescription: SeriesDescription,
seriesNumber: SeriesNumber,
instances: [instance],
};
const study = {
studyInstanceUid: StudyInstanceUID,
studyDate: StudyDate,
studyTime: StudyTime,
accessionNumber: AccessionNumber,
referringPhysicianName: ReferringPhysicianName,
patientName: PatientName,
patientId: PatientID,
patientBirthdate: PatientBirthDate,
patientSex: PatientSex,
studyId: StudyID,
studyDescription: StudyDescription,
/*
TODO: in case necessary to uncomment this block, double check every property
numberOfStudyRelatedSeries: NumberOfStudyRelatedSeries || DICOMWeb.getString(dataset['00201206']),
numberOfStudyRelatedInstances: NumberOfStudyRelatedInstances || DICOMWeb.getString(dataset['00201208']),
modality: Modality || DICOMWeb.getString(dataset['00080060']),
modalitiesInStudy: ModalitiesInStudy || DICOMWeb.getString(dataset['00080061']),
modalities:
*/
seriesList: [series],
};
return study;
}
})();
export default DICOMFileLoader;
@@ -0,0 +1,6 @@
export default class FileLoader {
fileType;
loadFile(file, imageId) { }
getDataset(image, imageId) { }
getStudies(dataset, imageId) { }
}
@@ -0,0 +1,82 @@
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import FileLoader from './fileLoader';
import PDFFileLoader from './pdfFileLoader';
import DICOMFileLoader from './dicomFileLoader';
class FileLoaderService extends FileLoader {
fileType;
loader;
constructor(file) {
super();
const fileType = file && file.type;
this.loader = this.getLoader(fileType);
this.fileType = this.loader.fileType;
}
static groupSeries(studies) {
const groupBy = (list, groupByKey, listKey) => {
let nonKeyCounter = 1;
return list.reduce((acc, obj) => {
let key = obj[groupByKey];
const list = obj[listKey];
// in case key not found, group it using counter
key = !!key ? key : '' + nonKeyCounter++;
if (!acc[key]) {
acc[key] = { ...obj };
acc[key][listKey] = [];
}
acc[key][listKey].push(...list);
return acc;
}, {});
};
const studiesGrouped = Object.values(
groupBy(studies, 'studyInstanceUid', 'seriesList')
);
const result = studiesGrouped.map(studyGroup => {
const seriesGrouped = groupBy(
studyGroup.seriesList,
'seriesInstanceUid',
'instances'
);
studyGroup.seriesList = Object.values(seriesGrouped);
return studyGroup;
});
return result;
}
addFile(file) {
return cornerstoneWADOImageLoader.wadouri.fileManager.add(file);
}
loadFile(file, imageId) {
return this.loader.loadFile(file, imageId);
}
getDataset(image, imageId) {
return this.loader.getDataset(image, imageId);
}
getStudies(dataset, imageId) {
return this.loader.getStudies(dataset, imageId);
}
getLoader(fileType) {
if (fileType === 'application/pdf') {
return PDFFileLoader;
} else {
// Default to dicom loader
return DICOMFileLoader;
}
}
}
export default FileLoaderService;
@@ -0,0 +1,58 @@
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import FileLoader from './fileLoader';
const PDFFileLoader = new (class extends FileLoader {
fileType = 'application/pdf';
loadFile(file, imageId) {
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
}
getDataset(image, imageId) {
const dataset = {};
dataset.imageId = image.imageId || imageId;
return dataset;
}
getStudies(dataset, imageId) {
return this.getDefaultStudy(imageId);
}
getDefaultStudy(imageId) {
const study = {
studyInstanceUid: '',
studyDate: '',
studyTime: '',
accessionNumber: '',
referringPhysicianName: '',
patientName: '',
patientId: '',
patientBirthdate: '',
patientSex: '',
studyId: '',
studyDescription: '',
seriesList: [
{
seriesInstanceUid: '',
seriesDescription: '',
seriesNumber: '',
instances: [
{
sopInstanceUid: '',
sopClassUid: '1.2.840.10008.5.1.4.1.1.104.1',
rows: '',
columns: '',
numberOfFrames: 0,
instanceNumber: 1,
getImageId: () => imageId,
isLocalFile: true,
},
],
},
],
};
return study;
}
})();
export default PDFFileLoader;