fix: 🐛 Proper error handling for derived display sets (#1708)

* fix: 🐛 Proper error handling for derived display sets

* Revert idc.js change
This commit is contained in:
James Petts 2020-05-04 15:10:39 +01:00 committed by GitHub
parent 9ea37a2201
commit 5b20d8f323
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 162 additions and 109 deletions

View File

@ -36,7 +36,7 @@
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.12.5", "cornerstone-tools": "4.12.5",
"cornerstone-wado-image-loader": "^3.1.0", "cornerstone-wado-image-loader": "^3.1.0",
"dcmjs": "^0.12.2", "dcmjs": "^0.12.3",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -29,7 +29,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"dcmjs": "^0.12.2", "dcmjs": "^0.12.3",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6" "react-dom": "^16.8.6"

View File

@ -31,7 +31,7 @@
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.0.9", "cornerstone-tools": "^4.0.9",
"dcmjs": "^0.12.2", "dcmjs": "^0.12.3",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6" "react-dom": "^16.8.6"

View File

@ -1,71 +0,0 @@
import { MODULE_TYPES, utils } from '@ohif/core';
import loadSegmentation from './loadSegmentation';
// TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = {
DICOM_SEG: '1.2.840.10008.5.1.4.1.1.66.4',
};
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
// TODO: Handle the case where there is more than one SOP Class Handler for the
// same SOP Class.
const OHIFDicomSegSopClassHandler = {
id: 'OHIFDicomSegSopClassHandler',
type: MODULE_TYPES.SOP_CLASS_HANDLER,
sopClassUIDs,
getDisplaySetFromSeries: function (
series,
study,
dicomWebClient,
authorizationHeaders
) {
const instance = series.getFirstInstance();
const metadata = instance.getData().metadata;
const {
SeriesDate,
SeriesTime,
SeriesDescription,
FrameOfReferenceUID,
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = metadata;
const segDisplaySet = {
Modality: 'SEG',
displaySetInstanceUID: utils.guid(),
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
FrameOfReferenceUID,
authorizationHeaders,
metadata,
isDerived: true,
referencedDisplaySetUID: null, // Assigned when loaded.
labelmapIndex: null, // Assigned when loaded.
isLoaded: false,
SeriesDate,
SeriesTime,
SeriesDescription,
};
segDisplaySet.load = function (referencedDisplaySet, studies) {
return loadSegmentation(
segDisplaySet,
referencedDisplaySet,
studies
).catch(error => {
segDisplaySet.isLoaded = false;
throw new Error(error);
});
};
return segDisplaySet;
},
};
export default OHIFDicomSegSopClassHandler;

View File

@ -41,6 +41,7 @@ const SegmentationPanel = ({
activeIndex, activeIndex,
isOpen, isOpen,
onSegItemClick, onSegItemClick,
UINotificationService,
}) => { }) => {
/* /*
* TODO: wrap get/set interactions with the cornerstoneTools * TODO: wrap get/set interactions with the cornerstoneTools
@ -115,7 +116,11 @@ const SegmentationPanel = ({
firstImageId, firstImageId,
activeViewport activeViewport
); );
const segmentList = getSegmentList(labelmap3D, firstImageId, brushStackState); const segmentList = getSegmentList(
labelmap3D,
firstImageId,
brushStackState
);
setState(state => ({ setState(state => ({
...state, ...state,
brushStackState, brushStackState,
@ -130,7 +135,14 @@ const SegmentationPanel = ({
segmentList: [], segmentList: [],
})); }));
} }
}, [studies, viewports, activeIndex, getLabelmapList, getSegmentList, state.selectedSegmentation]); }, [
studies,
viewports,
activeIndex,
getLabelmapList,
getSegmentList,
state.selectedSegmentation,
]);
/* Handle open/closed panel behaviour */ /* Handle open/closed panel behaviour */
useEffect(() => { useEffect(() => {
@ -170,7 +182,8 @@ const SegmentationPanel = ({
studies, studies,
displaySet, displaySet,
firstImageId, firstImageId,
brushStackState.activeLabelmapIndex brushStackState.activeLabelmapIndex,
UINotificationService
); );
updateState('selectedSegmentation', activatedLabelmapIndex); updateState('selectedSegmentation', activatedLabelmapIndex);
}, },
@ -424,7 +437,9 @@ const SegmentationPanel = ({
<h3>Segmentations</h3> <h3>Segmentations</h3>
<div className="segmentations"> <div className="segmentations">
<SegmentationSelect <SegmentationSelect
value={state.labelmapList.find(i => i.value === state.selectedSegmentation)} value={state.labelmapList.find(
i => i.value === state.selectedSegmentation
)}
formatOptionLabel={SegmentationItem} formatOptionLabel={SegmentationItem}
options={state.labelmapList} options={state.labelmapList}
/> />
@ -509,7 +524,8 @@ const _setActiveLabelmap = async (
studies, studies,
displaySet, displaySet,
firstImageId, firstImageId,
activeLabelmapIndex activeLabelmapIndex,
UINotificationService
) => { ) => {
if (displaySet.labelmapIndex === activeLabelmapIndex) { if (displaySet.labelmapIndex === activeLabelmapIndex) {
log.warn(`${activeLabelmapIndex} is already the active labelmap`); log.warn(`${activeLabelmapIndex} is already the active labelmap`);
@ -519,7 +535,22 @@ const _setActiveLabelmap = async (
if (!displaySet.isLoaded) { if (!displaySet.isLoaded) {
// What props does this expect `viewportSpecificData` to have? // What props does this expect `viewportSpecificData` to have?
// TODO: Should this return the `labelmapIndex`? // TODO: Should this return the `labelmapIndex`?
await displaySet.load(viewportSpecificData, studies);
const loadPromise = displaySet.load(viewportSpecificData, studies);
loadPromise.catch(error => {
UINotificationService.show({
title: 'DICOM Segmentation Loader',
message: error.message,
type: 'error',
autoClose: false,
});
// Return old index.
return activeLabelmapIndex;
});
await loadPromise;
} }
const { state } = cornerstoneTools.getModule('segmentation'); const { state } = cornerstoneTools.getModule('segmentation');

View File

@ -0,0 +1,64 @@
import { MODULE_TYPES, utils } from '@ohif/core';
import loadSegmentation from './loadSegmentation';
// TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = {
DICOM_SEG: '1.2.840.10008.5.1.4.1.1.66.4',
};
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
export default function getSopClassHandlerModule({ servicesManager }) {
// TODO: Handle the case where there is more than one SOP Class Handler for the
// same SOP Class.
return {
id: 'OHIFDicomSegSopClassHandler',
type: MODULE_TYPES.SOP_CLASS_HANDLER,
sopClassUIDs,
getDisplaySetFromSeries: function(
series,
study,
dicomWebClient,
authorizationHeaders
) {
const instance = series.getFirstInstance();
const metadata = instance.getData().metadata;
const {
SeriesDate,
SeriesTime,
SeriesDescription,
FrameOfReferenceUID,
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = metadata;
const segDisplaySet = {
Modality: 'SEG',
displaySetInstanceUID: utils.guid(),
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
FrameOfReferenceUID,
authorizationHeaders,
metadata,
isDerived: true,
referencedDisplaySetUID: null, // Assigned when loaded.
labelmapIndex: null, // Assigned when loaded.
isLoaded: false,
SeriesDate,
SeriesTime,
SeriesDescription,
};
segDisplaySet.load = function(referencedDisplaySet, studies) {
return loadSegmentation(segDisplaySet, referencedDisplaySet, studies);
};
return segDisplaySet;
},
};
}

View File

@ -2,7 +2,7 @@ import React from 'react';
import init from './init.js'; import init from './init.js';
import toolbarModule from './toolbarModule.js'; import toolbarModule from './toolbarModule.js';
import sopClassHandlerModule from './OHIFDicomSegSopClassHandler.js'; import getSopClassHandlerModule from './getOHIFDicomSegSopClassHandler.js';
import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js'; import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js';
export default { export default {
@ -23,14 +23,20 @@ export default {
getToolbarModule({ servicesManager }) { getToolbarModule({ servicesManager }) {
return toolbarModule; return toolbarModule;
}, },
getPanelModule({ commandsManager }) { getPanelModule({ commandsManager, servicesManager }) {
const ExtendedSegmentationPanel = props => { const ExtendedSegmentationPanel = props => {
const segItemClickHandler = segData => { const segItemClickHandler = segData => {
commandsManager.runCommand('jumpToImage', segData); commandsManager.runCommand('jumpToImage', segData);
}; };
const { UINotificationService } = servicesManager.services;
return ( return (
<SegmentationPanel {...props} onSegItemClick={segItemClickHandler} /> <SegmentationPanel
{...props}
onSegItemClick={segItemClickHandler}
UINotificationService={UINotificationService}
/>
); );
}; };
@ -72,7 +78,5 @@ export default {
defaultContext: ['VIEWER'], defaultContext: ['VIEWER'],
}; };
}, },
getSopClassHandlerModule({ servicesManager }) { getSopClassHandlerModule,
return sopClassHandlerModule;
},
}; };

View File

@ -34,28 +34,35 @@ export default async function loadSegmentation(
referencedDisplaySet.SeriesInstanceUID referencedDisplaySet.SeriesInstanceUID
); );
const results = _parseSeg(segArrayBuffer, imageIds); return new Promise((resolve, reject) => {
let results;
if (!results) { try {
throw new Error('Fractional segmentations are not yet supported'); results = _parseSeg(segArrayBuffer, imageIds);
} } catch (error) {
segDisplaySet.isLoaded = false;
reject(error);
}
const { labelmapBuffer, segMetadata, segmentsOnFrame } = results; const { labelmapBuffer, segMetadata, segmentsOnFrame } = results;
const { setters } = cornerstoneTools.getModule('segmentation'); const { setters } = cornerstoneTools.getModule('segmentation');
// TODO: Could define a color LUT based on colors in the SEG. // TODO: Could define a color LUT based on colors in the SEG.
const labelmapIndex = _getNextLabelmapIndex(imageIds[0]); const labelmapIndex = _getNextLabelmapIndex(imageIds[0]);
setters.labelmap3DByFirstImageId( setters.labelmap3DByFirstImageId(
imageIds[0], imageIds[0],
labelmapBuffer, labelmapBuffer,
labelmapIndex, labelmapIndex,
segMetadata, segMetadata,
imageIds.length, imageIds.length,
segmentsOnFrame segmentsOnFrame
); );
segDisplaySet.labelmapIndex = labelmapIndex; segDisplaySet.labelmapIndex = labelmapIndex;
resolve(labelmapIndex);
});
} }
function _getNextLabelmapIndex(firstImageId) { function _getNextLabelmapIndex(firstImageId) {

View File

@ -50,10 +50,12 @@ import studyMetadataManager from './studyMetadataManager';
const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => { const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet; const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
const promises = [];
const studyMetadata = studyMetadataManager.get(StudyInstanceUID); const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
if (!studyMetadata) { if (!studyMetadata) {
return; return promises;
} }
const derivedDisplaySets = studyMetadata.getDerivedDatasets({ const derivedDisplaySets = studyMetadata.getDerivedDatasets({
@ -61,7 +63,7 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
}); });
if (!derivedDisplaySets.length) { if (!derivedDisplaySets.length) {
return; return promises;
} }
// Filter by type // Filter by type
@ -100,8 +102,10 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
} }
}); });
recentDisplaySet.load(referencedDisplaySet, studies); promises.push(recentDisplaySet.load(referencedDisplaySet, studies));
}); });
return promises;
}; };
export default loadAndCacheDerivedDisplaySets; export default loadAndCacheDerivedDisplaySets;

