Files
ohif-viewer/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js
T

567 lines
17 KiB
JavaScript
Raw Normal View History

2020-04-24 07:25:05 -03:00
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import moment from 'moment';
import { utils, log } from '@ohif/core';
import { ScrollableArea, TableList, Icon } from '@ohif/ui';
import {
BrushColorSelector,
BrushRadius,
SegmentationItem,
SegmentItem,
SegmentationSelect,
} from '../index';
import './SegmentationPanel.css';
import SegmentationSettings from '../SegmentationSettings/SegmentationSettings';
const { studyMetadataManager } = utils;
const refreshViewport = () => {
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
};
/**
* SegmentationPanel component
*
2020-04-24 07:25:05 -03:00
* @param {Array} props.studies - Studies data
* @param {Array} props.viewports - Viewports data (viewportSpecificData)
* @param {number} props.activeIndex - Active viewport index
* @param {boolean} props.isOpen - Boolean that indicates if the panel is expanded
* @param {Function} props.onSegItemClick - Segment click handler
* @returns component
*/
2020-04-24 07:25:05 -03:00
const SegmentationPanel = ({
studies,
viewports,
activeIndex,
isOpen,
onSegItemClick,
}) => {
/*
* TODO: wrap get/set interactions with the cornerstoneTools
* store with context to make these kind of things less blurry.
*/
2020-04-01 00:54:02 -03:00
const { configuration } = cornerstoneTools.getModule('segmentation');
const DEFAULT_BRUSH_RADIUS = configuration.radius || 10;
2020-04-01 00:54:02 -03:00
const [state, setState] = useState({
brushRadius: DEFAULT_BRUSH_RADIUS,
2020-04-24 07:25:05 -03:00
brushColor:
'rgba(221, 85, 85, 1)' /* TODO: We shouldn't hardcode this color, in the future the SEG may set the colorLUT to whatever it wants. */,
2020-04-01 00:54:02 -03:00
selectedSegment: null,
selectedSegmentation: null,
showSegSettings: false,
brushStackState: null,
labelmapList: [],
2020-04-24 07:25:05 -03:00
segmentList: [],
2020-04-01 00:54:02 -03:00
});
useEffect(() => {
const labelmapModifiedHandler = event => {
2020-04-01 00:54:02 -03:00
log.warn('Segmentation Panel: labelmap modified', event);
const module = cornerstoneTools.getModule('segmentation');
const activeViewport = viewports[activeIndex];
2020-04-24 07:25:05 -03:00
const studyMetadata = studyMetadataManager.get(
activeViewport.StudyInstanceUID
);
const firstImageId = studyMetadata.getFirstImageId(
activeViewport.displaySetInstanceUID
);
2020-04-01 00:54:02 -03:00
updateState('brushStackState', module.state.series[firstImageId]);
};
/*
* These are specific to each element;
* Need to iterate cornerstone-tools tracked enabled elements?
* Then only care about the one tied to active viewport?
*/
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
enabledElement.addEventListener(
'cornerstonetoolslabelmapmodified',
labelmapModifiedHandler
)
);
return () => {
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
enabledElement.removeEventListener(
'cornerstonetoolslabelmapmodified',
labelmapModifiedHandler
)
);
};
});
2020-04-01 00:54:02 -03:00
useEffect(() => {
const module = cornerstoneTools.getModule('segmentation');
const activeViewport = viewports[activeIndex];
2020-04-24 07:25:05 -03:00
const studyMetadata = studyMetadataManager.get(
activeViewport.StudyInstanceUID
);
const firstImageId = studyMetadata.getFirstImageId(
activeViewport.displaySetInstanceUID
);
2020-04-01 00:54:02 -03:00
const brushStackState = module.state.series[firstImageId];
2020-04-01 00:54:02 -03:00
if (brushStackState) {
2020-04-24 07:25:05 -03:00
const labelmap3D =
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
const labelmapList = getLabelmapList(
brushStackState,
firstImageId,
activeViewport
);
const segmentList = getSegmentList(labelmap3D, firstImageId, brushStackState);
2020-04-01 00:54:02 -03:00
setState(state => ({
...state,
brushStackState,
selectedSegmentation: brushStackState.activeLabelmapIndex,
labelmapList,
2020-04-24 07:25:05 -03:00
segmentList,
2020-04-01 00:54:02 -03:00
}));
} else {
setState(state => ({
...state,
labelmapList: [],
segmentList: [],
}));
}
}, [studies, viewports, activeIndex, getLabelmapList, getSegmentList, state.selectedSegmentation]);
2020-04-01 00:54:02 -03:00
/* Handle open/closed panel behaviour */
useEffect(() => {
2020-04-24 07:25:05 -03:00
setState(state => ({
...state,
showSegSettings: state.showSegSettings && !isOpen,
}));
2020-04-01 00:54:02 -03:00
}, [isOpen]);
2020-04-24 07:25:05 -03:00
const getLabelmapList = useCallback(
(brushStackState, firstImageId, activeViewport) => {
/* Get list of SEG labelmaps specific to active viewport (reference series) */
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
activeViewport.StudyInstanceUID,
activeViewport.SeriesInstanceUID
);
2020-04-24 07:25:05 -03:00
return referencedSegDisplaysets.map((displaySet, index) => {
const { labelmapIndex, SeriesDate, SeriesTime } = displaySet;
/* 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 {
value: labelmapIndex,
title: displayDescription,
description: displayDate,
onClick: async () => {
const activatedLabelmapIndex = await _setActiveLabelmap(
activeViewport,
studies,
displaySet,
firstImageId,
brushStackState.activeLabelmapIndex
);
updateState('selectedSegmentation', activatedLabelmapIndex);
},
};
});
},
[studies]
);
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]);
}
}
}
2020-04-24 07:25:05 -03:00
return acc;
}, [])
.sort((a, b) => a - b);
2020-04-24 07:25:05 -03:00
const module = cornerstoneTools.getModule('segmentation');
const colorLutTable =
module.state.colorLutTables[labelmap3D.colorLUTIndex];
const hasLabelmapMeta = labelmap3D.metadata && labelmap3D.metadata.data;
2020-04-24 07:25:05 -03:00
const segmentList = [];
for (let i = 0; i < uniqueSegmentIndexes.length; i++) {
const segmentIndex = uniqueSegmentIndexes[i];
2020-04-24 07:25:05 -03:00
const color = colorLutTable[segmentIndex];
let segmentLabel = '(unlabeled)';
let segmentNumber = segmentIndex;
2020-04-24 07:25:05 -03:00
/* Meta */
if (hasLabelmapMeta) {
const segmentMeta = labelmap3D.metadata.data[segmentIndex];
2020-04-24 07:25:05 -03:00
if (segmentMeta) {
segmentNumber = segmentMeta.SegmentNumber;
segmentLabel = segmentMeta.SegmentLabel;
}
}
2020-04-24 07:25:05 -03:00
const sameSegment = state.selectedSegment === segmentNumber;
const setCurrentSelectedSegment = () => {
_setActiveSegment(
firstImageId,
segmentNumber,
labelmap3D.activeSegmentIndex
);
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;
});
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
);
onSegItemClick({
StudyInstanceUID,
SOPInstanceUID,
frameIndex,
activeViewportIndex: activeIndex,
});
};
const enabledElements = cornerstone.getEnabledElements();
const enabledElementViewport = enabledElements[activeIndex];
let isVisible = true;
if (enabledElementViewport) {
const element = enabledElementViewport.element;
const module = cornerstoneTools.getModule('segmentation');
isVisible = module.getters.isSegmentVisible(
element,
segmentNumber,
brushStackState.activeLabelmapIndex
);
}
segmentList.push(
<SegmentItem
key={segmentNumber}
itemClass={`segment-item ${sameSegment && 'selected'}`}
onClick={setCurrentSelectedSegment}
label={segmentLabel}
index={segmentNumber}
color={color}
visible={isVisible}
onVisibilityChange={() => {
const element = enabledElements[activeIndex].element;
module.setters.toggleSegmentVisibility(
element,
segmentNumber,
brushStackState.activeLabelmapIndex
);
refreshViewport();
}}
/>
);
2020-04-24 07:25:05 -03:00
}
2020-04-24 07:25:05 -03:00
return segmentList;
2020-04-01 00:54:02 -03:00
2020-04-24 07:25:05 -03:00
/*
* 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, onSegItemClick, state.selectedSegment]
);
2020-04-01 00:54:02 -03:00
const updateState = (field, value) => {
setState(state => ({ ...state, [field]: value }));
};
const updateBrushSize = evt => {
const updatedRadius = Number(evt.target.value);
if (updatedRadius !== brushRadius) {
2020-04-01 00:54:02 -03:00
updateState('brushRadius', updatedRadius);
const module = cornerstoneTools.getModule('segmentation');
module.setters.radius(updatedRadius);
}
};
const decrementSegment = event => {
event.preventDefault();
if (labelmap3D.activeSegmentIndex > 1) {
labelmap3D.activeSegmentIndex--;
}
2020-04-01 00:54:02 -03:00
updateState('selectedSegment', labelmap3D.activeSegmentIndex);
updateActiveSegmentColor();
};
const incrementSegment = event => {
event.preventDefault();
labelmap3D.activeSegmentIndex++;
2020-04-01 00:54:02 -03:00
updateState('selectedSegment', labelmap3D.activeSegmentIndex);
updateActiveSegmentColor();
};
const updateActiveSegmentColor = () => {
const color = getActiveSegmentColor();
2020-04-01 00:54:02 -03:00
updateState('brushColor', color);
};
const getActiveSegmentColor = () => {
2020-04-01 00:54:02 -03:00
if (!state.brushStackState) {
return 'rgba(255, 255, 255, 1)';
}
2020-04-01 00:54:02 -03:00
const module = cornerstoneTools.getModule('segmentation');
2020-04-24 07:25:05 -03:00
const colorLutTable = module.state.colorLutTables[labelmap3D.colorLUTIndex];
const color = colorLutTable[labelmap3D.activeSegmentIndex];
return `rgba(${color.join(',')})`;
};
const updateConfiguration = newConfiguration => {
/* Supported configuration */
configuration.renderFill = newConfiguration.renderFill;
configuration.renderOutline = newConfiguration.renderOutline;
configuration.shouldRenderInactiveLabelmaps =
newConfiguration.shouldRenderInactiveLabelmaps;
configuration.fillAlpha = newConfiguration.fillAlpha;
configuration.outlineAlpha = newConfiguration.outlineAlpha;
configuration.outlineWidth = newConfiguration.outlineWidth;
configuration.fillAlphaInactive = newConfiguration.fillAlphaInactive;
configuration.outlineAlphaInactive = newConfiguration.outlineAlphaInactive;
refreshViewport();
};
2020-04-01 00:54:02 -03:00
if (state.showSegSettings) {
return (
<SegmentationSettings
configuration={configuration}
2020-04-01 00:54:02 -03:00
onBack={() => updateState('showSegSettings', false)}
onChange={updateConfiguration}
/>
);
} else {
return (
<div className="dcmseg-segmentation-panel">
<Icon
className="cog-icon"
name="cog"
width="25px"
height="25px"
2020-04-01 00:54:02 -03:00
onClick={() => updateState('showSegSettings', true)}
/>
{false && (
<form className="selector-form">
<BrushColorSelector
defaultColor={brushColor}
2020-04-01 00:54:02 -03:00
index={state.selectedSegment}
onNext={incrementSegment}
onPrev={decrementSegment}
/>
<BrushRadius
value={brushRadius}
onChange={updateBrushSize}
min={configuration.minRadius}
max={configuration.maxRadius}
/>
</form>
)}
<h3>Segmentations</h3>
<div className="segmentations">
<SegmentationSelect
value={state.labelmapList.find(i => i.value === state.selectedSegmentation)}
formatOptionLabel={SegmentationItem}
2020-04-01 00:54:02 -03:00
options={state.labelmapList}
/>
</div>
<ScrollableArea>
<TableList
2020-04-01 00:54:02 -03:00
customHeader={<SegmentsHeader count={state.segmentList.length} />}
>
2020-04-01 00:54:02 -03:00
{state.segmentList}
</TableList>
</ScrollableArea>
</div>
);
}
};
SegmentationPanel.propTypes = {
/*
* An object, with int index keys?
* Maps to: state.viewports.viewportSpecificData, in `viewer`
* Passed in MODULE_TYPES.PANEL when specifying component in viewer
*/
viewports: PropTypes.shape({
displaySetInstanceUID: PropTypes.string,
frameRate: PropTypes.any,
InstanceNumber: PropTypes.number,
isMultiFrame: PropTypes.bool,
isReconstructable: PropTypes.bool,
Modality: PropTypes.string,
plugin: PropTypes.string,
SeriesDate: PropTypes.string,
SeriesDescription: PropTypes.string,
SeriesInstanceUID: PropTypes.string,
SeriesNumber: PropTypes.any,
SeriesTime: PropTypes.string,
sopClassUIDs: PropTypes.arrayOf(PropTypes.string),
StudyInstanceUID: PropTypes.string,
}),
activeIndex: PropTypes.number.isRequired,
studies: PropTypes.array.isRequired,
2020-04-09 07:21:43 -03:00
isOpen: PropTypes.bool.isRequired,
};
SegmentationPanel.defaultProps = {};
/**
* Returns SEG Displaysets that reference the target series, sorted by dateTime
*
* @param {string} StudyInstanceUID
* @param {string} SeriesInstanceUID
* @returns Array
*/
const _getReferencedSegDisplaysets = (StudyInstanceUID, SeriesInstanceUID) => {
/* Referenced DisplaySets */
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
referencedSeriesInstanceUID: SeriesInstanceUID,
Modality: 'SEG',
});
/* Sort */
referencedDisplaysets.sort((a, b) => {
const aNumber = Number(`${a.SeriesDate}${a.SeriesTime}`);
const bNumber = Number(`${b.SeriesDate}${b.SeriesTime}`);
return aNumber - bNumber;
});
return referencedDisplaysets;
};
/**
*
*
* @param {*} viewportSpecificData
* @param {*} studies
* @param {*} displaySet
* @param {*} firstImageId
* @param {*} activeLabelmapIndex
* @returns
*/
const _setActiveLabelmap = async (
viewportSpecificData,
studies,
displaySet,
firstImageId,
activeLabelmapIndex
) => {
if (displaySet.labelmapIndex === activeLabelmapIndex) {
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
2020-04-01 00:54:02 -03:00
return displaySet.labelmapIndex;
}
if (!displaySet.isLoaded) {
// What props does this expect `viewportSpecificData` to have?
// TODO: Should this return the `labelmapIndex`?
await displaySet.load(viewportSpecificData, studies);
}
const { state } = cornerstoneTools.getModule('segmentation');
const brushStackState = state.series[firstImageId];
brushStackState.activeLabelmapIndex = displaySet.labelmapIndex;
refreshViewport();
return displaySet.labelmapIndex;
};
/**
*
* @param {*} firstImageId
* @param {*} activeSegmentIndex
* @returns
*/
const _setActiveSegment = (firstImageId, segmentIndex, activeSegmentIndex) => {
if (segmentIndex === activeSegmentIndex) {
log.info(`${activeSegmentIndex} is already the active segment`);
return;
}
const { state } = cornerstoneTools.getModule('segmentation');
const brushStackState = state.series[firstImageId];
const labelmap3D =
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
labelmap3D.activeSegmentIndex = segmentIndex;
refreshViewport();
return segmentIndex;
};
const SegmentsHeader = ({ count }) => {
return (
<React.Fragment>
<div className="tableListHeaderTitle">Segments</div>
<div className="numberOfItems">{count}</div>
</React.Fragment>
);
};
export default SegmentationPanel;