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 2019-09-27 08:47:08 -03:00 committed by Erik Ziegler
parent e66c27affb
commit e7e1a8a6cd
16 changed files with 629 additions and 272 deletions

View File

@ -4,7 +4,7 @@ import OHIF from '@ohif/core';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
const { StackManager } = OHIF.utils;
const { StackManager, DicomLoaderService } = OHIF.utils;
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
const study = studies.find(
@ -52,14 +52,6 @@ function addSegMetadataToCornerstoneToolState(
}
}
function retrieveDicomData(wadoUri) {
// TODO: Authorization header depends on the server. If we ever have multiple servers
// we will need to figure out how / when to pass this information in.
return fetch(wadoUri, {
headers: OHIF.DICOMWeb.getAuthorizationHeader(),
}).then(response => response.arrayBuffer());
}
async function handleSegmentationStorage(
studies,
studyInstanceUid,
@ -73,8 +65,11 @@ async function handleSegmentationStorage(
studyInstanceUid,
displaySetInstanceUid
);
const segWadoUri = displaySet.images[0].getData().wadouri;
const arrayBuffer = await retrieveDicomData(segWadoUri);
const arrayBuffer = await DicomLoaderService.findDicomDataPromise(
displaySet,
studies
);
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict

View File

@ -1,46 +1,34 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import DicomHtmlViewport from './DicomHtmlViewport';
import OHIF from '@ohif/core';
const { DicomLoaderService } = OHIF.utils;
class OHIFDicomHtmlViewport extends Component {
static propTypes = {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportIndex: PropTypes.number
viewportIndex: PropTypes.number,
};
state = {
byteArray: null,
error: null
error: null,
};
componentDidMount() {
const { displaySet } = this.props.viewportData;
const {
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
} = displaySet;
const { displaySet, studies } = this.props.viewportData;
this.retrieveDicomData(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
).then(
byteArray => {
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
data => {
const byteArray = new Uint8Array(data);
this.setState({
byteArray
byteArray: byteArray,
});
},
error => {
this.setState({
error
error,
});
throw new Error(error);
@ -48,27 +36,6 @@ class OHIFDicomHtmlViewport extends Component {
);
}
retrieveDicomData(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
) {
// TODO: Passing in a lot of data we aren't using
// TODO: Authorization header depends on the server. If we ever have multiple servers
// we will need to figure out how / when to pass this information in.
return fetch(wadoUri, {
headers: authorizationHeaders
})
.then(response => response.arrayBuffer())
.then(arraybuffer => {
return new Uint8Array(arraybuffer);
});
}
render() {
return (
<>

View File

@ -4,41 +4,46 @@ import TypedArrayProp from './TypedArrayProp';
// TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = {
ENCAPSULATED_PDF: '1.2.840.10008.5.1.4.1.1.104.1'
ENCAPSULATED_PDF: '1.2.840.10008.5.1.4.1.1.104.1',
};
class DicomPDFViewport extends Component {
state = {
fileURL: null,
error: null
error: null,
};
static propTypes = {
byteArray: TypedArrayProp.uint8
byteArray: TypedArrayProp.uint8,
};
renderPDF = dataSet => {
const sopClassUid = dataSet.string('x00080016');
renderPDF = (dataSet, byteArray) => {
let pdfByteArray = byteArray;
if (sopClassUid !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
throw new Error('This is not a DICOM-encapsulated PDF');
if (dataSet) {
const sopClassUid = dataSet.string('x00080016');
if (sopClassUid !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
throw new Error('This is not a DICOM-encapsulated PDF');
}
const fileTag = dataSet.elements.x00420011;
const offset = fileTag.dataOffset;
const remainder = offset + fileTag.length;
pdfByteArray = dataSet.byteArray.slice(offset, remainder);
}
const fileTag = dataSet.elements.x00420011;
const offset = fileTag.dataOffset;
const remainder = offset + fileTag.length;
const pdfByteArray = dataSet.byteArray.slice(offset, remainder);
const PDF = new Blob([pdfByteArray], { type: 'application/pdf' });
const fileURL = URL.createObjectURL(PDF);
this.setState({
fileURL
fileURL,
});
};
parseByteArray = byteArray => {
const options = {
untilTag: ''
untilTag: '',
};
let dataSet;
@ -47,7 +52,7 @@ class DicomPDFViewport extends Component {
dataSet = dicomParser.parseDicom(byteArray, options);
} catch (error) {
this.setState({
error
error,
});
}
@ -57,7 +62,7 @@ class DicomPDFViewport extends Component {
componentDidMount() {
const dataSet = this.parseByteArray(this.props.byteArray);
this.renderPDF(dataSet);
this.renderPDF(dataSet, this.props.byteArray);
}
render() {

View File

@ -1,60 +1,54 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import OHIF from "@ohif/core";
import OHIFComponentPlugin from "./OHIFComponentPlugin.js";
import DicomPDFViewport from "./DicomPDFViewport";
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import OHIF from '@ohif/core';
import OHIFComponentPlugin from './OHIFComponentPlugin.js';
import DicomPDFViewport from './DicomPDFViewport';
const { DICOMWeb } = OHIF;
const { DicomLoaderService } = OHIF.utils;
class OHIFDicomPDFViewport extends Component {
static propTypes = {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportIndex: PropTypes.number
viewportIndex: PropTypes.number,
};
state = {
byteArray: null,
error: null
error: null,
};
static id = "DicomPDFViewportPDF";
static id = 'DicomPDFViewportPDF';
static init() {
console.log("DicomPDFViewport init()");
console.log('DicomPDFViewport init()');
}
static destroy() {
console.log("DicomPDFViewport destroy()");
console.log('DicomPDFViewport destroy()');
}
componentDidMount() {
const { displaySet } = this.props.viewportData;
const { displaySet, studies } = this.props.viewportData;
const {
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
authorizationHeaders,
} = displaySet;
this.retrieveDicomData(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
).then(
byteArray => {
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
data => {
const byteArray = new Uint8Array(data);
this.setState({
byteArray
byteArray: byteArray,
});
},
error => {
this.setState({
error
error,
});
throw new Error(error);
@ -62,27 +56,6 @@ class OHIFDicomPDFViewport extends Component {
);
}
retrieveDicomData(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
) {
// TODO: Passing in a lot of data we aren't using
// TODO: Authorization header depends on the server. If we ever have multiple servers
// we will need to figure out how / when to pass this information in.
return fetch(wadoUri, {
headers: authorizationHeaders
})
.then(response => response.arrayBuffer())
.then(arraybuffer => {
return new Uint8Array(arraybuffer);
});
}
render() {
const { id, init, destroy } = OHIFDicomPDFViewport;
const pluginProps = { id, init, destroy };

View File

@ -129,6 +129,7 @@ export class StudyMetadata extends Metadata {
);
if (displaySet) {
displaySet.sopClassModule = true;
displaySets.push(displaySet);
return;
@ -214,7 +215,7 @@ export class StudyMetadata extends Metadata {
* @returns {boolean} True on success, false on failure.
*/
addDisplaySet(displaySet) {
if (displaySet instanceof ImageSet) {
if (displaySet instanceof ImageSet || displaySet.sopClassModule) {
this._displaySets.push(displaySet);
return true;
}

View File

@ -402,6 +402,7 @@ async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
baseWadoRsUri: baseWadoRsUri,
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
wadoRoot: server.wadoRoot,
imageRendering: server.imageRendering,
thumbnailRendering: server.thumbnailRendering,
};

View File

@ -0,0 +1,199 @@
import cornerstone from 'cornerstone-core';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import { api } from 'dicomweb-client';
import DICOMWeb from '../DICOMWeb';
const getImageId = imageObj => {
if (!imageObj) {
return;
}
return typeof imageObj.getImageId === 'function'
? imageObj.getImageId()
: imageObj.url;
};
const findImageIdOnStudies = (studies, displaySetInstanceUid) => {
const study = studies.find(study => {
const displaySet = study.displaySets.some(
displaySet => displaySet.displaySetInstanceUid === displaySetInstanceUid
);
return displaySet;
});
const { seriesList = [] } = study;
const { instances = [] } = seriesList[0] || {};
const instance = instances[0];
return getImageId(instance);
};
const someInvalidStrings = strings => {
const stringsArray = Array.isArray(strings) ? strings : [strings];
const emptyString = string => !string;
let invalid = stringsArray.some(emptyString);
return invalid;
};
const getImageInstance = dataset => {
return dataset && dataset.images && dataset.images[0];
};
const getImageInstanceId = imageInstance => {
return getImageId(imageInstance);
};
const fetchIt = (url, headers = DICOMWeb.getAuthorizationHeader()) => {
return fetch(url, headers).then(response => response.arrayBuffer());
};
const cornerstoneRetriever = imageId => {
return cornerstone.loadAndCacheImage(imageId).then(image => {
return image && image.data && image.data.byteArray.buffer;
});
};
const wadorsRetriever = (
url,
studyInstanceUID,
seriesInstanceUID,
sopInstanceUID,
headers = DICOMWeb.getAuthorizationHeader()
) => {
const config = {
url,
headers,
};
const dicomWeb = new api.DICOMwebClient(config);
return dicomWeb.retrieveInstance({
studyInstanceUID,
seriesInstanceUID,
sopInstanceUID,
});
};
const getImageLoaderType = imageId => {
const loaderRegExp = /^\w+\:/;
const loaderType = loaderRegExp.exec(imageId);
return (
(loaderRegExp.lastIndex === 0 &&
loaderType &&
loaderType[0] &&
loaderType[0].replace(':', '')) ||
''
);
};
const DicomLoaderService = new (class {
getLocalData(dataset, studies) {
if (dataset && dataset.localFile) {
// Use referenced imageInstance
const imageInstance = getImageInstance(dataset);
let imageId = getImageInstanceId(imageInstance);
// or Try to get it from studies
if (someInvalidStrings(imageId)) {
imageId = findImageIdOnStudies(studies, dataset.displaySetInstanceUid);
}
if (!someInvalidStrings(imageId)) {
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
}
}
}
getDataByImageType(dataset) {
const imageInstance = getImageInstance(dataset);
if (imageInstance) {
const imageId = getImageInstanceId(imageInstance);
let getDicomDataMethod = fetchIt;
const loaderType = getImageLoaderType(imageId);
switch (loaderType) {
case 'dicomfile':
getDicomDataMethod = cornerstoneRetriever.bind(this, imageId);
break;
case 'wadors':
const url = imageInstance.getData().wadoRoot;
const studyInstanceUID = imageInstance.getStudyInstanceUID();
const seriesInstanceUID = imageInstance.getSeriesInstanceUID();
const sopInstanceUID = imageInstance.getSOPInstanceUID();
const invalidParams = someInvalidStrings([
url,
studyInstanceUID,
seriesInstanceUID,
sopInstanceUID,
]);
if (invalidParams) {
return;
}
getDicomDataMethod = wadorsRetriever.bind(
this,
url,
studyInstanceUID,
seriesInstanceUID,
sopInstanceUID
);
break;
case 'wadouri':
// Strip out the image loader specifier
imageId = imageId.substring(imageId.indexOf(':') + 1);
if (someInvalidStrings(imageId)) {
return;
}
getDicomDataMethod = fetchIt.bind(this, imageId);
break;
}
return getDicomDataMethod();
}
}
getDataByDatasetType(dataset) {
const {
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
authorizationHeaders,
wadoRoot,
wadoUri,
} = dataset;
// Retrieve wadors or just try to fetch wadouri
if (!someInvalidStrings(wadoRoot)) {
return wadorsRetriever(
wadoRoot,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
authorizationHeaders
);
} else if (!someInvalidStrings(wadoUri)) {
return fetchIt(wadoUri, { headers: authorizationHeaders });
}
}
*getLoaderIterator(dataset, studies) {
yield this.getLocalData(dataset, studies);
yield this.getDataByImageType(dataset);
yield this.getDataByDatasetType(dataset);
}
findDicomDataPromise(dataset, studies) {
const loaderIterator = this.getLoaderIterator(dataset, studies);
// it returns first valid retriever method.
for (const loader of loaderIterator) {
if (loader) {
return loader;
}
}
// in case of no valid loader
throw new Error('Invalid dicom data loader');
}
})();
export default DicomLoaderService;

View File

@ -5,11 +5,16 @@ function getWADORSImageUrl(instance, frame) {
return;
}
// We need to sum 1 because WADO-RS frame number is 1-based
frame = frame ? parseInt(frame) + 1 : 1;
// Use null to obtain an imageId which represents the instance
if (frame === null) {
wadorsuri = wadorsuri.replace(/frames\/(\d+)/, '');
} else {
// We need to sum 1 because WADO-RS frame number is 1-based
frame = frame ? parseInt(frame) + 1 : 1;
// Replaces /frame/1 by /frame/{frame}
wadorsuri = wadorsuri.replace(/frames\/(\d+)/, `frames/${frame}`);
// Replaces /frame/1 by /frame/{frame}
wadorsuri = wadorsuri.replace(/frames\/(\d+)/, `frames/${frame}`);
}
return wadorsuri;
}

View File

@ -8,6 +8,7 @@ import sortBy from './sortBy.js';
import studyMetadataManager from './studyMetadataManager';
import updateMetaDataManager from './updateMetaDataManager.js';
import writeScript from './writeScript.js';
import DicomLoaderService from './dicomLoaderService.js';
const utils = {
guid,
@ -21,6 +22,7 @@ const utils = {
// Updates WADO-RS metaDataManager
updateMetaDataManager,
DICOMTagDescriptions,
DicomLoaderService,
};
export {
@ -35,6 +37,7 @@ export {
// Updates WADO-RS metaDataManager
updateMetaDataManager,
DICOMTagDescriptions,
DicomLoaderService,
};
export default utils;

View File

@ -14,6 +14,7 @@ describe('Top level exports', () => {
// Updates WADO-RS metaDataManager
'updateMetaDataManager',
'DICOMTagDescriptions',
'DicomLoaderService',
].sort();
const exports = Object.keys(utils.default).sort();

View File

@ -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);

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());
}

View File

@ -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;

View File

@ -0,0 +1,6 @@
export default class FileLoader {
fileType;
loadFile(file, imageId) { }
getDataset(image, imageId) { }
getStudies(dataset, imageId) { }
}

View File

@ -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;

View File

@ -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;