View File

@ -127,7 +127,7 @@
.sb-message { .sb-message {
font-size: 14px; font-size: 14px;
word-break: break-all; word-break: normal;
} }
.sb-item { .sb-item {

View File

@ -4,6 +4,7 @@ import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { utils } from '@ohif/core'; import { utils } from '@ohif/core';
import { useSnackbarContext } from '@ohif/ui';
// //
import ViewportPane from './ViewportPane.js'; import ViewportPane from './ViewportPane.js';
import DefaultViewport from './DefaultViewport.js'; import DefaultViewport from './DefaultViewport.js';
@ -11,7 +12,7 @@ import EmptyViewport from './EmptyViewport.js';
const { loadAndCacheDerivedDisplaySets } = utils; const { loadAndCacheDerivedDisplaySets } = utils;
const ViewportGrid = function (props) { const ViewportGrid = function(props) {
const { const {
activeViewportIndex, activeViewportIndex,
availablePlugins, availablePlugins,
@ -33,9 +34,22 @@ const ViewportGrid = function (props) {
return null; return null;
} }
const snackbar = useSnackbarContext();
useEffect(() => { useEffect(() => {
viewportData.forEach(displaySet => { viewportData.forEach(displaySet => {
loadAndCacheDerivedDisplaySets(displaySet, studies); const promises = loadAndCacheDerivedDisplaySets(displaySet, studies);
promises.forEach(promise => {
promise.catch(error => {
snackbar.show({
title: 'Error loading derived display set:',
message: error.message,
type: 'error',
autoClose: false,
});
});
});
}); });
}, [studies, viewportData]); }, [studies, viewportData]);