diff --git a/extensions/dicom-rt/src/components/PanelSection/PanelSection.css b/extensions/dicom-rt/src/components/PanelSection/PanelSection.css index 73380bd3d..b485b5318 100644 --- a/extensions/dicom-rt/src/components/PanelSection/PanelSection.css +++ b/extensions/dicom-rt/src/components/PanelSection/PanelSection.css @@ -39,6 +39,8 @@ border-radius: 5px; } -.icons .angle-double-down, .icons .angle-double-up { +.icons .angle-double-down, +.icons .angle-double-up { margin-left: 15px; + margin-right: -5px; } diff --git a/extensions/dicom-rt/src/components/RTPanel/RTPanel.css b/extensions/dicom-rt/src/components/RTPanel/RTPanel.css index 6855f7c58..8dbdd1cca 100644 --- a/extensions/dicom-rt/src/components/RTPanel/RTPanel.css +++ b/extensions/dicom-rt/src/components/RTPanel/RTPanel.css @@ -9,16 +9,21 @@ .dcmrt-panel .dcmrt-panel-header { display: flex; - flex-direction: row; + flex-direction: column; justify-content: space-between; color: white; } +.dcmrt-panel .dcmrt-panel-header h3 { + margin-top: 0; +} + .dcmrt-panel .dcmrt-panel-header .cog-icon { align-self: flex-end; color: var(--default-color); cursor: pointer; - margin-bottom: 20px; + min-height: 23px; + max-height: 23px; } .dcmrt-panel .dcmrt-panel-header .cog-icon:hover { diff --git a/extensions/dicom-rt/src/components/RTPanel/RTPanel.js b/extensions/dicom-rt/src/components/RTPanel/RTPanel.js index 5d9c9b98b..0b0144260 100644 --- a/extensions/dicom-rt/src/components/RTPanel/RTPanel.js +++ b/extensions/dicom-rt/src/components/RTPanel/RTPanel.js @@ -44,7 +44,7 @@ const RTPanel = ({ activeContexts = [], contexts = {}, activeViewport, - getActiveViewport + getActiveViewport, }) => { const isVTK = () => activeContexts.includes(contexts.VTK); const isCornerstone = () => activeContexts.includes(contexts.CORNERSTONE); @@ -55,7 +55,7 @@ const RTPanel = ({ referencedDisplaysets: [], sets: [], selectedSet: null, - isLocked: false + isLocked: false, }; const [state, setState] = useState(DEFAULT_STATE); @@ -84,12 +84,17 @@ const RTPanel = ({ document.addEventListener('extensiondicomrtrtloaded', updateStructureSets); return () => { - document.removeEventListener('extensiondicomrtrtloaded', updateStructureSets); + document.removeEventListener( + 'extensiondicomrtrtloaded', + updateStructureSets + ); }; }, []); useEffect(() => { - const studyMetadata = studyMetadataManager.get(activeViewport.StudyInstanceUID); + const studyMetadata = studyMetadataManager.get( + activeViewport.StudyInstanceUID + ); const referencedDisplaysets = studyMetadata.getDerivedDatasets({ referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID, Modality: 'RTSTRUCT', @@ -235,7 +240,7 @@ const RTPanel = ({ return (
-

RT Structure Sets

+ {' '} {!state.isLocked && ( setShowSettings(true)} /> )} +

RT Structure Sets

- {!state.isLocked && !state.referencedDisplaysets.length && } - {state.sets && state.referencedDisplaysets.map(displaySet => { - const { SeriesInstanceUID, metadata, isLoaded } = displaySet; + {!state.isLocked && !state.referencedDisplaysets.length && ( + + )} + {state.sets && + state.referencedDisplaysets.map(displaySet => { + const { SeriesInstanceUID, metadata, isLoaded } = displaySet; - const module = cornerstoneTools.getModule('rtstruct'); - const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid( - activeViewport.SeriesInstanceUID - ); + const module = cornerstoneTools.getModule('rtstruct'); + const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid( + activeViewport.SeriesInstanceUID + ); - const loadedSet = sets.find( - set => set.SeriesInstanceUID === SeriesInstanceUID - ); + const loadedSet = sets.find( + set => set.SeriesInstanceUID === SeriesInstanceUID + ); - if (!loadedSet) return null; + if (!loadedSet) return null; - return ( - { - const module = cornerstoneTools.getModule('rtstruct'); - - if (newVisibility) { - module.setters.showStructureSet(loadedSet.SeriesInstanceUID); - } else { - module.setters.hideStructureSet(loadedSet.SeriesInstanceUID); + return ( + ({ ...state, sets })); - refreshViewport(); - }} - onExpandChange={async () => { - if (!isLoaded) { - await displaySet.load(activeViewport, studies); + onVisibilityChange={newVisibility => { const module = cornerstoneTools.getModule('rtstruct'); + + if (newVisibility) { + module.setters.showStructureSet(loadedSet.SeriesInstanceUID); + } else { + module.setters.hideStructureSet(loadedSet.SeriesInstanceUID); + } const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid( activeViewport.SeriesInstanceUID ); - const selectedSet = sets.find( - set => set.SeriesInstanceUID === SeriesInstanceUID - ); - setState(state => ({ ...state, selectedSet, sets })); - } - }} - > - - - {isLoaded && - loadedSet.ROIContours.map(c => toContourItem(c, loadedSet))} - - - - ); - })} + setState(state => ({ ...state, sets })); + refreshViewport(); + }} + onExpandChange={async () => { + if (!isLoaded) { + await displaySet.load(activeViewport, studies); + const module = cornerstoneTools.getModule('rtstruct'); + const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid( + activeViewport.SeriesInstanceUID + ); + const selectedSet = sets.find( + set => set.SeriesInstanceUID === SeriesInstanceUID + ); + setState(state => ({ ...state, selectedSet, sets })); + } + }} + > + + + {isLoaded && + loadedSet.ROIContours.map(c => toContourItem(c, loadedSet))} + + + + ); + })}
); }; diff --git a/extensions/dicom-segmentation/src/components/SegmentItem/SegmentItem.js b/extensions/dicom-segmentation/src/components/SegmentItem/SegmentItem.js index 84c7860a5..677b10da9 100644 --- a/extensions/dicom-segmentation/src/components/SegmentItem/SegmentItem.js +++ b/extensions/dicom-segmentation/src/components/SegmentItem/SegmentItem.js @@ -24,7 +24,7 @@ const SegmentItem = ({ onClick, itemClass, color, - visible = true, + visible, onVisibilityChange, }) => { const [isVisible, setIsVisible] = useState(visible); @@ -33,6 +33,15 @@ const SegmentItem = ({ setIsVisible(visible); }, [visible]); + const onClickHandler = () => onClick(index); + + const onVisibilityChangeHandler = event => { + event.stopPropagation(); + const newVisibility = !isVisible; + setIsVisible(newVisibility); + onVisibilityChange(newVisibility, index); + }; + return (
} itemMetaClass="segment-color-section" - onItemClick={onClick} + onItemClick={onClickHandler} >
@@ -63,12 +72,7 @@ const SegmentItem = ({ name={isVisible ? 'eye' : 'eye-closed'} width="20px" height="20px" - onClick={event => { - event.stopPropagation(); - const newVisibility = !isVisible; - setIsVisible(newVisibility); - onVisibilityChange(newVisibility); - }} + onClick={onVisibilityChangeHandler} />
{false &&
{'...'}
} diff --git a/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.css b/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.css index d22eafa19..6f22c392d 100644 --- a/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.css +++ b/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.css @@ -6,6 +6,7 @@ width: 100%; display: flex; flex-direction: column; + padding: 20px; } .dcmseg-segmentation-panel.disabled { @@ -14,19 +15,19 @@ } .dcmseg-segmentation-panel h3 { - padding-left: 20px; margin-top: 0; } .dcmseg-segmentation-panel .segmentations { - padding: 0 20px 20px 20px; + padding-bottom: 10px; } .dcmseg-segmentation-panel .cog-icon { - margin: 20px 20px 0 0; align-self: flex-end; color: var(--default-color); cursor: pointer; + min-height: 23px; + max-height: 23px; } .dcmseg-segmentation-panel .cog-icon:hover { @@ -48,11 +49,11 @@ height: 150px; } -.dcmseg-segmentation-panel .selector-form>div { +.dcmseg-segmentation-panel .selector-form > div { width: 100%; } -.dcmseg-segmentation-panel .selector-form>div:first-of-type { +.dcmseg-segmentation-panel .selector-form > div:first-of-type { display: flex; flex-direction: row; justify-content: center; @@ -64,3 +65,61 @@ flex: unset; margin-right: 16px; } + +.SegmentsSection { + display: flex; + flex-direction: column; + color: var(--text-primary-color); + font-size: 12px; + margin-bottom: 0px; + height: 100%; +} + +.SegmentsSection .header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + background-color: var(--ui-gray-dark); + border-radius: 5px; + font-weight: bold; + padding: 10px 15px; +} + +.SegmentsSection .count { + font-size: 15px; + color: var(--active-color); + margin-left: 15px; + border-radius: 5px; + background-color: var(--ui-gray-darker); + text-align: center; + min-width: 25px; +} + +.SegmentsSection .header .eye-icon, +.SegmentsSection .header .angle-double-down, +.SegmentsSection .header .angle-double-up { + cursor: pointer; + color: var(--active-color); +} + +.SegmentsSection .header .eye-icon:hover, +.SegmentsSection .header .angle-double-down:hover, +.SegmentsSection .header .angle-double-up:hover { + color: var(--hover-color); +} + +.SegmentsSection .header .eye-icon.expanded { + color: var(--default-color); +} + +.SegmentsSection .header .icons { + display: flex; + align-items: center; + border-radius: 5px; +} + +.SegmentsSection .header .icons .angle-double-down, +.SegmentsSection .header .icons .angle-double-up { + margin-left: 15px; +} diff --git a/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js b/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js index 1cd292442..8bd61e3b7 100644 --- a/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js +++ b/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js @@ -1,8 +1,9 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import cornerstoneTools from 'cornerstone-tools'; import cornerstone from 'cornerstone-core'; import moment from 'moment'; +import classNames from 'classnames'; import { utils, log } from '@ohif/core'; import { ScrollableArea, TableList, Icon } from '@ohif/ui'; import DICOMSegTempCrosshairsTool from '../../tools/DICOMSegTempCrosshairsTool'; @@ -67,29 +68,104 @@ const SegmentationPanel = ({ const [state, setState] = useState({ brushRadius: DEFAULT_BRUSH_RADIUS, brushColor: 'rgba(221, 85, 85, 1)', - selectedSegment: null, - selectedSegmentation: null, - showSegmentationSettings: false, - brushStackState: null, - labelmapList: [], + selectedSegment: 0, + selectedSegmentation: 0, + showSettings: false, + labelMapList: [], segmentList: [], - cachedSegmentsProperties: [], + segmentsHidden: [], + segmentNumbers: [], isLoading: false, isDisabled: true, }); + const getActiveViewport = () => viewports[activeIndex]; + + const getFirstImageId = () => { + const { StudyInstanceUID, displaySetInstanceUID } = getActiveViewport(); + const studyMetadata = studyMetadataManager.get(StudyInstanceUID); + return studyMetadata.getFirstImageId(displaySetInstanceUID); + }; + + const getActiveLabelMaps3D = () => { + const { labelmaps3D, activeLabelmapIndex } = getBrushStackState(); + return labelmaps3D[activeLabelmapIndex]; + }; + + const getActiveLabelMapIndex = () => { + const { activeLabelmapIndex } = getBrushStackState(); + return activeLabelmapIndex; + }; + + const getActiveSegmentIndex = () => { + const { activeSegmentIndex } = getActiveLabelMaps3D(); + return activeSegmentIndex; + }; + + const getActiveLabelMaps2D = () => { + const { labelmaps2D } = getActiveLabelMaps3D(); + return labelmaps2D; + }; + + const getCurrentDisplaySet = () => { + const { StudyInstanceUID, displaySetInstanceUID } = getActiveViewport(); + const studyMetadata = studyMetadataManager.get(StudyInstanceUID); + const allDisplaySets = studyMetadata.getDisplaySets(); + return allDisplaySets.find( + ds => ds.displaySetInstanceUID === displaySetInstanceUID + ); + }; + + const setActiveSegment = segmentIndex => { + const activeSegmentIndex = getActiveSegmentIndex(); + const activeViewport = getActiveViewport(); + + if (segmentIndex === activeSegmentIndex) { + log.info(`${activeSegmentIndex} is already the active segment`); + return; + } + + const labelmap3D = getActiveLabelMaps3D(); + labelmap3D.activeSegmentIndex = segmentIndex; + + /** + * 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 selectedSegmentation; + let newLabelmapIndex = getActiveLabelMapIndex(); + allDisplaySets.forEach(displaySet => { + if (displaySet.labelmapSegments) { + Object.keys(displaySet.labelmapSegments).forEach(labelmapIndex => { + if ( + displaySet.labelmapSegments[labelmapIndex].includes(segmentIndex) + ) { + newLabelmapIndex = labelmapIndex; + selectedSegmentation = + displaySet.hasOverlapping === true + ? displaySet.originLabelMapIndex + : labelmapIndex; + } + }); + } + }); + + const brushStackState = getBrushStackState(); + brushStackState.activeLabelmapIndex = newLabelmapIndex; + setState(state => ({ ...state, selectedSegmentation })); + + refreshViewports(); + + return segmentIndex; + }; + useEffect(() => { const labelmapModifiedHandler = event => { log.warn('Segmentation Panel: labelmap modified', event); - const module = cornerstoneTools.getModule('segmentation'); - const activeViewport = viewports[activeIndex]; - const studyMetadata = studyMetadataManager.get( - activeViewport.StudyInstanceUID - ); - const firstImageId = studyMetadata.getFirstImageId( - activeViewport.displaySetInstanceUID - ); - updateState('brushStackState', module.state.series[firstImageId]); + refreshSegmentations(); }; /* @@ -140,50 +216,38 @@ const SegmentationPanel = ({ }; }, [activeIndex, viewports]); - const refreshSegmentations = useCallback(() => { - const module = cornerstoneTools.getModule('segmentation'); - const activeViewport = viewports[activeIndex]; - + const refreshSegmentations = () => { + const activeViewport = getActiveViewport(); const isDisabled = !activeViewport || !activeViewport.StudyInstanceUID; if (!isDisabled) { - const studyMetadata = studyMetadataManager.get( - activeViewport.StudyInstanceUID - ); - const firstImageId = studyMetadata.getFirstImageId( - activeViewport.displaySetInstanceUID - ); - const brushStackState = module.state.series[firstImageId]; + const brushStackState = getBrushStackState(); if (brushStackState) { - const labelmap3D = - brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex]; - const labelmapList = getLabelmapList( - brushStackState, - firstImageId, - activeViewport, - ); - const segmentList = getSegmentList( - labelmap3D, - firstImageId, - brushStackState, - ); + const labelMapList = getLabelMapList(); + const { + items: segmentList, + numbers: segmentNumbers, + segmentsHidden, + } = getSegmentList(); setState(state => ({ ...state, - brushStackState, - selectedSegmentation: brushStackState.activeLabelmapIndex, - labelmapList, + segmentsHidden, + segmentNumbers, + labelMapList, segmentList, isDisabled, })); } else { setState(state => ({ ...state, - labelmapList: [], + segmentsHidden: [], + segmentNumbers: [], + labelMapList: [], segmentList: [], isDisabled, })); } } - }, [viewports, activeIndex, state.isLoading]); + }; useEffect(() => { refreshSegmentations(); @@ -200,309 +264,285 @@ const SegmentationPanel = ({ useEffect(() => { setState(state => ({ ...state, - showSegmentationSettings: state.showSegmentationSettings && !isOpen, + showSettings: state.showSettings && !isOpen, })); }, [isOpen]); - const getLabelmapList = useCallback( - (brushStackState, firstImageId, activeViewport) => { - /* Get list of SEG labelmaps specific to active viewport (reference series) */ - const referencedSegDisplaysets = _getReferencedSegDisplaysets( - activeViewport.StudyInstanceUID, - activeViewport.SeriesInstanceUID - ); + const getLabelMapList = () => { + const activeViewport = getActiveViewport(); - return referencedSegDisplaysets.map((displaySet, index) => { - const { labelmapIndex, SeriesDate, SeriesTime } = displaySet; + /* Get list of SEG labelmaps specific to active viewport (reference series) */ + const referencedSegDisplaysets = _getReferencedSegDisplaysets( + activeViewport.StudyInstanceUID, + activeViewport.SeriesInstanceUID + ); - /* Map to display representation */ - const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0]; - const date = moment(dateStr, 'YYYYMMDD:HHmmss'); - const isActiveLabelmap = - labelmapIndex === brushStackState.activeLabelmapIndex; - const displayDate = date.format('ddd, MMM Do YYYY'); - const displayTime = date.format('h:mm:ss a'); - const displayDescription = displaySet.SeriesDescription; + return referencedSegDisplaysets.map((displaySet, index) => { + const { + labelmapIndex, + originLabelMapIndex, + hasOverlapping, + SeriesDate, + SeriesTime, + } = displaySet; - return { - value: labelmapIndex, - title: displayDescription, - description: displayDate, - onClick: async () => { - const activatedLabelmapIndex = await setActiveLabelmap( - activeViewport, - studies, - displaySet, - () => onSelectedSegmentationChange(), - onDisplaySetLoadFailure - ); - updateState('selectedSegmentation', activatedLabelmapIndex); - }, - }; - }); - }, - [studies] - ); + /* Map to display representation */ + const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0]; + const date = moment(dateStr, 'YYYYMMDD:HHmmss'); + const displayDate = date.format('ddd, MMM Do YYYY'); + const displayDescription = displaySet.SeriesDescription; - const getSegmentList = useCallback( - (labelmap3D, firstImageId, brushStackState) => { - /* - * Newly created segments have no `meta` - * So we instead build a list of all segment indexes in use - * Then find any associated metadata - */ - const uniqueSegmentIndexes = labelmap3D.labelmaps2D - .reduce((acc, labelmap2D) => { - if (labelmap2D) { - const segmentIndexes = labelmap2D.segmentsOnLabelmap; - - for (let i = 0; i < segmentIndexes.length; i++) { - if (!acc.includes(segmentIndexes[i]) && segmentIndexes[i] !== 0) { - acc.push(segmentIndexes[i]); - } - } - } - - return acc; - }, []) - .sort((a, b) => a - b); - - const module = cornerstoneTools.getModule('segmentation'); - const colorLutTable = - module.state.colorLutTables[labelmap3D.colorLUTIndex]; - const hasLabelmapMeta = labelmap3D.metadata && labelmap3D.metadata.data; - - const segmentList = []; - for (let i = 0; i < uniqueSegmentIndexes.length; i++) { - const segmentIndex = uniqueSegmentIndexes[i]; - - const color = colorLutTable[segmentIndex]; - let segmentLabel = '(unlabeled)'; - let segmentNumber = segmentIndex; - - /* Meta */ - if (hasLabelmapMeta) { - const segmentMeta = labelmap3D.metadata.data[segmentIndex]; - - if (segmentMeta) { - segmentNumber = segmentMeta.SegmentNumber; - segmentLabel = segmentMeta.SegmentLabel; - } - } - - const sameSegment = state.selectedSegment === segmentNumber; - const setCurrentSelectedSegment = () => { - const activeViewport = viewports[activeIndex]; - - _setActiveSegment( - firstImageId, - segmentNumber, - labelmap3D.activeSegmentIndex, - activeViewport + return { + value: hasOverlapping === true ? originLabelMapIndex : labelmapIndex, + title: displayDescription, + description: displayDate, + onClick: async () => { + const activatedLabelmapIndex = await setActiveLabelmap( + activeViewport, + studies, + displaySet, + onSelectedSegmentationChange, + onDisplaySetLoadFailure ); - updateState('selectedSegment', sameSegment ? null : segmentNumber); - - const validIndexList = []; - labelmap3D.labelmaps2D.forEach((labelMap2D, index) => { - if (labelMap2D.segmentsOnLabelmap.includes(segmentNumber)) { - validIndexList.push(index); - } - }); - const avg = array => array.reduce((a, b) => a + b) / array.length; - const average = avg(validIndexList); - const closest = validIndexList.reduce((prev, curr) => { - return Math.abs(curr - average) < Math.abs(prev - average) - ? curr - : prev; - }); - - if (isCornerstone()) { - const enabledElements = cornerstone.getEnabledElements(); - const element = enabledElements[activeIndex].element; - const toolState = cornerstoneTools.getToolState(element, 'stack'); - - if (!toolState) { - return; - } - - const imageIds = toolState.data[0].imageIds; - const imageId = imageIds[closest]; - const frameIndex = imageIds.indexOf(imageId); - - const SOPInstanceUID = cornerstone.metaData.get( - 'SOPInstanceUID', - imageId - ); - const StudyInstanceUID = cornerstone.metaData.get( - 'StudyInstanceUID', - imageId - ); - - DICOMSegTempCrosshairsTool.addCrosshair( - element, - imageId, - segmentNumber - ); - - onSegmentItemClick({ - StudyInstanceUID, - SOPInstanceUID, - frameIndex, - activeViewportIndex: activeIndex, - }); - } - - if (isVTK()) { - const activeViewport = viewports[activeIndex]; - const studyMetadata = studyMetadataManager.get( - activeViewport.StudyInstanceUID - ); - const allDisplaySets = studyMetadata.getDisplaySets(); - const currentDisplaySet = allDisplaySets.find( - displaySet => - displaySet.displaySetInstanceUID === - activeViewport.displaySetInstanceUID - ); - - const frame = labelmap3D.labelmaps2D[closest]; - - onSegmentItemClick({ - studies, - StudyInstanceUID: currentDisplaySet.StudyInstanceUID, - displaySetInstanceUID: currentDisplaySet.displaySetInstanceUID, - SOPClassUID: viewports[activeIndex].sopClassUIDs[0], - SOPInstanceUID: currentDisplaySet.SOPInstanceUID, - segmentNumber, - frameIndex: closest, - frame, - }); - } - }; - - const isSegmentVisible = () => { - return !labelmap3D.segmentsHidden[segmentIndex]; - }; - - const toggleSegmentVisibility = () => { - const segmentsHidden = labelmap3D.segmentsHidden; - segmentsHidden[segmentIndex] = !segmentsHidden[segmentIndex]; - return !segmentsHidden[segmentIndex]; - }; - - const cachedSegmentProperties = - state.cachedSegmentsProperties[segmentNumber]; - let visible = isSegmentVisible(); - if ( - cachedSegmentProperties && - cachedSegmentProperties.visible !== visible - ) { - toggleSegmentVisibility(); - } - - segmentList.push( - setCurrentSelectedSegment()} - label={segmentLabel} - index={segmentNumber} - color={color} - visible={visible} - onVisibilityChange={newVisibility => { - if (isCornerstone()) { - const enabledElements = cornerstone.getEnabledElements(); - const element = enabledElements[activeIndex].element; - module.setters.toggleSegmentVisibility( - element, - segmentNumber, - brushStackState.activeLabelmapIndex - ); - } - - if (isVTK()) { - onSegmentVisibilityChange(segmentNumber, newVisibility); - } - - updateCachedSegmentsProperties(segmentNumber, { - visible: newVisibility, - }); - refreshViewports(); - }} - /> - ); - } - - return segmentList; - - /* - * Let's iterate over segmentIndexes ^ above - * If meta has a match, use it to show info - * If now, add "no-meta" class - * Show default name - */ - }, - [activeIndex, onSegmentItemClick, state.selectedSegment, state.isLoading] - ); - - const updateCachedSegmentsProperties = (segmentNumber, properties) => { - const segmentsProperties = state.cachedSegmentsProperties; - const segmentProperties = state.cachedSegmentsProperties[segmentNumber]; - - segmentsProperties[segmentNumber] = segmentProperties - ? { ...segmentProperties, ...properties } - : properties; - - updateState('cachedSegmentsProperties', segmentsProperties); + setState(state => ({ + ...state, + selectedSegmentation: activatedLabelmapIndex, + })); + }, + }; + }); }; - useEffect(() => { - updateState('cachedSegmentsProperties', []); - }, [activeContexts]); + const setCurrentSelectedSegment = segmentNumber => { + setActiveSegment(segmentNumber); - const updateState = (field, value) => { - setState(state => ({ ...state, [field]: value })); + const sameSegment = state.selectedSegment === segmentNumber; + if (!sameSegment) { + setState(state => ({ ...state, selectedSegment: segmentNumber })); + } + + const validIndexList = []; + getActiveLabelMaps2D().forEach((labelMap2D, index) => { + if (labelMap2D.segmentsOnLabelmap.includes(segmentNumber)) { + validIndexList.push(index); + } + }); + + const avg = array => array.reduce((a, b) => a + b) / array.length; + const average = avg(validIndexList); + const closest = validIndexList.reduce((prev, curr) => { + return Math.abs(curr - average) < Math.abs(prev - average) ? curr : prev; + }); + + if (isCornerstone()) { + const element = getEnabledElement(); + const toolState = cornerstoneTools.getToolState(element, 'stack'); + + if (!toolState) return; + + const imageIds = toolState.data[0].imageIds; + const imageId = imageIds[closest]; + const frameIndex = imageIds.indexOf(imageId); + + const SOPInstanceUID = cornerstone.metaData.get( + 'SOPInstanceUID', + imageId + ); + const StudyInstanceUID = cornerstone.metaData.get( + 'StudyInstanceUID', + imageId + ); + + DICOMSegTempCrosshairsTool.addCrosshair(element, imageId, segmentNumber); + + onSegmentItemClick({ + StudyInstanceUID, + SOPInstanceUID, + frameIndex, + activeViewportIndex: activeIndex, + }); + } + + if (isVTK()) { + const labelMaps3D = getActiveLabelMaps3D(); + const currentDisplaySet = getCurrentDisplaySet(); + const frame = labelMaps3D.labelmaps2D[closest]; + + onSegmentItemClick({ + studies, + StudyInstanceUID: currentDisplaySet.StudyInstanceUID, + displaySetInstanceUID: currentDisplaySet.displaySetInstanceUID, + SOPClassUID: getActiveViewport().sopClassUIDs[0], + SOPInstanceUID: currentDisplaySet.SOPInstanceUID, + segmentNumber, + frameIndex: closest, + frame, + }); + } + }; + + const getColorLUTTable = () => { + const { state } = cornerstoneTools.getModule('segmentation'); + const { colorLUTIndex } = getActiveLabelMaps3D(); + return state.colorLutTables[colorLUTIndex]; + }; + + const getEnabledElement = () => { + const enabledElements = cornerstone.getEnabledElements(); + return enabledElements[activeIndex].element; + }; + + const onSegmentVisibilityChangeHandler = (isVisible, segmentNumber) => { + /** Get all labelmaps with this segmentNumber (overlapping segments) */ + const { labelmaps3D } = getBrushStackState(); + const possibleLabelMaps3D = labelmaps3D.filter(({ labelmaps2D }) => { + return labelmaps2D.some(({ segmentsOnLabelmap }) => + segmentsOnLabelmap.includes(segmentNumber) + ); + }); + + let segmentsHidden = []; + possibleLabelMaps3D.forEach(labelmap3D => { + labelmap3D.segmentsHidden[segmentNumber] = !isVisible; + + segmentsHidden = [ + ...new Set([...segmentsHidden, ...labelmap3D.segmentsHidden]), + ]; + }); + + setState(state => ({ ...state, segmentsHidden })); + + refreshSegmentations(); + refreshViewports(); + + if (isVTK()) { + onSegmentVisibilityChange(segmentNumber, isVisible); + } + }; + + const getSegmentList = () => { + /* + * Newly created segments have no `meta` + * So we instead build a list of all segment indexes in use + * Then find any associated metadata + */ + const uniqueSegmentIndexes = getActiveLabelMaps2D() + .reduce((acc, labelmap2D) => { + if (labelmap2D) { + const segmentIndexes = labelmap2D.segmentsOnLabelmap; + + for (let i = 0; i < segmentIndexes.length; i++) { + if (!acc.includes(segmentIndexes[i]) && segmentIndexes[i] !== 0) { + acc.push(segmentIndexes[i]); + } + } + } + + return acc; + }, []) + .sort((a, b) => a - b); + + const labelmap3D = getActiveLabelMaps3D(); + const colorLutTable = getColorLUTTable(); + const hasLabelmapMeta = labelmap3D.metadata && labelmap3D.metadata.data; + + const segmentList = []; + const segmentNumbers = []; + for (let i = 0; i < uniqueSegmentIndexes.length; i++) { + const segmentIndex = uniqueSegmentIndexes[i]; + + const color = colorLutTable[segmentIndex]; + let segmentLabel = '(unlabeled)'; + let segmentNumber = segmentIndex; + + /* Meta */ + if (hasLabelmapMeta) { + const segmentMeta = labelmap3D.metadata.data[segmentIndex]; + if (segmentMeta) { + segmentNumber = segmentMeta.SegmentNumber; + segmentLabel = segmentMeta.SegmentLabel; + } + } + + const sameSegment = state.selectedSegment === segmentNumber; + + segmentNumbers.push(segmentNumber); + segmentList.push( + + ); + } + + return { + items: segmentList, + numbers: segmentNumbers, + segmentsHidden: labelmap3D.segmentsHidden, + }; + + /* + * Let's iterate over segmentIndexes ^ above + * If meta has a match, use it to show info + * If now, add "no-meta" class + * Show default name + */ }; const updateBrushSize = evt => { const updatedRadius = Number(evt.target.value); if (updatedRadius !== brushRadius) { - updateState('brushRadius', updatedRadius); + setState(state => ({ ...state, brushRadius: updatedRadius })); const module = cornerstoneTools.getModule('segmentation'); module.setters.radius(updatedRadius); } }; const decrementSegment = event => { + const activeSegmentIndex = getActiveSegmentIndex(); event.preventDefault(); - if (labelmap3D.activeSegmentIndex > 1) { - labelmap3D.activeSegmentIndex--; + if (activeSegmentIndex > 1) { + activeSegmentIndex--; } - updateState('selectedSegment', labelmap3D.activeSegmentIndex); + setState(state => ({ ...state, selectedSegment: activeSegmentIndex })); updateActiveSegmentColor(); }; const incrementSegment = event => { + const activeSegmentIndex = getActiveSegmentIndex(); event.preventDefault(); - labelmap3D.activeSegmentIndex++; - updateState('selectedSegment', labelmap3D.activeSegmentIndex); + activeSegmentIndex++; + setState(state => ({ ...state, selectedSegment: activeSegmentIndex })); updateActiveSegmentColor(); }; const updateActiveSegmentColor = () => { const color = getActiveSegmentColor(); - updateState('brushColor', color); + setState(state => ({ ...state, brushColor: color })); + }; + + const getBrushStackState = () => { + const module = cornerstoneTools.getModule('segmentation'); + const firstImageId = getFirstImageId(); + const brushStackState = module.state.series[firstImageId]; + return brushStackState; }; const getActiveSegmentColor = () => { - if (!state.brushStackState) { + const brushStackState = getBrushStackState(); + if (!brushStackState) { return 'rgba(255, 255, 255, 1)'; } - const module = cornerstoneTools.getModule('segmentation'); - const colorLutTable = module.state.colorLutTables[labelmap3D.colorLUTIndex]; + const colorLutTable = getColorLUTTable(); const color = colorLutTable[labelmap3D.activeSegmentIndex]; - return `rgba(${color.join(',')})`; }; @@ -520,65 +560,103 @@ const SegmentationPanel = ({ refreshViewports(); }; + const onVisibilityChangeHandler = isVisible => { + let segmentsHidden = []; + state.segmentNumbers.forEach(segmentNumber => { + if (isVTK()) { + onSegmentVisibilityChange(segmentNumber, isVisible); + } + + /** Get all labelmaps with this segmentNumber (overlapping segments) */ + const { labelmaps3D } = getBrushStackState(); + const possibleLabelMaps3D = labelmaps3D.filter(({ labelmaps2D }) => { + return labelmaps2D.some(({ segmentsOnLabelmap }) => + segmentsOnLabelmap.includes(segmentNumber) + ); + }); + + possibleLabelMaps3D.forEach(labelmap3D => { + labelmap3D.segmentsHidden[segmentNumber] = !isVisible; + segmentsHidden = [ + ...new Set([...segmentsHidden, ...labelmap3D.segmentsHidden]), + ]; + }); + }); + + setState(state => ({ ...state, segmentsHidden })); + + refreshSegmentations(); + refreshViewports(); + }; + const disabledConfigurationFields = [ 'outlineAlpha', 'shouldRenderInactiveLabelmaps', ]; - if (state.showSegmentationSettings) { + + const selectedSegmentationOption = state.labelMapList.find( + i => i.value === state.selectedSegmentation + ); + + if (state.showSettings) { return ( updateState('showSegmentationSettings', false)} + onBack={() => setState(state => ({ ...state, showSettings: false }))} onChange={updateConfiguration} /> ); } else { return (
- updateState('showSegmentationSettings', true)} - /> {false && (
)} + setState(state => ({ ...state, showSettings: true }))} + />

Segmentations

i.value === state.selectedSegmentation - )} + value={selectedSegmentationOption} formatOptionLabel={SegmentationItem} - options={state.labelmapList} + options={state.labelMapList} />
- - } - > - {state.segmentList} - - + isHidden === true).length < + state.segmentNumbers.length + } + onVisibilityChange={onVisibilityChangeHandler} + > + + {state.segmentList} + +
); } @@ -613,7 +691,7 @@ SegmentationPanel.propTypes = { SegmentationPanel.defaultProps = {}; /** - * Returns SEG Displaysets that reference the target series, sorted by dateTime + * Returns SEG DisplaySets that reference the target series, sorted by dateTime * * @param {string} StudyInstanceUID * @param {string} SeriesInstanceUID @@ -637,56 +715,48 @@ const _getReferencedSegDisplaysets = (StudyInstanceUID, SeriesInstanceUID) => { return referencedDisplaysets; }; -/** - * - * @param {*} firstImageId - * @param {*} activeSegmentIndex - * @returns - */ -const _setActiveSegment = (firstImageId, segmentIndex, activeSegmentIndex, activeViewport) => { - if (segmentIndex === activeSegmentIndex) { - log.info(`${activeSegmentIndex} is already the active segment`); - return; - } +const SegmentsSection = ({ + count, + children, + isVisible: defaultVisibility, + onVisibilityChange, +}) => { + const [isVisible, setIsVisible] = useState(defaultVisibility); - const { state } = cornerstoneTools.getModule('segmentation'); - const brushStackState = state.series[firstImageId]; + const onVisibilityChangeHandler = () => { + const newVisibility = !isVisible; + setIsVisible(newVisibility); + onVisibilityChange(newVisibility); + }; - const labelmap3D = - brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex]; - labelmap3D.activeSegmentIndex = segmentIndex; + useEffect(() => { + setIsVisible(defaultVisibility); + }, [defaultVisibility]); - /** - * 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; -}; - -const SegmentsHeader = ({ count }) => { return ( - -
Segments
-
{count}
-
+
+
+
Segments
+
+ +
{count}
+
+
+ {children} +
); }; +const noop = () => {}; + +SegmentsSection.defaultProps = { + onVisibilityChange: noop, +}; + export default SegmentationPanel; diff --git a/extensions/dicom-segmentation/src/getOHIFDicomSegSopClassHandler.js b/extensions/dicom-segmentation/src/getOHIFDicomSegSopClassHandler.js index f21c080f4..4d9435893 100644 --- a/extensions/dicom-segmentation/src/getOHIFDicomSegSopClassHandler.js +++ b/extensions/dicom-segmentation/src/getOHIFDicomSegSopClassHandler.js @@ -21,7 +21,7 @@ export default function getSopClassHandlerModule({ servicesManager }) { id: 'OHIFDicomSegSopClassHandler', type: MODULE_TYPES.SOP_CLASS_HANDLER, sopClassUIDs, - getDisplaySetFromSeries: function ( + getDisplaySetFromSeries: function( series, study, dicomWebClient, @@ -55,6 +55,7 @@ export default function getSopClassHandlerModule({ servicesManager }) { referencedDisplaySetUID: null, // Assigned when loaded. labelmapIndex: null, // Assigned when loaded. isLoaded: false, + hasOverlapping: false, SeriesDate, SeriesTime, SeriesNumber, @@ -62,11 +63,11 @@ export default function getSopClassHandlerModule({ servicesManager }) { metadata, }; - segDisplaySet.getSourceDisplaySet = function (studies) { + segDisplaySet.getSourceDisplaySet = function(studies) { return getSourceDisplaySet(studies, segDisplaySet); }; - segDisplaySet.load = async function (referencedDisplaySet, studies) { + segDisplaySet.load = async function(referencedDisplaySet, studies) { segDisplaySet.isLoaded = true; const { StudyInstanceUID } = referencedDisplaySet; const segArrayBuffer = await DicomLoaderService.findDicomDataPromise( @@ -81,7 +82,7 @@ export default function getSopClassHandlerModule({ servicesManager }) { StudyInstanceUID, referencedDisplaySet.SeriesInstanceUID ); - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { let results; try { results = _parseSeg(segArrayBuffer, imageIds); @@ -90,18 +91,47 @@ export default function getSopClassHandlerModule({ servicesManager }) { segDisplaySet.loadError = true; reject(error); } - const { labelmapBufferArray, segMetadata, segmentsOnFrame, segmentsOnFrameArray } = results; + const { + labelmapBufferArray, + segMetadata, + segmentsOnFrame, + segmentsOnFrameArray, + } = results; + let labelmapIndex; if (labelmapBufferArray.length > 1) { + let labelmapIndexes = []; for (let i = 0; i < labelmapBufferArray.length; ++i) { - loadSegmentation(imageIds, segDisplaySet, labelmapBufferArray[i], segMetadata, segmentsOnFrame, segmentsOnFrameArray[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 { - loadSegmentation(imageIds, segDisplaySet, labelmapBufferArray[0], segMetadata, segmentsOnFrame, []); + labelmapIndex = await loadSegmentation( + imageIds, + segDisplaySet, + labelmapBufferArray[0], + segMetadata, + segmentsOnFrame, + [] + ); } - segDisplaySet.labelmapIndex = 0; - - resolve(); + resolve(labelmapIndex); }); }; diff --git a/extensions/dicom-segmentation/src/loadSegmentation.js b/extensions/dicom-segmentation/src/loadSegmentation.js index beb4f80ba..a655c0687 100644 --- a/extensions/dicom-segmentation/src/loadSegmentation.js +++ b/extensions/dicom-segmentation/src/loadSegmentation.js @@ -34,7 +34,11 @@ export default async function loadSegmentation( * 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.labelmapSegments[labelmapIndex] = labelmapSegments.length + ? Array.from( + new Set(labelmapSegments.filter(a => !!a).reduce((a, b) => a.concat(b))) + ) + : []; segDisplaySet.labelmapIndex = labelmapIndex; /* @@ -57,6 +61,8 @@ export default async function loadSegmentation( console.log('Segmentation loaded.'); const event = new CustomEvent('extensiondicomsegmentationsegloaded'); document.dispatchEvent(event); + + return labelmapIndex; } function _getNextLabelmapIndex(firstImageId) { diff --git a/extensions/dicom-segmentation/src/utils/setActiveLabelMap.js b/extensions/dicom-segmentation/src/utils/setActiveLabelMap.js index ec5535b3a..8a08aad10 100644 --- a/extensions/dicom-segmentation/src/utils/setActiveLabelMap.js +++ b/extensions/dicom-segmentation/src/utils/setActiveLabelMap.js @@ -37,9 +37,14 @@ export default async function setActiveLabelmap( ? brushStackState.activeLabelmapIndex : undefined; - if (displaySet.labelmapIndex === activeLabelmapIndex) { + let labelmapIndex = + displaySet.hasOverlapping === true + ? displaySet.originLabelMapIndex + : displaySet.labelmapIndex; + + if (labelmapIndex === activeLabelmapIndex) { log.warn(`${activeLabelmapIndex} is already the active labelmap`); - return displaySet.labelmapIndex; + return labelmapIndex; } if (!displaySet.isLoaded) { @@ -55,13 +60,18 @@ export default async function setActiveLabelmap( await loadPromise; } + labelmapIndex = + displaySet.hasOverlapping === true + ? displaySet.originLabelMapIndex + : displaySet.labelmapIndex; + // This might have just been created, so need to use the non-cached value. state = cornerstoneTools.getModule('segmentation').state; brushStackState = state.series[firstImageId]; - brushStackState.activeLabelmapIndex = displaySet.labelmapIndex; + brushStackState.activeLabelmapIndex = labelmapIndex; refreshViewports(); callback(); - return displaySet.labelmapIndex; + return labelmapIndex; }