IDC2345: implement thumbnails footnotes UI for segmentation series inconsistencies warning (#2364)

* IDC2345: implement thumbnails footnotes UI for segmentation series inconsistencies warning
This commit is contained in:
Davide Punzo 2021-04-16 20:14:04 +02:00 committed by GitHub
parent a08caa9bdb
commit 07b272e024
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 280 additions and 140 deletions

View File

@ -190,6 +190,11 @@ const SegmentationPanel = ({
refreshSegmentations refreshSegmentations
); );
document.addEventListener(
'extensiondicomsegmentationsegloadingfailed',
cleanSegmentationComboBox
);
/* /*
* These are specific to each element; * These are specific to each element;
* Need to iterate cornerstone-tools tracked enabled elements? * Need to iterate cornerstone-tools tracked enabled elements?
@ -207,6 +212,10 @@ const SegmentationPanel = ({
'extensiondicomsegmentationsegloaded', 'extensiondicomsegmentationsegloaded',
refreshSegmentations refreshSegmentations
); );
document.removeEventListener(
'extensiondicomsegmentationsegloadingfailed',
cleanSegmentationComboBox
);
cornerstoneTools.store.state.enabledElements.forEach(enabledElement => cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
enabledElement.removeEventListener( enabledElement.removeEventListener(
'cornerstonetoolslabelmapmodified', 'cornerstonetoolslabelmapmodified',
@ -216,6 +225,17 @@ const SegmentationPanel = ({
}; };
}, [activeIndex, viewports]); }, [activeIndex, viewports]);
const cleanSegmentationComboBox = () => {
setState(state => ({
...state,
segmentsHidden: [],
segmentNumbers: [],
labelMapList: [],
segmentList: [],
isDisabled: true,
}));
}
const refreshSegmentations = () => { const refreshSegmentations = () => {
const activeViewport = getActiveViewport(); const activeViewport = getActiveViewport();
const isDisabled = !activeViewport || !activeViewport.StudyInstanceUID; const isDisabled = !activeViewport || !activeViewport.StudyInstanceUID;
@ -277,7 +297,10 @@ const SegmentationPanel = ({
activeViewport.SeriesInstanceUID activeViewport.SeriesInstanceUID
); );
return referencedSegDisplaysets.map((displaySet, index) => { const filteredReferencedSegDisplaysets = referencedSegDisplaysets.filter(
(segDisplay => segDisplay.loadError !== true && segDisplay.isLoaded));
return filteredReferencedSegDisplaysets.map((displaySet, index) => {
const { const {
labelmapIndex, labelmapIndex,
originLabelMapIndex, originLabelMapIndex,

View File

@ -63,11 +63,12 @@ export default function getSopClassHandlerModule({ servicesManager }) {
metadata, metadata,
}; };
segDisplaySet.getSourceDisplaySet = function(studies) { segDisplaySet.getSourceDisplaySet = function(studies, activateLabelMap = true, onDisplaySetLoadFailureHandler) {
return getSourceDisplaySet(studies, segDisplaySet); return getSourceDisplaySet(studies, segDisplaySet, activateLabelMap, onDisplaySetLoadFailureHandler);
}; };
segDisplaySet.load = async function(referencedDisplaySet, studies) { segDisplaySet.load = async function(referencedDisplaySet, studies) {
segDisplaySet.isLoading = true;
segDisplaySet.isLoaded = true; segDisplaySet.isLoaded = true;
const { StudyInstanceUID } = referencedDisplaySet; const { StudyInstanceUID } = referencedDisplaySet;
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise( const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
@ -82,14 +83,10 @@ export default function getSopClassHandlerModule({ servicesManager }) {
StudyInstanceUID, StudyInstanceUID,
referencedDisplaySet.SeriesInstanceUID referencedDisplaySet.SeriesInstanceUID
); );
return new Promise(async (resolve, reject) => {
let results; const results = await _parseSeg(segArrayBuffer, imageIds);
try { if (results === undefined) {
results = _parseSeg(segArrayBuffer, imageIds); return;
} catch (error) {
segDisplaySet.isLoaded = false;
segDisplaySet.loadError = true;
reject(error);
} }
const { const {
labelmapBufferArray, labelmapBufferArray,
@ -97,7 +94,6 @@ export default function getSopClassHandlerModule({ servicesManager }) {
segmentsOnFrame, segmentsOnFrame,
segmentsOnFrameArray, segmentsOnFrameArray,
} = results; } = results;
let labelmapIndex; let labelmapIndex;
if (labelmapBufferArray.length > 1) { if (labelmapBufferArray.length > 1) {
let labelmapIndexes = []; let labelmapIndexes = [];
@ -114,13 +110,13 @@ export default function getSopClassHandlerModule({ servicesManager }) {
); );
} }
/** /**
* Since overlapping segmentations have virtual labelmaps, * Since overlapping segments have virtual labelmaps,
* originLabelMapIndex is used in the panel to select the correct dropdown value. * originLabelMapIndex is used in the panel to select the correct dropdown value.
*/ */
segDisplaySet.hasOverlapping = true; segDisplaySet.hasOverlapping = true;
segDisplaySet.originLabelMapIndex = labelmapIndexes[0]; segDisplaySet.originLabelMapIndex = labelmapIndexes[0];
labelmapIndex = labelmapIndexes[0]; labelmapIndex = labelmapIndexes[0];
console.warn('Overlapping segmentations!'); console.warn('Overlapping segments!');
} else { } else {
labelmapIndex = await loadSegmentation( labelmapIndex = await loadSegmentation(
imageIds, imageIds,
@ -131,10 +127,9 @@ export default function getSopClassHandlerModule({ servicesManager }) {
[] []
); );
} }
resolve(labelmapIndex);
});
}; };
segDisplaySet.isLoading = false;
return segDisplaySet; return segDisplaySet;
}, },
}; };

