feat: 🎸 Seg jump to slice + show/hide
* Add segment part 2 (jump to frame and visibility toggle)
* Cr updates
* Filter displaysets with images
* feat: 🎸 Seg jump to slice + show/hide
Co-authored-by: James Petts <jamesapetts@gmail.com>
This commit is contained in:
parent
fc40d84092
commit
835f64d47a
@ -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.10.1",
|
"dcmjs": "^0.12.2",
|
||||||
"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",
|
||||||
|
|||||||
@ -267,7 +267,7 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
const study = studyMetadataManager.get(StudyInstanceUID);
|
const study = studyMetadataManager.get(StudyInstanceUID);
|
||||||
|
|
||||||
const displaySet = study.findDisplaySet(ds => {
|
const displaySet = study.findDisplaySet(ds => {
|
||||||
return ds.images.find(i => i.getSOPInstanceUID() === SOPInstanceUID)
|
return ds.images && ds.images.find(i => i.getSOPInstanceUID() === SOPInstanceUID)
|
||||||
});
|
});
|
||||||
|
|
||||||
displaySet.SOPInstanceUID = SOPInstanceUID;
|
displaySet.SOPInstanceUID = SOPInstanceUID;
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"dcmjs": "^0.10.1",
|
"dcmjs": "^0.12.2",
|
||||||
"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"
|
||||||
|
|||||||
@ -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.8.3",
|
"dcmjs": "^0.12.2",
|
||||||
"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"
|
||||||
|
|||||||
@ -61,14 +61,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dcmrt-structure-set-item .item-label {
|
.dcmrt-structure-set-item .item-label {
|
||||||
overflow: hidden;
|
|
||||||
max-width: calc(100% - 15px); /* calc(100% - 50px); 20px = eye icon */
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow-wrap: normal;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label span {
|
||||||
|
overflow-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: calc(100% - 40px); /* calc(100% - 50px); 20px = eye icon */
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dcmrt-structure-set-item .item-label .eye-icon {
|
.dcmrt-structure-set-item .item-label .eye-icon {
|
||||||
|
|||||||
@ -41,7 +41,7 @@ const StructureSetItem = ({
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="item-label" style={{ marginBottom: 4 }}>
|
<div className="item-label" style={{ marginBottom: 4 }}>
|
||||||
{label}
|
<span>{label}</span>
|
||||||
<Icon
|
<Icon
|
||||||
className={`eye-icon ${isVisible && '--visible'}`}
|
className={`eye-icon ${isVisible && '--visible'}`}
|
||||||
name={isVisible ? 'eye' : 'eye-closed'}
|
name={isVisible ? 'eye' : 'eye-closed'}
|
||||||
|
|||||||
@ -27,10 +27,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RTPanel
|
<RTPanel {...props} onContourItemClick={contourItemClickHandler} />
|
||||||
{...props}
|
|
||||||
onContourItemClick={contourItemClickHandler}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -51,8 +48,10 @@ export default {
|
|||||||
if (study && study.series) {
|
if (study && study.series) {
|
||||||
for (let j = 0; j < study.series.length; j++) {
|
for (let j = 0; j < study.series.length; j++) {
|
||||||
const series = study.series[j];
|
const series = study.series[j];
|
||||||
console.log(studies);
|
if (
|
||||||
if (['RTSTRUCT', 'RTPLAN', 'RTDOSE'].includes(series.Modality)) {
|
/* Could be expanded to contain RTPLAN and RTDOSE information in the future */
|
||||||
|
['RTSTRUCT'].includes(series.Modality)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.12.5",
|
"cornerstone-tools": "4.12.5",
|
||||||
"dcmjs": "^0.6.1",
|
"dcmjs": "^0.12.2",
|
||||||
"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"
|
||||||
|
|||||||
@ -12,14 +12,6 @@
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dcmseg-segment-item .segment-label {
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: calc(100% - 50px);
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow-wrap: normal;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dcmseg-segment-item .segment-info {
|
.dcmseg-segment-item .segment-info {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 9px;
|
margin-top: 9px;
|
||||||
@ -62,3 +54,30 @@
|
|||||||
.dcmseg-segment-item .segment-actions .btnAction i {
|
.dcmseg-segment-item .segment-actions .btnAction i {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dcmseg-segment-item .segment-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmseg-segment-item .segment-label span {
|
||||||
|
overflow-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: calc(100% - 40px); /* calc(100% - 50px); 20px = eye icon */
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmseg-segment-item .segment-label .eye-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmseg-segment-item .segment-label .eye-icon:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmseg-segment-item .segment-label .eye-icon.--visible {
|
||||||
|
color: var(--default-color);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { TableListItem, Icon } from '@ohif/ui';
|
import { TableListItem, Icon } from '@ohif/ui';
|
||||||
|
|
||||||
@ -17,48 +17,63 @@ ColoredCircle.propTypes = {
|
|||||||
color: PropTypes.array.isRequired,
|
color: PropTypes.array.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SegmentItem = ({ index, label, onClick, itemClass, color }) => (
|
const SegmentItem = ({ index, label, onClick, itemClass, color, visible = true, onVisibilityChange }) => {
|
||||||
<div className="dcmseg-segment-item">
|
const [isVisible, setIsVisible] = useState(visible);
|
||||||
<TableListItem
|
return (
|
||||||
key={index}
|
<div className="dcmseg-segment-item">
|
||||||
itemKey={index}
|
<TableListItem
|
||||||
itemIndex={index}
|
key={index}
|
||||||
itemClass={itemClass}
|
itemKey={index}
|
||||||
itemMeta={<ColoredCircle color={color} />}
|
itemIndex={index}
|
||||||
itemMetaClass="segment-color-section"
|
itemClass={itemClass}
|
||||||
onItemClick={onClick}
|
itemMeta={<ColoredCircle color={color} />}
|
||||||
>
|
itemMetaClass="segment-color-section"
|
||||||
<div>
|
onItemClick={onClick}
|
||||||
<div className="segment-label" style={{ marginBottom: 4 }}>
|
>
|
||||||
{label}
|
<div>
|
||||||
</div>
|
<div className="segment-label" style={{ marginBottom: 4 }}>
|
||||||
{false && <div className="segment-info">{'...'}</div>}
|
<span>{label}</span>
|
||||||
{false && (
|
<Icon
|
||||||
<div className="segment-actions">
|
className={`eye-icon ${isVisible && '--visible'}`}
|
||||||
<button
|
name={isVisible ? 'eye' : 'eye-closed'}
|
||||||
className="btnAction"
|
width="20px"
|
||||||
onClick={() => console.log('Relabelling...')}
|
height="20px"
|
||||||
>
|
onClick={event => {
|
||||||
<span style={{ marginRight: '4px' }}>
|
event.stopPropagation();
|
||||||
<Icon name="edit" width="14px" height="14px" />
|
const newVisibility = !isVisible;
|
||||||
</span>
|
setIsVisible(newVisibility);
|
||||||
|
onVisibilityChange(newVisibility);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{false && <div className="segment-info">{'...'}</div>}
|
||||||
|
{false && (
|
||||||
|
<div className="segment-actions">
|
||||||
|
<button
|
||||||
|
className="btnAction"
|
||||||
|
onClick={() => console.log('Relabelling...')}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: '4px' }}>
|
||||||
|
<Icon name="edit" width="14px" height="14px" />
|
||||||
|
</span>
|
||||||
Relabel
|
Relabel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btnAction"
|
className="btnAction"
|
||||||
onClick={() => console.log('Editing description...')}
|
onClick={() => console.log('Editing description...')}
|
||||||
>
|
>
|
||||||
<span style={{ marginRight: '4px' }}>
|
<span style={{ marginRight: '4px' }}>
|
||||||
<Icon name="edit" width="14px" height="14px" />
|
<Icon name="edit" width="14px" height="14px" />
|
||||||
</span>
|
</span>
|
||||||
Description
|
Description
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableListItem>
|
</TableListItem>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
SegmentItem.propTypes = {
|
SegmentItem.propTypes = {
|
||||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
@ -28,13 +28,20 @@ const refreshViewport = () => {
|
|||||||
/**
|
/**
|
||||||
* SegmentationPanel component
|
* SegmentationPanel component
|
||||||
*
|
*
|
||||||
* @param {Object} props
|
* @param {Array} props.studies - Studies data
|
||||||
* @param {Array} props.studies
|
* @param {Array} props.viewports - Viewports data (viewportSpecificData)
|
||||||
* @param {Array} props.viewports - viewportSpecificData
|
* @param {number} props.activeIndex - Active viewport index
|
||||||
* @param {number} props.activeIndex - activeViewportIndex
|
* @param {boolean} props.isOpen - Boolean that indicates if the panel is expanded
|
||||||
|
* @param {Function} props.onSegItemClick - Segment click handler
|
||||||
* @returns component
|
* @returns component
|
||||||
*/
|
*/
|
||||||
const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
const SegmentationPanel = ({
|
||||||
|
studies,
|
||||||
|
viewports,
|
||||||
|
activeIndex,
|
||||||
|
isOpen,
|
||||||
|
onSegItemClick,
|
||||||
|
}) => {
|
||||||
/*
|
/*
|
||||||
* TODO: wrap get/set interactions with the cornerstoneTools
|
* TODO: wrap get/set interactions with the cornerstoneTools
|
||||||
* store with context to make these kind of things less blurry.
|
* store with context to make these kind of things less blurry.
|
||||||
@ -43,13 +50,14 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
const DEFAULT_BRUSH_RADIUS = configuration.radius || 10;
|
const DEFAULT_BRUSH_RADIUS = configuration.radius || 10;
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
brushRadius: DEFAULT_BRUSH_RADIUS,
|
brushRadius: DEFAULT_BRUSH_RADIUS,
|
||||||
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. */
|
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. */,
|
||||||
selectedSegment: null,
|
selectedSegment: null,
|
||||||
selectedSegmentation: null,
|
selectedSegmentation: null,
|
||||||
showSegSettings: false,
|
showSegSettings: false,
|
||||||
brushStackState: null,
|
brushStackState: null,
|
||||||
labelmapList: [],
|
labelmapList: [],
|
||||||
segmentList: []
|
segmentList: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -57,7 +65,12 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
log.warn('Segmentation Panel: labelmap modified', event);
|
log.warn('Segmentation Panel: labelmap modified', event);
|
||||||
const module = cornerstoneTools.getModule('segmentation');
|
const module = cornerstoneTools.getModule('segmentation');
|
||||||
const activeViewport = viewports[activeIndex];
|
const activeViewport = viewports[activeIndex];
|
||||||
const firstImageId = studyMetadata.getFirstImageId(activeViewport.displaySetInstanceUID);
|
const studyMetadata = studyMetadataManager.get(
|
||||||
|
activeViewport.StudyInstanceUID
|
||||||
|
);
|
||||||
|
const firstImageId = studyMetadata.getFirstImageId(
|
||||||
|
activeViewport.displaySetInstanceUID
|
||||||
|
);
|
||||||
updateState('brushStackState', module.state.series[firstImageId]);
|
updateState('brushStackState', module.state.series[firstImageId]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,20 +99,29 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const module = cornerstoneTools.getModule('segmentation');
|
const module = cornerstoneTools.getModule('segmentation');
|
||||||
const activeViewport = viewports[activeIndex];
|
const activeViewport = viewports[activeIndex];
|
||||||
const studyMetadata = studyMetadataManager.get(activeViewport.StudyInstanceUID);
|
const studyMetadata = studyMetadataManager.get(
|
||||||
const firstImageId = studyMetadata.getFirstImageId(activeViewport.displaySetInstanceUID);
|
activeViewport.StudyInstanceUID
|
||||||
|
);
|
||||||
|
const firstImageId = studyMetadata.getFirstImageId(
|
||||||
|
activeViewport.displaySetInstanceUID
|
||||||
|
);
|
||||||
const brushStackState = module.state.series[firstImageId];
|
const brushStackState = module.state.series[firstImageId];
|
||||||
|
|
||||||
if (brushStackState) {
|
if (brushStackState) {
|
||||||
const labelmap3D = brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
const labelmap3D =
|
||||||
const labelmapList = getLabelmapList(brushStackState, firstImageId, activeViewport);
|
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
||||||
const segmentList = getSegmentList(labelmap3D, firstImageId);
|
const labelmapList = getLabelmapList(
|
||||||
|
brushStackState,
|
||||||
|
firstImageId,
|
||||||
|
activeViewport
|
||||||
|
);
|
||||||
|
const segmentList = getSegmentList(labelmap3D, firstImageId, brushStackState);
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
brushStackState,
|
brushStackState,
|
||||||
selectedSegmentation: brushStackState.activeLabelmapIndex,
|
selectedSegmentation: brushStackState.activeLabelmapIndex,
|
||||||
labelmapList,
|
labelmapList,
|
||||||
segmentList
|
segmentList,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
@ -108,126 +130,201 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
segmentList: [],
|
segmentList: [],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [studies, viewports, activeIndex]);
|
}, [studies, viewports, activeIndex, getLabelmapList, getSegmentList]);
|
||||||
|
|
||||||
/* Handle open/closed panel behaviour */
|
/* Handle open/closed panel behaviour */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateState('showSegSettings', state.showSegSettings && !isOpen);
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
showSegSettings: state.showSegSettings && !isOpen,
|
||||||
|
}));
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const getLabelmapList = (brushStackState, firstImageId, activeViewport) => {
|
const getLabelmapList = useCallback(
|
||||||
/* Get list of SEG labelmaps specific to active viewport (reference series) */
|
(brushStackState, firstImageId, activeViewport) => {
|
||||||
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
|
/* Get list of SEG labelmaps specific to active viewport (reference series) */
|
||||||
activeViewport.StudyInstanceUID,
|
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
|
||||||
activeViewport.SeriesInstanceUID
|
activeViewport.StudyInstanceUID,
|
||||||
);
|
activeViewport.SeriesInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
return referencedSegDisplaysets.map((displaySet, index) => {
|
return referencedSegDisplaysets.map((displaySet, index) => {
|
||||||
const { labelmapIndex, SeriesDate, SeriesTime } = displaySet;
|
const { labelmapIndex, SeriesDate, SeriesTime } = displaySet;
|
||||||
|
|
||||||
/* Map to display representation */
|
/* Map to display representation */
|
||||||
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
|
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
|
||||||
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
|
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
|
||||||
const isActiveLabelmap =
|
const isActiveLabelmap =
|
||||||
labelmapIndex === brushStackState.activeLabelmapIndex;
|
labelmapIndex === brushStackState.activeLabelmapIndex;
|
||||||
const displayDate = date.format('ddd, MMM Do YYYY');
|
const displayDate = date.format('ddd, MMM Do YYYY');
|
||||||
const displayTime = date.format('h:mm:ss a');
|
const displayTime = date.format('h:mm:ss a');
|
||||||
const displayDescription = displaySet.SeriesDescription;
|
const displayDescription = displaySet.SeriesDescription;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: labelmapIndex,
|
value: labelmapIndex,
|
||||||
title: displayDescription,
|
title: displayDescription,
|
||||||
description: displayDate,
|
description: displayDate,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const activatedLabelmapIndex = await _setActiveLabelmap(
|
const activatedLabelmapIndex = await _setActiveLabelmap(
|
||||||
activeViewport,
|
activeViewport,
|
||||||
studies,
|
studies,
|
||||||
displaySet,
|
displaySet,
|
||||||
firstImageId,
|
firstImageId,
|
||||||
brushStackState.activeLabelmapIndex
|
brushStackState.activeLabelmapIndex
|
||||||
);
|
);
|
||||||
updateState('selectedSegmentation', activatedLabelmapIndex);
|
updateState('selectedSegmentation', activatedLabelmapIndex);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
[studies]
|
||||||
|
);
|
||||||
|
|
||||||
const getSegmentList = (labelmap3D, firstImageId) => {
|
const getSegmentList = useCallback(
|
||||||
/*
|
(labelmap3D, firstImageId, brushStackState) => {
|
||||||
* Newly created segments have no `meta`
|
/*
|
||||||
* So we instead build a list of all segment indexes in use
|
* Newly created segments have no `meta`
|
||||||
* Then find any associated metadata
|
* So we instead build a list of all segment indexes in use
|
||||||
*/
|
* Then find any associated metadata
|
||||||
const uniqueSegmentIndexes = labelmap3D.labelmaps2D
|
*/
|
||||||
.reduce((acc, labelmap2D) => {
|
const uniqueSegmentIndexes = labelmap3D.labelmaps2D
|
||||||
if (labelmap2D) {
|
.reduce((acc, labelmap2D) => {
|
||||||
const segmentIndexes = labelmap2D.segmentsOnLabelmap;
|
if (labelmap2D) {
|
||||||
|
const segmentIndexes = labelmap2D.segmentsOnLabelmap;
|
||||||
|
|
||||||
for (let i = 0; i < segmentIndexes.length; i++) {
|
for (let i = 0; i < segmentIndexes.length; i++) {
|
||||||
if (!acc.includes(segmentIndexes[i]) && segmentIndexes[i] !== 0) {
|
if (!acc.includes(segmentIndexes[i]) && segmentIndexes[i] !== 0) {
|
||||||
acc.push(segmentIndexes[i]);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
const sameSegment = state.selectedSegment === segmentNumber;
|
||||||
}, [])
|
const setCurrentSelectedSegment = () => {
|
||||||
.sort((a, b) => a - b);
|
_setActiveSegment(
|
||||||
|
firstImageId,
|
||||||
|
segmentNumber,
|
||||||
|
labelmap3D.activeSegmentIndex
|
||||||
|
);
|
||||||
|
updateState('selectedSegment', sameSegment ? null : segmentNumber);
|
||||||
|
|
||||||
const module = cornerstoneTools.getModule('segmentation');
|
const validIndexList = [];
|
||||||
const colorLutTable =
|
labelmap3D.labelmaps2D.forEach((labelMap2D, index) => {
|
||||||
module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
if (labelMap2D.segmentsOnLabelmap.includes(segmentNumber)) {
|
||||||
const hasLabelmapMeta = labelmap3D.metadata && labelmap3D.metadata.data;
|
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 segmentList = [];
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
for (let i = 0; i < uniqueSegmentIndexes.length; i++) {
|
const element = enabledElements[activeIndex].element;
|
||||||
const segmentIndex = uniqueSegmentIndexes[i];
|
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
||||||
|
|
||||||
const color = colorLutTable[segmentIndex];
|
if (!toolState) {
|
||||||
let segmentLabel = '(unlabeled)';
|
return;
|
||||||
let segmentNumber = segmentIndex;
|
}
|
||||||
|
|
||||||
/* Meta */
|
const imageIds = toolState.data[0].imageIds;
|
||||||
if (hasLabelmapMeta) {
|
const imageId = imageIds[closest];
|
||||||
const segmentMeta = labelmap3D.metadata.data[segmentIndex];
|
const frameIndex = imageIds.indexOf(imageId);
|
||||||
|
|
||||||
if (segmentMeta) {
|
const SOPInstanceUID = cornerstone.metaData.get(
|
||||||
segmentNumber = segmentMeta.SegmentNumber;
|
'SOPInstanceUID',
|
||||||
segmentLabel = segmentMeta.SegmentLabel;
|
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();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sameSegment = state.selectedSegment === segmentNumber;
|
return segmentList;
|
||||||
const setCurrentSelectedSegment = () => {
|
|
||||||
_setActiveSegment(
|
|
||||||
firstImageId,
|
|
||||||
segmentNumber,
|
|
||||||
labelmap3D.activeSegmentIndex
|
|
||||||
);
|
|
||||||
updateState('selectedSegment', sameSegment ? null : segmentNumber);
|
|
||||||
};
|
|
||||||
|
|
||||||
segmentList.push(
|
/*
|
||||||
<SegmentItem
|
* Let's iterate over segmentIndexes ^ above
|
||||||
key={segmentNumber}
|
* If meta has a match, use it to show info
|
||||||
itemClass={`segment-item ${sameSegment && 'selected'}`}
|
* If now, add "no-meta" class
|
||||||
onClick={setCurrentSelectedSegment}
|
* Show default name
|
||||||
label={segmentLabel}
|
*/
|
||||||
index={segmentNumber}
|
},
|
||||||
color={color}
|
[activeIndex, onSegItemClick, state.selectedSegment]
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateState = (field, value) => {
|
const updateState = (field, value) => {
|
||||||
setState(state => ({ ...state, [field]: value }));
|
setState(state => ({ ...state, [field]: value }));
|
||||||
@ -270,8 +367,7 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const module = cornerstoneTools.getModule('segmentation');
|
const module = cornerstoneTools.getModule('segmentation');
|
||||||
const colorLutTable =
|
const colorLutTable = module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
||||||
module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
|
||||||
const color = colorLutTable[labelmap3D.activeSegmentIndex];
|
const color = colorLutTable[labelmap3D.activeSegmentIndex];
|
||||||
|
|
||||||
return `rgba(${color.join(',')})`;
|
return `rgba(${color.join(',')})`;
|
||||||
@ -329,7 +425,9 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
<div className="segmentations">
|
<div className="segmentations">
|
||||||
<SegmentationSelect
|
<SegmentationSelect
|
||||||
value={
|
value={
|
||||||
state.labelmapList.find(i => i.value === state.selectedSegmentation) || null
|
state.labelmapList.find(
|
||||||
|
i => i.value === state.selectedSegmentation
|
||||||
|
) || null
|
||||||
}
|
}
|
||||||
formatOptionLabel={SegmentationItem}
|
formatOptionLabel={SegmentationItem}
|
||||||
options={state.labelmapList}
|
options={state.labelmapList}
|
||||||
|
|||||||
@ -17,6 +17,10 @@ const defaultColor = computedstyle.getPropertyValue('--default-color');
|
|||||||
const uiGrayDark = computedstyle.getPropertyValue('--ui-gray-dark');
|
const uiGrayDark = computedstyle.getPropertyValue('--ui-gray-dark');
|
||||||
|
|
||||||
const segmentationSelectStyles = {
|
const segmentationSelectStyles = {
|
||||||
|
singleValue: (base, state) => ({
|
||||||
|
...base,
|
||||||
|
width: '100%'
|
||||||
|
}),
|
||||||
control: (base, state) => ({
|
control: (base, state) => ({
|
||||||
...base,
|
...base,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
|
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 panelModule from './panelModule.js';
|
|
||||||
import sopClassHandlerModule from './OHIFDicomSegSopClassHandler.js';
|
import sopClassHandlerModule from './OHIFDicomSegSopClassHandler.js';
|
||||||
|
import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@ -21,8 +23,54 @@ export default {
|
|||||||
getToolbarModule({ servicesManager }) {
|
getToolbarModule({ servicesManager }) {
|
||||||
return toolbarModule;
|
return toolbarModule;
|
||||||
},
|
},
|
||||||
getPanelModule({ servicesManager }) {
|
getPanelModule({ commandsManager }) {
|
||||||
return panelModule;
|
const ExtendedSegmentationPanel = props => {
|
||||||
|
const segItemClickHandler = segData => {
|
||||||
|
commandsManager.runCommand('jumpToImage', segData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SegmentationPanel {...props} onSegItemClick={segItemClickHandler} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
icon: 'list',
|
||||||
|
label: 'Segmentations',
|
||||||
|
target: 'segmentation-panel',
|
||||||
|
isDisabled: studies => {
|
||||||
|
if (!studies) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < studies.length; i++) {
|
||||||
|
const study = studies[i];
|
||||||
|
|
||||||
|
if (study && study.series) {
|
||||||
|
for (let j = 0; j < study.series.length; j++) {
|
||||||
|
const series = study.series[j];
|
||||||
|
|
||||||
|
if (series.Modality === 'SEG') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
id: 'segmentation-panel',
|
||||||
|
component: ExtendedSegmentationPanel,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultContext: ['VIEWER'],
|
||||||
|
};
|
||||||
},
|
},
|
||||||
getSopClassHandlerModule({ servicesManager }) {
|
getSopClassHandlerModule({ servicesManager }) {
|
||||||
return sopClassHandlerModule;
|
return sopClassHandlerModule;
|
||||||
|
|||||||
@ -1,39 +0,0 @@
|
|||||||
import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
icon: 'list',
|
|
||||||
label: 'Segmentations',
|
|
||||||
target: 'segmentation-panel',
|
|
||||||
isDisabled: studies => {
|
|
||||||
if (!studies) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < studies.length; i++) {
|
|
||||||
const study = studies[i];
|
|
||||||
|
|
||||||
if (study && study.series) {
|
|
||||||
for (let j = 0; j < study.series.length; j++) {
|
|
||||||
const series = study.series[j];
|
|
||||||
|
|
||||||
if (series.Modality === 'SEG') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
id: 'segmentation-panel',
|
|
||||||
component: SegmentationPanel,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultContext: ['VIEWER'],
|
|
||||||
};
|
|
||||||
@ -35,7 +35,7 @@
|
|||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.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.10.1",
|
"dcmjs": "^0.12.2",
|
||||||
"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",
|
||||||
|
|||||||
@ -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.12.0",
|
"dcmjs": "^0.12.2",
|
||||||
"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",
|
||||||
|
|||||||
@ -400,6 +400,7 @@ class MetadataProvider {
|
|||||||
|
|
||||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE:
|
case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE:
|
||||||
metadata = {
|
metadata = {
|
||||||
|
sopInstanceUid: instance.SOPInstanceUID,
|
||||||
instanceNumber: instance.InstanceNumber,
|
instanceNumber: instance.InstanceNumber,
|
||||||
lossyImageCompression: instance.LossyImageCompression,
|
lossyImageCompression: instance.LossyImageCompression,
|
||||||
lossyImageCompressionRatio: instance.LossyImageCompressionRatio,
|
lossyImageCompressionRatio: instance.LossyImageCompressionRatio,
|
||||||
|
|||||||
@ -67,7 +67,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.0",
|
"dcmjs": "^0.12.2",
|
||||||
"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",
|
||||||
|
|||||||
@ -6534,10 +6534,10 @@ 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.12.0:
|
dcmjs@^0.12.2:
|
||||||
version "0.12.0"
|
version "0.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.12.0.tgz#8b1634f9b66e452075295f5d1f2c5bf3dabdf03e"
|
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.12.2.tgz#1c16e19c27ff43202abc7cd25c5ecd6d8bf7672e"
|
||||||
integrity sha512-AZAnFMvzAxUv5+KWoZcxnTYflLKk0rNPeeFt2KrVAgaExFBFfDgJQh1lEgqdNlIP+XQDFrEEUmp726SQhgmVCg==
|
integrity sha512-B7cEVCfDi3mOLKoBYUMYy+6COjLZhtKaesNpq5XNA4IbEH4EJYVweJfiI9gsR5hypv9LAPg72xG/xrbqjy8TgQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/polyfill" "^7.8.3"
|
"@babel/polyfill" "^7.8.3"
|
||||||
"@babel/runtime" "^7.8.4"
|
"@babel/runtime" "^7.8.4"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user