feat: visualize overlapping segments in cornerstone (#2185)

* feat: visualize overlapping segments

load multiple labelMaps (splitted by dcmjs because overlapping) in a segmentations [IDC2164]

Dynamically update active labelmap [IDC2164]

Co-authored-by: Davide Punzo <punzodavide@hotmail.it>
Co-authored-by: Igor Octaviano <igoroctaviano@gmail.com>
This commit is contained in:
Davide Punzo 2020-12-10 11:04:27 +01:00 committed by GitHub
parent a7a5a359dc
commit 29fceacee9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 182 additions and 136 deletions

View File

@ -36,7 +36,7 @@
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.20.1", "cornerstone-tools": "^4.20.1",
"cornerstone-wado-image-loader": "^3.1.0", "cornerstone-wado-image-loader": "^3.1.0",
"dcmjs": "0.16.5", "dcmjs": "0.17.0",
"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.16.5", "dcmjs": "0.17.0",
"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.20.1", "cornerstone-tools": "^4.20.1",
"dcmjs": "0.16.5", "dcmjs": "0.17.0",
"gl-matrix": "^3.3.0", "gl-matrix": "^3.3.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.8.6", "react": "^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.20.1", "cornerstone-tools": "^4.20.1",
"dcmjs": "0.16.5", "dcmjs": "0.17.0",
"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

@ -159,12 +159,12 @@ const SegmentationPanel = ({
const labelmapList = getLabelmapList( const labelmapList = getLabelmapList(
brushStackState, brushStackState,
firstImageId, firstImageId,
activeViewport activeViewport,
); );
const segmentList = getSegmentList( const segmentList = getSegmentList(
labelmap3D, labelmap3D,
firstImageId, firstImageId,
brushStackState brushStackState,
); );
setState(state => ({ setState(state => ({
...state, ...state,
@ -292,10 +292,13 @@ const SegmentationPanel = ({
const sameSegment = state.selectedSegment === segmentNumber; const sameSegment = state.selectedSegment === segmentNumber;
const setCurrentSelectedSegment = () => { const setCurrentSelectedSegment = () => {
const activeViewport = viewports[activeIndex];
_setActiveSegment( _setActiveSegment(
firstImageId, firstImageId,
segmentNumber, segmentNumber,
labelmap3D.activeSegmentIndex labelmap3D.activeSegmentIndex,
activeViewport
); );
updateState('selectedSegment', sameSegment ? null : segmentNumber); updateState('selectedSegment', sameSegment ? null : segmentNumber);
@ -640,7 +643,7 @@ const _getReferencedSegDisplaysets = (StudyInstanceUID, SeriesInstanceUID) => {
* @param {*} activeSegmentIndex * @param {*} activeSegmentIndex
* @returns * @returns
*/ */
const _setActiveSegment = (firstImageId, segmentIndex, activeSegmentIndex) => { const _setActiveSegment = (firstImageId, segmentIndex, activeSegmentIndex, activeViewport) => {
if (segmentIndex === activeSegmentIndex) { if (segmentIndex === activeSegmentIndex) {
log.info(`${activeSegmentIndex} is already the active segment`); log.info(`${activeSegmentIndex} is already the active segment`);
return; return;
@ -653,8 +656,27 @@ const _setActiveSegment = (firstImageId, segmentIndex, activeSegmentIndex) => {
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex]; brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
labelmap3D.activeSegmentIndex = segmentIndex; labelmap3D.activeSegmentIndex = segmentIndex;
refreshViewports(); /**
* Activates the correct label map if clicked segment
* does not belong to the active labelmap
*/
const { StudyInstanceUID } = activeViewport;
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
const allDisplaySets = studyMetadata.getDisplaySets();
let newLabelmapIndex = brushStackState.activeLabelmapIndex;
allDisplaySets.forEach(displaySet => {
if (displaySet.labelmapSegments) {
Object.keys(displaySet.labelmapSegments).forEach(labelmapIndex => {
if (displaySet.labelmapSegments[labelmapIndex].includes(segmentIndex)) {
newLabelmapIndex = labelmapIndex;
}
});
}
});
brushStackState.activeLabelmapIndex = newLabelmapIndex;
refreshViewports();
return segmentIndex; return segmentIndex;
}; };

View File

@ -1,6 +1,11 @@
import { MODULE_TYPES, utils } from '@ohif/core'; import { MODULE_TYPES, utils } from '@ohif/core';
import loadSegmentation from './loadSegmentation'; import loadSegmentation from './loadSegmentation';
import getSourceDisplaySet from './getSourceDisplaySet'; import getSourceDisplaySet from './getSourceDisplaySet';
import OHIF from '@ohif/core';
import dcmjs from 'dcmjs';
const { DicomLoaderService } = OHIF.utils;
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
// TODO: Should probably use dcmjs for this // TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = { const SOP_CLASS_UIDS = {
@ -46,7 +51,6 @@ export default function getSopClassHandlerModule({ servicesManager }) {
StudyInstanceUID, StudyInstanceUID,
FrameOfReferenceUID, FrameOfReferenceUID,
authorizationHeaders, authorizationHeaders,
metadata,
isDerived: true, isDerived: true,
referencedDisplaySetUID: null, // Assigned when loaded. referencedDisplaySetUID: null, // Assigned when loaded.
labelmapIndex: null, // Assigned when loaded. labelmapIndex: null, // Assigned when loaded.
@ -62,11 +66,81 @@ export default function getSopClassHandlerModule({ servicesManager }) {
return getSourceDisplaySet(studies, segDisplaySet); return getSourceDisplaySet(studies, segDisplaySet);
}; };
segDisplaySet.load = function(referencedDisplaySet, studies) { segDisplaySet.load = async function (referencedDisplaySet, studies) {
return loadSegmentation(segDisplaySet, referencedDisplaySet, studies); segDisplaySet.isLoaded = true;
const { StudyInstanceUID } = referencedDisplaySet;
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
segDisplaySet,
studies
);
const dicomData = DicomMessage.readFile(segArrayBuffer);
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
const imageIds = _getImageIdsForDisplaySet(
studies,
StudyInstanceUID,
referencedDisplaySet.SeriesInstanceUID
);
return new Promise((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;
if (labelmapBufferArray.length > 1) {
for (let i = 0; i < labelmapBufferArray.length; ++i) {
loadSegmentation(imageIds, segDisplaySet, labelmapBufferArray[i], segMetadata, segmentsOnFrame, segmentsOnFrameArray[i]);
}
} else {
loadSegmentation(imageIds, segDisplaySet, labelmapBufferArray[0], segMetadata, segmentsOnFrame, []);
}
segDisplaySet.labelmapIndex = 0;
resolve();
});
}; };
return segDisplaySet; return segDisplaySet;
}, },
}; };
} }
function _parseSeg(arrayBuffer, imageIds) {
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
imageIds,
arrayBuffer,
cornerstone.metaData
);
}
function _getImageIdsForDisplaySet(
studies,
StudyInstanceUID,
SeriesInstanceUID
) {
const study = studies.find(
study => study.StudyInstanceUID === StudyInstanceUID
);
const displaySets = study.displaySets.filter(displaySet => {
return displaySet.SeriesInstanceUID === SeriesInstanceUID;
});
if (displaySets.length > 1) {
console.warn(
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
);
// TODO -> We could make check the instance list and see if any match?
// Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
// ^ Will that even happen?
}
const referencedDisplaySet = displaySets[0];
return referencedDisplaySet.images.map(image => image.getImageId());
}

View File

@ -1,51 +1,15 @@
import OHIF from '@ohif/core';
import dcmjs from 'dcmjs'; import dcmjs from 'dcmjs';
import cornerstone from 'cornerstone-core'; import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools'; import cornerstoneTools from 'cornerstone-tools';
const { DicomLoaderService } = OHIF.utils;
export default async function loadSegmentation( export default async function loadSegmentation(
imageIds,
segDisplaySet, segDisplaySet,
referencedDisplaySet, labelmapBuffer,
studies segMetadata,
segmentsOnFrame,
labelmapSegments
) { ) {
const { StudyInstanceUID } = referencedDisplaySet;
// Set here is loading is asynchronous.
// If this function throws its set back to false.
segDisplaySet.isLoaded = true;
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
segDisplaySet,
studies
);
const dicomData = dcmjs.data.DicomMessage.readFile(segArrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
const imageIds = _getImageIdsForDisplaySet(
studies,
StudyInstanceUID,
referencedDisplaySet.SeriesInstanceUID
);
return new Promise((resolve, reject) => {
let results;
try {
results = _parseSeg(segArrayBuffer, imageIds);
} catch (error) {
segDisplaySet.isLoaded = false;
segDisplaySet.loadError = true;
reject(error);
}
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.
@ -62,6 +26,15 @@ export default async function loadSegmentation(
colorLUTIndex colorLUTIndex
); );
if (!segDisplaySet.labelmapSegments) {
segDisplaySet.labelmapSegments = {};
}
/**
* Cache each labelmap segments.
* This data is used to determine the active label map when a given segment is activated/clicked.
*/
segDisplaySet.labelmapSegments[labelmapIndex] = labelmapSegments.length ? Array.from(new Set(labelmapSegments.filter(a => !!a).reduce((a, b) => a.concat(b)))) : [];
segDisplaySet.labelmapIndex = labelmapIndex; segDisplaySet.labelmapIndex = labelmapIndex;
/* /*
@ -84,9 +57,6 @@ export default async function loadSegmentation(
console.log('Segmentation loaded.'); console.log('Segmentation loaded.');
const event = new CustomEvent('extensiondicomsegmentationsegloaded'); const event = new CustomEvent('extensiondicomsegmentationsegloaded');
document.dispatchEvent(event); document.dispatchEvent(event);
resolve(labelmapIndex);
});
} }
function _getNextLabelmapIndex(firstImageId) { function _getNextLabelmapIndex(firstImageId) {
@ -172,38 +142,3 @@ function _getNextColorLUTIndex() {
return colorLUTIndex; return colorLUTIndex;
} }
function _parseSeg(arrayBuffer, imageIds) {
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
imageIds,
arrayBuffer,
cornerstone.metaData
);
}
function _getImageIdsForDisplaySet(
studies,
StudyInstanceUID,
SeriesInstanceUID
) {
const study = studies.find(
study => study.StudyInstanceUID === StudyInstanceUID
);
const displaySets = study.displaySets.filter(displaySet => {
return displaySet.SeriesInstanceUID === SeriesInstanceUID;
});
if (displaySets.length > 1) {
console.warn(
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
);
// TODO -> We could make check the instance list and see if any match?
// Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
// ^ Will that even happen?
}
const referencedDisplaySet = displaySets[0];
return referencedDisplaySet.images.map(image => image.getImageId());
}

View File

@ -28,7 +28,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^2.6.0", "@ohif/core": "^2.6.0",
"dcmjs": "0.16.5", "dcmjs": "0.17.0",
"react": "^16.8.6" "react": "^16.8.6"
}, },
"dependencies": { "dependencies": {

View File

@ -35,7 +35,7 @@
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.20.1", "cornerstone-tools": "^4.20.1",
"cornerstone-wado-image-loader": "^3.1.0", "cornerstone-wado-image-loader": "^3.1.0",
"dcmjs": "0.16.5", "dcmjs": "0.17.0",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"i18next": "^17.0.3", "i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "^3.0.1",

View File

@ -159,6 +159,15 @@ class OHIFVTKViewport extends Component {
const { activeLabelmapIndex } = brushStackState; const { activeLabelmapIndex } = brushStackState;
const labelmap3D = brushStackState.labelmaps3D[activeLabelmapIndex]; const labelmap3D = brushStackState.labelmaps3D[activeLabelmapIndex];
if (brushStackState.labelmaps3D.length > 1 && this.props.viewportIndex === 0) {
const { UINotificationService } = this.props.servicesManager.services;
UINotificationService.show({
title: 'Overlapping Segmentation Found',
message: 'Overlapping segmentations cannot be displayed when in MPR mode',
type: 'info',
});
}
this.segmentsDefaultProperties = labelmap3D.segmentsHidden.map( this.segmentsDefaultProperties = labelmap3D.segmentsHidden.map(
isHidden => { isHidden => {
return { visible: !isHidden }; return { visible: !isHidden };

View File

@ -39,7 +39,7 @@
"dependencies": { "dependencies": {
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
"ajv": "^6.10.0", "ajv": "^6.10.0",
"dcmjs": "0.16.5", "dcmjs": "0.17.0",
"dicomweb-client": "^0.6.0", "dicomweb-client": "^0.6.0",
"immer": "6.0.2", "immer": "6.0.2",
"isomorphic-base64": "^1.0.2", "isomorphic-base64": "^1.0.2",

View File

@ -37,6 +37,10 @@ const SnackbarProvider = ({ children, service }) => {
return null; return null;
} }
if (options.type === 'error') {
console.error(options.error);
}
const newItem = { const newItem = {
...DEFAULT_OPTIONS, ...DEFAULT_OPTIONS,
...options, ...options,

View File

@ -68,7 +68,7 @@
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.20.1", "cornerstone-tools": "^4.20.1",
"cornerstone-wado-image-loader": "^3.1.0", "cornerstone-wado-image-loader": "^3.1.0",
"dcmjs": "0.16.5", "dcmjs": "0.17.0",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"dicomweb-client": "^0.4.4", "dicomweb-client": "^0.4.4",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",

View File

@ -48,6 +48,7 @@ const ViewportGrid = function(props) {
title: 'Error loading derived display set:', title: 'Error loading derived display set:',
message: error.message, message: error.message,
type: 'error', type: 'error',
error,
autoClose: false, autoClose: false,
}); });
}); });

View File

@ -6619,14 +6619,15 @@ 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.16.5: dcmjs@0.17.0:
version "0.16.5" version "0.17.0"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.16.5.tgz#05aa0c8d539a4d5777f996b9b796932d1f848993" resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.17.0.tgz#dbfd6db3efd2936f9bead9c902b7db396f64e9f5"
integrity sha512-Y92KAx4JSfev79xjYhaukQZjWgyJlaHnXSTdbtkGkJAHERjIakgHcCIPmACuVmtW/nHViTu04zIGAB0pmZ4GaA== integrity sha512-7JnNqNWXqjGyspLRwKM4nn+TU/+w3iMl9boU4eHrr831M4crMWWIiSQDejpOnfDT9+opqyeaYcNkiekr7p6TFg==
dependencies: dependencies:
"@babel/polyfill" "^7.8.3" "@babel/polyfill" "^7.8.3"
"@babel/runtime" "^7.8.4" "@babel/runtime" "^7.8.4"
gl-matrix "^3.1.0" gl-matrix "^3.1.0"
lodash.clonedeep "^4.5.0"
loglevelnext "^3.0.1" loglevelnext "^3.0.1"
ndarray "^1.0.19" ndarray "^1.0.19"