From e7e1a8a6cdfcc333c7d2723e156a2760f8fa722e Mon Sep 17 00:00:00 2001 From: ladeirarodolfo <39910206+ladeirarodolfo@users.noreply.github.com> Date: Fri, 27 Sep 2019 08:47:08 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Add=20DicomLoaderService?= =?UTF-8?q?=20&=20FileLoaderService=20to=20fix=20SR,=20PDF,=20and=20SEG=20?= =?UTF-8?q?support=20in=20local=20file=20and=20WADO-RS-only=20use=20cases?= =?UTF-8?q?=20(#862)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../src/handleSegmentationStorage.js | 17 +- .../dicom-html/src/OHIFDicomHtmlViewport.js | 53 +---- extensions/dicom-pdf/src/DicomPDFViewport.js | 35 +-- .../dicom-pdf/src/OHIFDicomPDFViewport.js | 63 ++---- .../src/classes/metadata/StudyMetadata.js | 3 +- .../studies/services/wado/retrieveMetadata.js | 1 + platform/core/src/utils/dicomLoaderService.js | 199 ++++++++++++++++++ platform/core/src/utils/getWADORSImageId.js | 13 +- platform/core/src/utils/index.js | 3 + platform/core/src/utils/index.test.js | 1 + .../ViewerLocalFileData.js | 3 + platform/viewer/src/lib/filesToStudies.js | 172 ++------------- .../lib/localFileLoaders/dicomFileLoader.js | 192 +++++++++++++++++ .../src/lib/localFileLoaders/fileLoader.js | 6 + .../lib/localFileLoaders/fileLoaderService.js | 82 ++++++++ .../src/lib/localFileLoaders/pdfFileLoader.js | 58 +++++ 16 files changed, 629 insertions(+), 272 deletions(-) create mode 100644 platform/core/src/utils/dicomLoaderService.js create mode 100644 platform/viewer/src/lib/localFileLoaders/dicomFileLoader.js create mode 100644 platform/viewer/src/lib/localFileLoaders/fileLoader.js create mode 100644 platform/viewer/src/lib/localFileLoaders/fileLoaderService.js create mode 100644 platform/viewer/src/lib/localFileLoaders/pdfFileLoader.js diff --git a/extensions/cornerstone/src/handleSegmentationStorage.js b/extensions/cornerstone/src/handleSegmentationStorage.js index 668d73cef..64ac60cfd 100644 --- a/extensions/cornerstone/src/handleSegmentationStorage.js +++ b/extensions/cornerstone/src/handleSegmentationStorage.js @@ -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 diff --git a/extensions/dicom-html/src/OHIFDicomHtmlViewport.js b/extensions/dicom-html/src/OHIFDicomHtmlViewport.js index 255b78e7c..0d9e26557 100644 --- a/extensions/dicom-html/src/OHIFDicomHtmlViewport.js +++ b/extensions/dicom-html/src/OHIFDicomHtmlViewport.js @@ -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 ( <> diff --git a/extensions/dicom-pdf/src/DicomPDFViewport.js b/extensions/dicom-pdf/src/DicomPDFViewport.js index ce618fcf6..55bf1a2c9 100644 --- a/extensions/dicom-pdf/src/DicomPDFViewport.js +++ b/extensions/dicom-pdf/src/DicomPDFViewport.js @@ -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() { diff --git a/extensions/dicom-pdf/src/OHIFDicomPDFViewport.js b/extensions/dicom-pdf/src/OHIFDicomPDFViewport.js index 37ed988dd..302a8ee39 100644 --- a/extensions/dicom-pdf/src/OHIFDicomPDFViewport.js +++ b/extensions/dicom-pdf/src/OHIFDicomPDFViewport.js @@ -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 }; diff --git a/platform/core/src/classes/metadata/StudyMetadata.js b/platform/core/src/classes/metadata/StudyMetadata.js index 20bd17097..0ca9e7ebf 100644 --- a/platform/core/src/classes/metadata/StudyMetadata.js +++ b/platform/core/src/classes/metadata/StudyMetadata.js @@ -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; } diff --git a/platform/core/src/studies/services/wado/retrieveMetadata.js b/platform/core/src/studies/services/wado/retrieveMetadata.js index 4f7839819..ae7de40f3 100644 --- a/platform/core/src/studies/services/wado/retrieveMetadata.js +++ b/platform/core/src/studies/services/wado/retrieveMetadata.js @@ -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, }; diff --git a/platform/core/src/utils/dicomLoaderService.js b/platform/core/src/utils/dicomLoaderService.js new file mode 100644 index 000000000..9abaaee6f --- /dev/null +++ b/platform/core/src/utils/dicomLoaderService.js @@ -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; diff --git a/platform/core/src/utils/getWADORSImageId.js b/platform/core/src/utils/getWADORSImageId.js index 56dd6ab0d..b75615b23 100644 --- a/platform/core/src/utils/getWADORSImageId.js +++ b/platform/core/src/utils/getWADORSImageId.js @@ -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; } diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index 5d18a51fa..0b57b2e47 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -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; diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index a9d1d5b72..a2d127a52 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -14,6 +14,7 @@ describe('Top level exports', () => { // Updates WADO-RS metaDataManager 'updateMetaDataManager', 'DICOMTagDescriptions', + 'DicomLoaderService', ].sort(); const exports = Object.keys(utils.default).sort(); diff --git a/platform/viewer/src/connectedComponents/ViewerLocalFileData.js b/platform/viewer/src/connectedComponents/ViewerLocalFileData.js index e36f90718..c3a451124 100644 --- a/platform/viewer/src/connectedComponents/ViewerLocalFileData.js +++ b/platform/viewer/src/connectedComponents/ViewerLocalFileData.js @@ -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); diff --git a/platform/viewer/src/lib/filesToStudies.js b/platform/viewer/src/lib/filesToStudies.js index 8e24db30d..4f192b04b 100644 --- a/platform/viewer/src/lib/filesToStudies.js +++ b/platform/viewer/src/lib/filesToStudies.js @@ -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()); } diff --git a/platform/viewer/src/lib/localFileLoaders/dicomFileLoader.js b/platform/viewer/src/lib/localFileLoaders/dicomFileLoader.js new file mode 100644 index 000000000..ba1b20ef2 --- /dev/null +++ b/platform/viewer/src/lib/localFileLoaders/dicomFileLoader.js @@ -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; diff --git a/platform/viewer/src/lib/localFileLoaders/fileLoader.js b/platform/viewer/src/lib/localFileLoaders/fileLoader.js new file mode 100644 index 000000000..753786895 --- /dev/null +++ b/platform/viewer/src/lib/localFileLoaders/fileLoader.js @@ -0,0 +1,6 @@ +export default class FileLoader { + fileType; + loadFile(file, imageId) { } + getDataset(image, imageId) { } + getStudies(dataset, imageId) { } +} diff --git a/platform/viewer/src/lib/localFileLoaders/fileLoaderService.js b/platform/viewer/src/lib/localFileLoaders/fileLoaderService.js new file mode 100644 index 000000000..35faee70f --- /dev/null +++ b/platform/viewer/src/lib/localFileLoaders/fileLoaderService.js @@ -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; diff --git a/platform/viewer/src/lib/localFileLoaders/pdfFileLoader.js b/platform/viewer/src/lib/localFileLoaders/pdfFileLoader.js new file mode 100644 index 000000000..c2651ded0 --- /dev/null +++ b/platform/viewer/src/lib/localFileLoaders/pdfFileLoader.js @@ -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;