diff --git a/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js b/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js index 62a98d206..25fdfd35b 100644 --- a/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js +++ b/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js @@ -190,6 +190,11 @@ const SegmentationPanel = ({ refreshSegmentations ); + document.addEventListener( + 'extensiondicomsegmentationsegloadingfailed', + cleanSegmentationComboBox + ); + /* * These are specific to each element; * Need to iterate cornerstone-tools tracked enabled elements? @@ -207,6 +212,10 @@ const SegmentationPanel = ({ 'extensiondicomsegmentationsegloaded', refreshSegmentations ); + document.removeEventListener( + 'extensiondicomsegmentationsegloadingfailed', + cleanSegmentationComboBox + ); cornerstoneTools.store.state.enabledElements.forEach(enabledElement => enabledElement.removeEventListener( 'cornerstonetoolslabelmapmodified', @@ -216,6 +225,17 @@ const SegmentationPanel = ({ }; }, [activeIndex, viewports]); + const cleanSegmentationComboBox = () => { + setState(state => ({ + ...state, + segmentsHidden: [], + segmentNumbers: [], + labelMapList: [], + segmentList: [], + isDisabled: true, + })); + } + const refreshSegmentations = () => { const activeViewport = getActiveViewport(); const isDisabled = !activeViewport || !activeViewport.StudyInstanceUID; @@ -277,7 +297,10 @@ const SegmentationPanel = ({ activeViewport.SeriesInstanceUID ); - return referencedSegDisplaysets.map((displaySet, index) => { + const filteredReferencedSegDisplaysets = referencedSegDisplaysets.filter( + (segDisplay => segDisplay.loadError !== true && segDisplay.isLoaded)); + + return filteredReferencedSegDisplaysets.map((displaySet, index) => { const { labelmapIndex, originLabelMapIndex, diff --git a/extensions/dicom-segmentation/src/getOHIFDicomSegSopClassHandler.js b/extensions/dicom-segmentation/src/getOHIFDicomSegSopClassHandler.js index 4d9435893..9142fc8a1 100644 --- a/extensions/dicom-segmentation/src/getOHIFDicomSegSopClassHandler.js +++ b/extensions/dicom-segmentation/src/getOHIFDicomSegSopClassHandler.js @@ -63,11 +63,12 @@ export default function getSopClassHandlerModule({ servicesManager }) { metadata, }; - segDisplaySet.getSourceDisplaySet = function(studies) { - return getSourceDisplaySet(studies, segDisplaySet); + segDisplaySet.getSourceDisplaySet = function(studies, activateLabelMap = true, onDisplaySetLoadFailureHandler) { + return getSourceDisplaySet(studies, segDisplaySet, activateLabelMap, onDisplaySetLoadFailureHandler); }; segDisplaySet.load = async function(referencedDisplaySet, studies) { + segDisplaySet.isLoading = true; segDisplaySet.isLoaded = true; const { StudyInstanceUID } = referencedDisplaySet; const segArrayBuffer = await DicomLoaderService.findDicomDataPromise( @@ -82,59 +83,53 @@ export default function getSopClassHandlerModule({ servicesManager }) { StudyInstanceUID, referencedDisplaySet.SeriesInstanceUID ); - return new Promise(async (resolve, reject) => { - let results; - try { - results = _parseSeg(segArrayBuffer, imageIds); - } catch (error) { - segDisplaySet.isLoaded = false; - segDisplaySet.loadError = true; - reject(error); - } - const { - labelmapBufferArray, - segMetadata, - segmentsOnFrame, - segmentsOnFrameArray, - } = results; - let labelmapIndex; - if (labelmapBufferArray.length > 1) { - let labelmapIndexes = []; - for (let i = 0; i < labelmapBufferArray.length; ++i) { - labelmapIndexes.push( - await loadSegmentation( - imageIds, - segDisplaySet, - labelmapBufferArray[i], - segMetadata, - segmentsOnFrame, - segmentsOnFrameArray[i] - ) - ); - } - /** - * Since overlapping segmentations have virtual labelmaps, - * originLabelMapIndex is used in the panel to select the correct dropdown value. - */ - segDisplaySet.hasOverlapping = true; - segDisplaySet.originLabelMapIndex = labelmapIndexes[0]; - labelmapIndex = labelmapIndexes[0]; - console.warn('Overlapping segmentations!'); - } else { - labelmapIndex = await loadSegmentation( - imageIds, - segDisplaySet, - labelmapBufferArray[0], - segMetadata, - segmentsOnFrame, - [] + const results = await _parseSeg(segArrayBuffer, imageIds); + if (results === undefined) { + return; + } + const { + labelmapBufferArray, + segMetadata, + segmentsOnFrame, + segmentsOnFrameArray, + } = results; + let labelmapIndex; + if (labelmapBufferArray.length > 1) { + let labelmapIndexes = []; + for (let i = 0; i < labelmapBufferArray.length; ++i) { + labelmapIndexes.push( + await loadSegmentation( + imageIds, + segDisplaySet, + labelmapBufferArray[i], + segMetadata, + segmentsOnFrame, + segmentsOnFrameArray[i] + ) ); } - resolve(labelmapIndex); - }); + /** + * Since overlapping segments have virtual labelmaps, + * originLabelMapIndex is used in the panel to select the correct dropdown value. + */ + segDisplaySet.hasOverlapping = true; + segDisplaySet.originLabelMapIndex = labelmapIndexes[0]; + labelmapIndex = labelmapIndexes[0]; + console.warn('Overlapping segments!'); + } else { + labelmapIndex = await loadSegmentation( + imageIds, + segDisplaySet, + labelmapBufferArray[0], + segMetadata, + segmentsOnFrame, + [] + ); + } }; + segDisplaySet.isLoading = false; return segDisplaySet; }, }; diff --git a/extensions/dicom-segmentation/src/getSourceDisplaySet.js b/extensions/dicom-segmentation/src/getSourceDisplaySet.js index f42351fce..c9612322a 100644 --- a/extensions/dicom-segmentation/src/getSourceDisplaySet.js +++ b/extensions/dicom-segmentation/src/getSourceDisplaySet.js @@ -3,10 +3,12 @@ import { classes } from '@ohif/core'; const { ImageSet } = classes; -export default function getSourceDisplaySet(studies, segDisplaySet) { +export default function getSourceDisplaySet(studies, segDisplaySet, activateLabelMap = true, onDisplaySetLoadFailureHandler) { const referencedDisplaySet = _getReferencedDisplaySet(segDisplaySet, studies); - setActiveLabelmap(referencedDisplaySet, studies, segDisplaySet); + if (activateLabelMap) { + setActiveLabelmap(referencedDisplaySet, studies, segDisplaySet, undefined, onDisplaySetLoadFailureHandler); + } return referencedDisplaySet; } diff --git a/extensions/dicom-segmentation/src/utils/setActiveLabelMap.js b/extensions/dicom-segmentation/src/utils/setActiveLabelMap.js index 8a08aad10..3644d861a 100644 --- a/extensions/dicom-segmentation/src/utils/setActiveLabelMap.js +++ b/extensions/dicom-segmentation/src/utils/setActiveLabelMap.js @@ -20,7 +20,7 @@ export default async function setActiveLabelmap( displaySet, callback = () => {}, onDisplaySetLoadFailure = err => { - throw new Error(err.message); + console.error(err.message); } ) { const studyMetadata = studyMetadataManager.get( @@ -47,17 +47,24 @@ export default async function setActiveLabelmap( return labelmapIndex; } - if (!displaySet.isLoaded) { - const loadPromise = displaySet.load(referencedDisplaySet, studies); + if (displaySet.isLoading) { + return activeLabelmapIndex; + } - loadPromise.catch(error => { + if (!displaySet.isLoaded) { + try { + await displaySet.load(referencedDisplaySet, studies); + } catch (error) { + displaySet.isLoaded = false; + displaySet.isLoading = false; + displaySet.loadError = true; onDisplaySetLoadFailure(error); - // Return old index. - return activeLabelmapIndex; - }); + const event = new CustomEvent('extensiondicomsegmentationsegloadingfailed'); + document.dispatchEvent(event); - await loadPromise; + return activeLabelmapIndex; + } } labelmapIndex = diff --git a/platform/core/src/utils/loadAndCacheDerivedDisplaySets.js b/platform/core/src/utils/loadAndCacheDerivedDisplaySets.js index 4fcf10db1..1cfac4ee7 100644 --- a/platform/core/src/utils/loadAndCacheDerivedDisplaySets.js +++ b/platform/core/src/utils/loadAndCacheDerivedDisplaySets.js @@ -45,9 +45,11 @@ import studyMetadataManager from './studyMetadataManager'; * @param {string} referencedDisplaySet.studyInstanceUid * @param {Array} referencedDisplaySet.sopClassUids * @param {Study[]} studies Collection of studies + * @param {object} logger + * @param {object} snackbar * @returns void */ -const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => { +async function loadAndCacheDerivedDisplaySets(referencedDisplaySet, studies, logger, snackbar) { const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet; const promises = []; const studyMetadata = studyMetadataManager.get(StudyInstanceUID); @@ -78,11 +80,10 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => { }); // For each type, see if any are loaded, if not load the most recent. - Object.keys(displaySetsPerModality).forEach(key => { + await Promise.all(Object.keys(displaySetsPerModality).map(async (key) => { const displaySets = displaySetsPerModality[key]; const isLoaded = displaySets.some(displaySet => displaySet.isLoaded); - if (isLoaded) { return; } @@ -107,20 +108,27 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => { recentDisplaySet.isLoading = true; - promises.push(recentDisplaySet.load(referencedDisplaySet, studies)); - }); + try { + await recentDisplaySet.load(referencedDisplaySet, studies); + } catch (error) { + logger.error({ error, message: error.message }); + snackbar.show({ + title: 'Error loading derived display set:', + message: error.message, + type: 'error', + error, + autoClose: false, + }); + } + })); - Promise.all(promises).then(() => { - /* - * TODO: Improve the way we notify parts of the app - * that depends on derived display sets to be loaded. - * (Implement pubsub for better tracking of derived display sets) - */ - const event = new CustomEvent('deriveddisplaysetsloadedandcached'); - document.dispatchEvent(event); - }); - - return promises; + /* + * TODO: Improve the way we notify parts of the app + * that depends on derived display sets to be loaded. + * (Implement pubsub for better tracking of derived display sets) + */ + const event = new CustomEvent('deriveddisplaysetsloadedandcached'); + document.dispatchEvent(event); }; export default loadAndCacheDerivedDisplaySets; diff --git a/platform/viewer/src/components/ViewportGrid/ViewportGrid.js b/platform/viewer/src/components/ViewportGrid/ViewportGrid.js index 2541caf08..e91680e16 100644 --- a/platform/viewer/src/components/ViewportGrid/ViewportGrid.js +++ b/platform/viewer/src/components/ViewportGrid/ViewportGrid.js @@ -41,19 +41,7 @@ const ViewportGrid = function(props) { useEffect(() => { if (isStudyLoaded) { viewportData.forEach(displaySet => { - const promises = loadAndCacheDerivedDisplaySets(displaySet, studies); - promises.forEach(promise => { - promise.catch(error => { - logger.error({ error, message: error.message }); - snackbar.show({ - title: 'Error loading derived display set:', - message: error.message, - type: 'error', - error, - autoClose: false, - }); - }); - }); + loadAndCacheDerivedDisplaySets(displaySet, studies, logger, snackbar); }); } }, [studies, viewportData, isStudyLoaded, snackbar]); diff --git a/platform/viewer/src/connectedComponents/ConnectedStudyBrowser.js b/platform/viewer/src/connectedComponents/ConnectedStudyBrowser.js index e0e277117..519103c29 100644 --- a/platform/viewer/src/connectedComponents/ConnectedStudyBrowser.js +++ b/platform/viewer/src/connectedComponents/ConnectedStudyBrowser.js @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import { StudyBrowser } from '@ohif/ui'; import cloneDeep from 'lodash.clonedeep'; import findDisplaySetByUID from './findDisplaySetByUID'; +import { servicesManager } from './../App.js'; const { studyMetadataManager } = OHIF.utils; @@ -48,8 +49,22 @@ const mapDispatchToProps = (dispatch, ownProps) => { if (displaySet.isDerived) { const { Modality } = displaySet; + if (Modality === 'SEG' && servicesManager) { + const {LoggerService, UINotificationService} = servicesManager.services; + const onDisplaySetLoadFailureHandler = error => { + LoggerService.error({ error, message: error.message }); + UINotificationService.show({ + title: 'DICOM Segmentation Loader', + message: error.message, + type: 'error', + autoClose: true, + }); + }; - displaySet = displaySet.getSourceDisplaySet(ownProps.studyMetadata); + displaySet = displaySet.getSourceDisplaySet(ownProps.studyMetadata, true, onDisplaySetLoadFailureHandler); + } else { + displaySet = displaySet.getSourceDisplaySet(ownProps.studyMetadata); + } if (!displaySet) { throw new Error( diff --git a/platform/viewer/src/connectedComponents/Viewer.js b/platform/viewer/src/connectedComponents/Viewer.js index 0eaadc847..06dc60a90 100644 --- a/platform/viewer/src/connectedComponents/Viewer.js +++ b/platform/viewer/src/connectedComponents/Viewer.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { useLogger } from '@ohif/ui'; import OHIF, { MODULE_TYPES, DICOMSR } from '@ohif/core'; import { withDialog } from '@ohif/ui'; @@ -13,6 +14,7 @@ import SidePanel from './../components/SidePanel.js'; import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog'; import { extensionManager } from './../App.js'; import { ReconstructionIssues } from './../../../core/src/enums.js'; +import dcmjs from 'dcmjs'; // Contexts import WhiteLabelingContext from '../context/WhiteLabelingContext.js'; @@ -21,6 +23,7 @@ import AppContext from '../context/AppContext'; import './Viewer.css'; import { finished } from 'stream'; +import { cornerstoneWADOImageLoader } from 'cornerstone-wado-image-loader'; class Viewer extends Component { static propTypes = { @@ -369,59 +372,144 @@ class Viewer extends Component { export default withDialog(Viewer); /** - * Async function to check if there are any inconsistences in the series - * (i.e. reconstructable to a 3D volume). If not reconstructable MPR is disabled. - * The actual computations are done in isDisplaySetReconstructable. + * Async function to check if there are any inconsistences in the series. * + * For segmentation checks that the geometry is consistent with the source images: + * 1) no frames out of plane; + * 2) have the same width and height. + * + * For reconstructable 3D volume: * 1) Is series multiframe? - * 2) Do the frames have different dimensions/numer of components/orientations? + * 2) Do the frames have different dimensions/number of components/orientations? * 3) Has the series any missing frames or irregular spacing? * 4) Is the series 4D? * + * If not reconstructable, MPR is disabled. + * The actual computations are done in isDisplaySetReconstructable. + * * @param {*object} displaySet * @returns {[string]} an array of strings containing the warnings */ -const _checkForSeriesInconsistencesWarnings = async function (displaySet) { - // NOTE: at the moment this function is async even if it does not perfom any heavy calculation. - // We may add or move here some of the computations - // done when creating the displaySet (see makeDisplaySet and isDisplaySetReconstructable). - // the thumbnail footnotes warning react element is already set up to handle a promise. +const _checkForSeriesInconsistencesWarnings = async function (displaySet, studies) { const warningsList = []; - if (displaySet.warningIssues && displaySet.warningIssues.length !== 0) { - displaySet.warningIssues.forEach(warning => { - switch (warning) { - case ReconstructionIssues.DATASET_4D: - warningsList.push("The dataset is 4D."); - break; - case ReconstructionIssues.VARYING_IMAGESDIMENSIONS: - warningsList.push("The dataset frames have different dimensions (rows, columns)."); - break; - case ReconstructionIssues.VARYING_IMAGESCOMPONENTS: - warningsList.push("The dataset frames have different components (Sample per pixel)."); - break; - case ReconstructionIssues.VARYING_IMAGESORIENTATION: - warningsList.push("The dataset frames have different orientation."); - break; - case ReconstructionIssues.IRREGULAR_SPACING: - warningsList.push("The dataset frames have different pixel spacing."); - break; - case ReconstructionIssues.MULTIFFRAMES: - warningsList.push("The dataset is a multiframes."); - break; - default: - break; - } - }); + + if (displaySet.Modality !== 'SEG') { + if (displaySet.warningIssues && displaySet.warningIssues.length !== 0) { + displaySet.warningIssues.forEach(warning => { + switch (warning) { + case ReconstructionIssues.DATASET_4D: + warningsList.push('The dataset is 4D.'); + break; + case ReconstructionIssues.VARYING_IMAGESDIMENSIONS: + warningsList.push('The dataset frames have different dimensions (rows, columns).'); + break; + case ReconstructionIssues.VARYING_IMAGESCOMPONENTS: + warningsList.push('The dataset frames have different components (Sample per pixel).'); + break; + case ReconstructionIssues.VARYING_IMAGESORIENTATION: + warningsList.push('The dataset frames have different orientation.'); + break; + case ReconstructionIssues.IRREGULAR_SPACING: + warningsList.push('The dataset frames have different pixel spacing.'); + break; + case ReconstructionIssues.MULTIFFRAMES: + warningsList.push('The dataset is a multiframes.'); + break; + default: + break; + } + }); warningsList.push('The datasets is not a reconstructable 3D volume. MPR mode is not available.'); + } + + if (displaySet.missingFrames && + (!displaySet.warningIssues || + (displaySet.warningIssues && !displaySet.warningIssues.find(warn => warn === ReconstructionIssues.DATASET_4D)))) { + warningsList.push('The datasets is missing frames: ' + displaySet.missingFrames + '.'); + } + } else { + const segMetadata = displaySet.metadata; + if (!segMetadata) { + return warningsList; + } + + const sourceDisplaySet = displaySet.getSourceDisplaySet(studies, false); + const imageIds = sourceDisplaySet.images.map(image => image.getImageId()); + if (!imageIds || imageIds.length === 0) { + return warningsList; + } + + for ( + let i = 0, groupsLen = segMetadata.PerFrameFunctionalGroupsSequence.length; + i < groupsLen; + ++i + ) { + const PerFrameFunctionalGroups = segMetadata.PerFrameFunctionalGroupsSequence[i]; + if (!PerFrameFunctionalGroups) { + continue; + } + + let SourceImageSequence = undefined; + if (segMetadata.SourceImageSequence) { + SourceImageSequence = segMetadata.SourceImageSequence[i]; + } else if (PerFrameFunctionalGroups.DerivationImageSequence) { + SourceImageSequence = + PerFrameFunctionalGroups.DerivationImageSequence + .SourceImageSequence; + } + if (!SourceImageSequence) { + if (warningsList.length === 0) { + const warningMessage = 'The segmentation ' + + 'has frames out of plane respect to the source images.'; + warningsList.push(warningMessage); + } + continue; + } + + const { + ReferencedSOPInstanceUID, + } = SourceImageSequence; + + const imageId = imageIds.find(imageId => { + const sopCommonModule = cornerstone.metaData.get( + "sopCommonModule", + imageId + ); + if (!sopCommonModule) { + return; + } + + return sopCommonModule.sopInstanceUID === ReferencedSOPInstanceUID; + }); + + if (!imageId) { + continue; + } + + const sourceImageMetadata = cornerstone.metaData.get( + "instance", + imageId + ); + if ( + segMetadata.Rows !== sourceImageMetadata.Rows || + segMetadata.Columns !== sourceImageMetadata.Columns + ) { + const warningMessage = 'The segmentation ' + + 'has frames with different geometry ' + + 'dimensions (Rows and Columns) respect to the source images.'; + warningsList.push(warningMessage); + break; + } + } + + if (warningsList.length !== 0) { + const warningMessage = 'The segmentation format is not supported yet. ' + + 'The segmentation data (segments) could not be loaded.'; + warningsList.push(warningMessage); + } } - if (displaySet.missingFrames && - (!displaySet.warningIssues || - (displaySet.warningIssues && !displaySet.warningIssues.find(warn => warn === ReconstructionIssues.DATASET_4D)))) { - warningsList.push('The datasets is missing frames: ' + displaySet.missingFrames + '.'); - } - - return warningsList + return warningsList; } /** @@ -463,7 +551,7 @@ const _mapStudiesToThumbnails = function(studies) { altImageText = displaySet.Modality ? displaySet.Modality : 'UN'; } - const hasWarnings = _checkForSeriesInconsistencesWarnings(displaySet) + const hasWarnings = _checkForSeriesInconsistencesWarnings(displaySet, studies); return { imageId, diff --git a/platform/viewer/src/connectedComponents/ViewerMain.js b/platform/viewer/src/connectedComponents/ViewerMain.js index 97992ecca..04c5e736a 100644 --- a/platform/viewer/src/connectedComponents/ViewerMain.js +++ b/platform/viewer/src/connectedComponents/ViewerMain.js @@ -1,5 +1,5 @@ import './ViewerMain.css'; - +import { servicesManager } from './../App.js'; import { Component } from 'react'; import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js'; import PropTypes from 'prop-types'; @@ -141,7 +141,22 @@ class ViewerMain extends Component { if (displaySet.isDerived) { const { Modality } = displaySet; - displaySet = displaySet.getSourceDisplaySet(this.props.studies); + if (Modality === 'SEG' && servicesManager) { + const {LoggerService, UINotificationService} = servicesManager.services; + const onDisplaySetLoadFailureHandler = error => { + LoggerService.error({ error, message: error.message }); + UINotificationService.show({ + title: 'DICOM Segmentation Loader', + message: error.message, + type: 'error', + autoClose: true, + }); + }; + + displaySet = displaySet.getSourceDisplaySet(this.props.studies, true, onDisplaySetLoadFailureHandler); + } else { + displaySet = displaySet.getSourceDisplaySet(this.props.studies); + } if (!displaySet) { throw new Error( diff --git a/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js b/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js index d2902d573..7f34b2eb9 100644 --- a/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js +++ b/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js @@ -301,7 +301,6 @@ function ViewerRetrieveStudyData({ }); setStudies(studies); - setIsStudyLoaded(true); } }; diff --git a/yarn.lock b/yarn.lock index 9b21af59c..616c6ff96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6619,10 +6619,10 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -dcmjs@0.18.3: - version "0.18.3" - resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.18.3.tgz#17d5a23d96b63264b8a1a859e4cb000b3e20495b" - integrity sha512-nmG6wEBcIMnadXAZOTcXwacBv+lrh8bqPwIKZwdy7AkdihLEwI6/OnyP+PYjzcdltuPu9wOQcKZionvXuCteag== +dcmjs@0.18.4: + version "0.18.4" + resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.18.4.tgz#70643e665dda373ed83bc7228981350f07dc3849" + integrity sha512-iprngI79c6iAJRq16/K8JNVvUvrSzN7u6yGES8pl2/KvtbSIRP2LEXwbZ01bKtXz9syEtiQNvdtTR+gTZtd4Sw== dependencies: "@babel/polyfill" "^7.8.3" "@babel/runtime" "^7.8.4"