View File

@ -3,10 +3,12 @@ import { classes } from '@ohif/core';
const { ImageSet } = classes; const { ImageSet } = classes;
export default function getSourceDisplaySet(studies, segDisplaySet) { export default function getSourceDisplaySet(studies, segDisplaySet, activateLabelMap = true, onDisplaySetLoadFailureHandler) {
const referencedDisplaySet = _getReferencedDisplaySet(segDisplaySet, studies); const referencedDisplaySet = _getReferencedDisplaySet(segDisplaySet, studies);
setActiveLabelmap(referencedDisplaySet, studies, segDisplaySet); if (activateLabelMap) {
setActiveLabelmap(referencedDisplaySet, studies, segDisplaySet, undefined, onDisplaySetLoadFailureHandler);
}
return referencedDisplaySet; return referencedDisplaySet;
} }

View File

@ -20,7 +20,7 @@ export default async function setActiveLabelmap(
displaySet, displaySet,
callback = () => {}, callback = () => {},
onDisplaySetLoadFailure = err => { onDisplaySetLoadFailure = err => {
throw new Error(err.message); console.error(err.message);
} }
) { ) {
const studyMetadata = studyMetadataManager.get( const studyMetadata = studyMetadataManager.get(
@ -47,17 +47,24 @@ export default async function setActiveLabelmap(
return labelmapIndex; return labelmapIndex;
} }
if (!displaySet.isLoaded) { if (displaySet.isLoading) {
const loadPromise = displaySet.load(referencedDisplaySet, studies); 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); onDisplaySetLoadFailure(error);
// Return old index. const event = new CustomEvent('extensiondicomsegmentationsegloadingfailed');
return activeLabelmapIndex; document.dispatchEvent(event);
});
await loadPromise; return activeLabelmapIndex;
}
} }
labelmapIndex = labelmapIndex =

View File

@ -45,9 +45,11 @@ import studyMetadataManager from './studyMetadataManager';
* @param {string} referencedDisplaySet.studyInstanceUid * @param {string} referencedDisplaySet.studyInstanceUid
* @param {Array} referencedDisplaySet.sopClassUids * @param {Array} referencedDisplaySet.sopClassUids
* @param {Study[]} studies Collection of studies * @param {Study[]} studies Collection of studies
* @param {object} logger
* @param {object} snackbar
* @returns void * @returns void
*/ */
const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => { async function loadAndCacheDerivedDisplaySets(referencedDisplaySet, studies, logger, snackbar) {
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet; const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
const promises = []; const promises = [];
const studyMetadata = studyMetadataManager.get(StudyInstanceUID); 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. // 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 displaySets = displaySetsPerModality[key];
const isLoaded = displaySets.some(displaySet => displaySet.isLoaded); const isLoaded = displaySets.some(displaySet => displaySet.isLoaded);
if (isLoaded) { if (isLoaded) {
return; return;
} }
@ -107,10 +108,20 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
recentDisplaySet.isLoading = true; 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 * TODO: Improve the way we notify parts of the app
* that depends on derived display sets to be loaded. * that depends on derived display sets to be loaded.
@ -118,9 +129,6 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
*/ */
const event = new CustomEvent('deriveddisplaysetsloadedandcached'); const event = new CustomEvent('deriveddisplaysetsloadedandcached');
document.dispatchEvent(event); document.dispatchEvent(event);
});
return promises;
}; };
export default loadAndCacheDerivedDisplaySets; export default loadAndCacheDerivedDisplaySets;

