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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -301,7 +301,6 @@ function ViewerRetrieveStudyData({
});
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"
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"