View File

@ -41,19 +41,7 @@ const ViewportGrid = function(props) {
useEffect(() => { useEffect(() => {
if (isStudyLoaded) { if (isStudyLoaded) {
viewportData.forEach(displaySet => { viewportData.forEach(displaySet => {
const promises = loadAndCacheDerivedDisplaySets(displaySet, studies); loadAndCacheDerivedDisplaySets(displaySet, studies, logger, snackbar);
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,
});
});
});
}); });
} }
}, [studies, viewportData, isStudyLoaded, snackbar]); }, [studies, viewportData, isStudyLoaded, snackbar]);

View File

@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import { StudyBrowser } from '@ohif/ui'; import { StudyBrowser } from '@ohif/ui';
import cloneDeep from 'lodash.clonedeep'; import cloneDeep from 'lodash.clonedeep';
import findDisplaySetByUID from './findDisplaySetByUID'; import findDisplaySetByUID from './findDisplaySetByUID';
import { servicesManager } from './../App.js';
const { studyMetadataManager } = OHIF.utils; const { studyMetadataManager } = OHIF.utils;
@ -48,8 +49,22 @@ const mapDispatchToProps = (dispatch, ownProps) => {
if (displaySet.isDerived) { if (displaySet.isDerived) {
const { Modality } = displaySet; 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, true, onDisplaySetLoadFailureHandler);
} else {
displaySet = displaySet.getSourceDisplaySet(ownProps.studyMetadata); displaySet = displaySet.getSourceDisplaySet(ownProps.studyMetadata);
}
if (!displaySet) { if (!displaySet) {
throw new Error( throw new Error(

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { useLogger } from '@ohif/ui';
import OHIF, { MODULE_TYPES, DICOMSR } from '@ohif/core'; import OHIF, { MODULE_TYPES, DICOMSR } from '@ohif/core';
import { withDialog } from '@ohif/ui'; import { withDialog } from '@ohif/ui';
@ -13,6 +14,7 @@ import SidePanel from './../components/SidePanel.js';
import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog'; import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog';
import { extensionManager } from './../App.js'; import { extensionManager } from './../App.js';
import { ReconstructionIssues } from './../../../core/src/enums.js'; import { ReconstructionIssues } from './../../../core/src/enums.js';
import dcmjs from 'dcmjs';
// Contexts // Contexts
import WhiteLabelingContext from '../context/WhiteLabelingContext.js'; import WhiteLabelingContext from '../context/WhiteLabelingContext.js';
@ -21,6 +23,7 @@ import AppContext from '../context/AppContext';
import './Viewer.css'; import './Viewer.css';
import { finished } from 'stream'; import { finished } from 'stream';
import { cornerstoneWADOImageLoader } from 'cornerstone-wado-image-loader';
class Viewer extends Component { class Viewer extends Component {
static propTypes = { static propTypes = {
@ -369,44 +372,48 @@ class Viewer extends Component {
export default withDialog(Viewer); export default withDialog(Viewer);
/** /**
* Async function to check if there are any inconsistences in the series * 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.
* *
* 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? * 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? * 3) Has the series any missing frames or irregular spacing?
* 4) Is the series 4D? * 4) Is the series 4D?
* *
* If not reconstructable, MPR is disabled.
* The actual computations are done in isDisplaySetReconstructable.
*
* @param {*object} displaySet * @param {*object} displaySet
* @returns {[string]} an array of strings containing the warnings * @returns {[string]} an array of strings containing the warnings
*/ */
const _checkForSeriesInconsistencesWarnings = async function (displaySet) { const _checkForSeriesInconsistencesWarnings = async function (displaySet, studies) {
// 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 warningsList = []; const warningsList = [];
if (displaySet.Modality !== 'SEG') {
if (displaySet.warningIssues && displaySet.warningIssues.length !== 0) { if (displaySet.warningIssues && displaySet.warningIssues.length !== 0) {
displaySet.warningIssues.forEach(warning => { displaySet.warningIssues.forEach(warning => {
switch (warning) { switch (warning) {
case ReconstructionIssues.DATASET_4D: case ReconstructionIssues.DATASET_4D:
warningsList.push("The dataset is 4D."); warningsList.push('The dataset is 4D.');
break; break;
case ReconstructionIssues.VARYING_IMAGESDIMENSIONS: case ReconstructionIssues.VARYING_IMAGESDIMENSIONS:
warningsList.push("The dataset frames have different dimensions (rows, columns)."); warningsList.push('The dataset frames have different dimensions (rows, columns).');
break; break;
case ReconstructionIssues.VARYING_IMAGESCOMPONENTS: case ReconstructionIssues.VARYING_IMAGESCOMPONENTS:
warningsList.push("The dataset frames have different components (Sample per pixel)."); warningsList.push('The dataset frames have different components (Sample per pixel).');
break; break;
case ReconstructionIssues.VARYING_IMAGESORIENTATION: case ReconstructionIssues.VARYING_IMAGESORIENTATION:
warningsList.push("The dataset frames have different orientation."); warningsList.push('The dataset frames have different orientation.');
break; break;
case ReconstructionIssues.IRREGULAR_SPACING: case ReconstructionIssues.IRREGULAR_SPACING:
warningsList.push("The dataset frames have different pixel spacing."); warningsList.push('The dataset frames have different pixel spacing.');
break; break;
case ReconstructionIssues.MULTIFFRAMES: case ReconstructionIssues.MULTIFFRAMES:
warningsList.push("The dataset is a multiframes."); warningsList.push('The dataset is a multiframes.');
break; break;
default: default:
break; break;
@ -420,8 +427,89 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet) {
(displaySet.warningIssues && !displaySet.warningIssues.find(warn => warn === ReconstructionIssues.DATASET_4D)))) { (displaySet.warningIssues && !displaySet.warningIssues.find(warn => warn === ReconstructionIssues.DATASET_4D)))) {
warningsList.push('The datasets is missing frames: ' + displaySet.missingFrames + '.'); warningsList.push('The datasets is missing frames: ' + displaySet.missingFrames + '.');
} }
} else {
const segMetadata = displaySet.metadata;
if (!segMetadata) {
return warningsList;
}
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);
}
}
return warningsList;
} }
/** /**
@ -463,7 +551,7 @@ const _mapStudiesToThumbnails = function(studies) {
altImageText = displaySet.Modality ? displaySet.Modality : 'UN'; altImageText = displaySet.Modality ? displaySet.Modality : 'UN';
} }
const hasWarnings = _checkForSeriesInconsistencesWarnings(displaySet) const hasWarnings = _checkForSeriesInconsistencesWarnings(displaySet, studies);
return { return {
imageId, imageId,

View File

@ -1,5 +1,5 @@
import './ViewerMain.css'; import './ViewerMain.css';
import { servicesManager } from './../App.js';
import { Component } from 'react'; import { Component } from 'react';
import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js'; import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -141,7 +141,22 @@ class ViewerMain extends Component {
if (displaySet.isDerived) { if (displaySet.isDerived) {
const { Modality } = displaySet; 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(this.props.studies, true, onDisplaySetLoadFailureHandler);
} else {
displaySet = displaySet.getSourceDisplaySet(this.props.studies); displaySet = displaySet.getSourceDisplaySet(this.props.studies);
}
if (!displaySet) { if (!displaySet) {
throw new Error( throw new Error(

View File

@ -301,7 +301,6 @@ function ViewerRetrieveStudyData({
}); });
setStudies(studies); setStudies(studies);
setIsStudyLoaded(true);
} }
}; };

View File

@ -6619,10 +6619,10 @@ dateformat@^3.0.0:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
dcmjs@0.18.3: dcmjs@0.18.4:
version "0.18.3" version "0.18.4"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.18.3.tgz#17d5a23d96b63264b8a1a859e4cb000b3e20495b" resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.18.4.tgz#70643e665dda373ed83bc7228981350f07dc3849"
integrity sha512-nmG6wEBcIMnadXAZOTcXwacBv+lrh8bqPwIKZwdy7AkdihLEwI6/OnyP+PYjzcdltuPu9wOQcKZionvXuCteag== integrity sha512-iprngI79c6iAJRq16/K8JNVvUvrSzN7u6yGES8pl2/KvtbSIRP2LEXwbZ01bKtXz9syEtiQNvdtTR+gTZtd4Sw==
dependencies: dependencies:
"@babel/polyfill" "^7.8.3" "@babel/polyfill" "^7.8.3"
"@babel/runtime" "^7.8.4" "@babel/runtime" "^7.8.4"