-
);
}
diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/LegacyPanelROIThresholdSegmentation.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/LegacyPanelROIThresholdSegmentation.tsx
new file mode 100644
index 000000000..ed20412ca
--- /dev/null
+++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/LegacyPanelROIThresholdSegmentation.tsx
@@ -0,0 +1,287 @@
+import React, { useEffect, useState, useCallback, useReducer } from 'react';
+import PropTypes from 'prop-types';
+import { SegmentationTable, Button, Icon } from '@ohif/ui';
+
+import { useTranslation } from 'react-i18next';
+import segmentationEditHandler from './segmentationEditHandler';
+import ExportReports from './ExportReports';
+import ROIThresholdConfiguration, { ROI_STAT } from './ROIThresholdConfiguration';
+
+const LOWER_CT_THRESHOLD_DEFAULT = -1024;
+const UPPER_CT_THRESHOLD_DEFAULT = 1024;
+const LOWER_PT_THRESHOLD_DEFAULT = 2.5;
+const UPPER_PT_THRESHOLD_DEFAULT = 100;
+const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature
+const DEFAULT_STRATEGY = ROI_STAT;
+
+function reducer(state, action) {
+ const { payload } = action;
+ const { strategy, ctLower, ctUpper, ptLower, ptUpper, weight } = payload;
+
+ switch (action.type) {
+ case 'setStrategy':
+ return {
+ ...state,
+ strategy,
+ };
+ case 'setThreshold':
+ return {
+ ...state,
+ ctLower: ctLower ? ctLower : state.ctLower,
+ ctUpper: ctUpper ? ctUpper : state.ctUpper,
+ ptLower: ptLower ? ptLower : state.ptLower,
+ ptUpper: ptUpper ? ptUpper : state.ptUpper,
+ };
+ case 'setWeight':
+ return {
+ ...state,
+ weight,
+ };
+ default:
+ return state;
+ }
+}
+
+export default function LegacyPanelRoiThresholdSegmentation({ servicesManager, commandsManager }) {
+ const { segmentationService } = servicesManager.services;
+
+ const { t } = useTranslation('PanelSUV');
+ const [showConfig, setShowConfig] = useState(false);
+ const [labelmapLoading, setLabelmapLoading] = useState(false);
+ const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
+ const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
+
+ const [config, dispatch] = useReducer(reducer, {
+ strategy: DEFAULT_STRATEGY,
+ ctLower: LOWER_CT_THRESHOLD_DEFAULT,
+ ctUpper: UPPER_CT_THRESHOLD_DEFAULT,
+ ptLower: LOWER_PT_THRESHOLD_DEFAULT,
+ ptUpper: UPPER_PT_THRESHOLD_DEFAULT,
+ weight: WEIGHT_DEFAULT,
+ });
+
+ const [tmtvValue, setTmtvValue] = useState(null);
+
+ const runCommand = useCallback(
+ (commandName, commandOptions = {}) => {
+ return commandsManager.runCommand(commandName, commandOptions);
+ },
+ [commandsManager]
+ );
+
+ const handleTMTVCalculation = useCallback(() => {
+ const tmtv = runCommand('calculateTMTV', { segmentations });
+
+ if (tmtv !== undefined) {
+ setTmtvValue(tmtv.toFixed(2));
+ }
+ }, [segmentations, runCommand]);
+
+ const handleROIThresholding = useCallback(() => {
+ const labelmap = runCommand('thresholdSegmentationByRectangleROITool', {
+ segmentationId: selectedSegmentationId,
+ config,
+ });
+
+ const lesionStats = runCommand('getLesionStats', { labelmap });
+ const suvPeak = runCommand('calculateSuvPeak', { labelmap });
+ const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
+
+ // update segDetails with the suv peak for the active segmentation
+ const segmentation = segmentationService.getSegmentation(selectedSegmentationId);
+
+ const cachedStats = {
+ lesionStats,
+ suvPeak,
+ lesionGlyoclysisStats,
+ };
+
+ const notYetUpdatedAtSource = true;
+ segmentationService.addOrUpdateSegmentation(
+ {
+ ...segmentation,
+ ...Object.assign(segmentation.cachedStats, cachedStats),
+ displayText: [`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`],
+ },
+ notYetUpdatedAtSource
+ );
+
+ handleTMTVCalculation();
+ }, [selectedSegmentationId, config]);
+
+ /**
+ * Update UI based on segmentation changes (added, removed, updated)
+ */
+ useEffect(() => {
+ // ~~ Subscription
+ const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
+ const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
+ const subscriptions = [];
+
+ [added, updated].forEach(evt => {
+ const { unsubscribe } = segmentationService.subscribe(evt, () => {
+ const segmentations = segmentationService.getSegmentations();
+ setSegmentations(segmentations);
+ });
+ subscriptions.push(unsubscribe);
+ });
+
+ return () => {
+ subscriptions.forEach(unsub => {
+ unsub();
+ });
+ };
+ }, []);
+
+ useEffect(() => {
+ const { unsubscribe } = segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_REMOVED,
+ () => {
+ const segmentations = segmentationService.getSegmentations();
+ setSegmentations(segmentations);
+
+ if (segmentations.length > 0) {
+ setSelectedSegmentationId(segmentations[0].id);
+ handleTMTVCalculation();
+ } else {
+ setSelectedSegmentationId(null);
+ setTmtvValue(null);
+ }
+ }
+ );
+
+ return () => {
+ unsubscribe();
+ };
+ }, []);
+
+ /**
+ * Whenever the segmentations change, update the TMTV calculations
+ */
+ useEffect(() => {
+ if (!selectedSegmentationId && segmentations.length > 0) {
+ setSelectedSegmentationId(segmentations[0].id);
+ }
+
+ handleTMTVCalculation();
+ }, [segmentations, selectedSegmentationId]);
+
+ return (
+ <>
+
+
+
+ {
+ setLabelmapLoading(true);
+ setTimeout(() => {
+ runCommand('createNewLabelmapFromPT').then(segmentationId => {
+ setLabelmapLoading(false);
+ setSelectedSegmentationId(segmentationId);
+ });
+ });
+ }}
+ >
+ {labelmapLoading ? 'loading ...' : 'New Label'}
+
+ Run
+
+
{
+ setShowConfig(!showConfig);
+ }}
+ >
+
{t('ROI Threshold Configuration')}
+
+ {showConfig && (
+
+ )}
+ {/* show segmentation table */}
+
+ {segmentations?.length ? (
+ {
+ runCommand('setSegmentationActiveForToolGroups', {
+ segmentationId: id,
+ });
+ setSelectedSegmentationId(id);
+ }}
+ onToggleVisibility={id => {
+ segmentationService.toggleSegmentationVisibility(id);
+ }}
+ onToggleVisibilityAll={ids => {
+ ids.map(id => {
+ segmentationService.toggleSegmentationVisibility(id);
+ });
+ }}
+ onDelete={id => {
+ segmentationService.remove(id);
+ }}
+ onEdit={id => {
+ segmentationEditHandler({
+ id,
+ servicesManager,
+ });
+ }}
+ />
+ ) : null}
+
+ {tmtvValue !== null ? (
+
+
+ {'TMTV:'}
+
+
{`${tmtvValue} mL`}
+
+ ) : null}
+
+
+
+
{
+ // navigate to a url in a new tab
+ window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
+ }}
+ >
+
+ {'User Guide'}
+
+ >
+ );
+}
+
+LegacyPanelRoiThresholdSegmentation.propTypes = {
+ commandsManager: PropTypes.shape({
+ runCommand: PropTypes.func.isRequired,
+ }),
+ servicesManager: PropTypes.shape({
+ services: PropTypes.shape({
+ segmentationService: PropTypes.shape({
+ getSegmentation: PropTypes.func.isRequired,
+ getSegmentations: PropTypes.func.isRequired,
+ toggleSegmentationVisibility: PropTypes.func.isRequired,
+ subscribe: PropTypes.func.isRequired,
+ EVENTS: PropTypes.object.isRequired,
+ }).isRequired,
+ }).isRequired,
+ }).isRequired,
+};
diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdSegmentation.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdSegmentation.tsx
index 306c39c94..dd5c0cdbb 100644
--- a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdSegmentation.tsx
+++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdSegmentation.tsx
@@ -1,67 +1,23 @@
-import React, { useEffect, useState, useCallback, useReducer } from 'react';
+import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
-import { SegmentationTable, Button, Icon } from '@ohif/ui';
+import { SegmentationGroupTableExpanded, Icon } from '@ohif/ui';
+import { createReportAsync } from '@ohif/extension-default';
-import { useTranslation } from 'react-i18next';
import segmentationEditHandler from './segmentationEditHandler';
import ExportReports from './ExportReports';
-import ROIThresholdConfiguration, { ROI_STAT } from './ROIThresholdConfiguration';
+import callInputDialog from './callInputDialog';
+import callColorPickerDialog from './colorPickerDialog';
-const LOWER_CT_THRESHOLD_DEFAULT = -1024;
-const UPPER_CT_THRESHOLD_DEFAULT = 1024;
-const LOWER_PT_THRESHOLD_DEFAULT = 2.5;
-const UPPER_PT_THRESHOLD_DEFAULT = 100;
-const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature
-const DEFAULT_STRATEGY = ROI_STAT;
+export default function PanelRoiThresholdSegmentation({
+ servicesManager,
+ commandsManager,
+ extensionManager,
+}) {
+ const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
-function reducer(state, action) {
- const { payload } = action;
- const { strategy, ctLower, ctUpper, ptLower, ptUpper, weight } = payload;
-
- switch (action.type) {
- case 'setStrategy':
- return {
- ...state,
- strategy,
- };
- case 'setThreshold':
- return {
- ...state,
- ctLower: ctLower ? ctLower : state.ctLower,
- ctUpper: ctUpper ? ctUpper : state.ctUpper,
- ptLower: ptLower ? ptLower : state.ptLower,
- ptUpper: ptUpper ? ptUpper : state.ptUpper,
- };
- case 'setWeight':
- return {
- ...state,
- weight,
- };
- default:
- return state;
- }
-}
-
-export default function PanelRoiThresholdSegmentation({ servicesManager, commandsManager }) {
- const { segmentationService } = servicesManager.services;
-
- const { t } = useTranslation('PanelSUV');
- const [showConfig, setShowConfig] = useState(false);
- const [labelmapLoading, setLabelmapLoading] = useState(false);
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
- const [config, dispatch] = useReducer(reducer, {
- strategy: DEFAULT_STRATEGY,
- ctLower: LOWER_CT_THRESHOLD_DEFAULT,
- ctUpper: UPPER_CT_THRESHOLD_DEFAULT,
- ptLower: LOWER_PT_THRESHOLD_DEFAULT,
- ptUpper: UPPER_PT_THRESHOLD_DEFAULT,
- weight: WEIGHT_DEFAULT,
- });
-
- const [tmtvValue, setTmtvValue] = useState(null);
-
const runCommand = useCallback(
(commandName, commandOptions = {}) => {
return commandsManager.runCommand(commandName, commandOptions);
@@ -69,46 +25,6 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
[commandsManager]
);
- const handleTMTVCalculation = useCallback(() => {
- const tmtv = runCommand('calculateTMTV', { segmentations });
-
- if (tmtv !== undefined) {
- setTmtvValue(tmtv.toFixed(2));
- }
- }, [segmentations, runCommand]);
-
- const handleROIThresholding = useCallback(() => {
- const labelmap = runCommand('thresholdSegmentationByRectangleROITool', {
- segmentationId: selectedSegmentationId,
- config,
- });
-
- const lesionStats = runCommand('getLesionStats', { labelmap });
- const suvPeak = runCommand('calculateSuvPeak', { labelmap });
- const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
-
- // update segDetails with the suv peak for the active segmentation
- const segmentation = segmentationService.getSegmentation(selectedSegmentationId);
-
- const cachedStats = {
- lesionStats,
- suvPeak,
- lesionGlyoclysisStats,
- };
-
- const notYetUpdatedAtSource = true;
- segmentationService.addOrUpdateSegmentation(
- {
- ...segmentation,
- ...Object.assign(segmentation.cachedStats, cachedStats),
- displayText: [`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`],
- },
- notYetUpdatedAtSource
- );
-
- handleTMTVCalculation();
- }, [selectedSegmentationId, config]);
-
/**
* Update UI based on segmentation changes (added, removed, updated)
*/
@@ -116,9 +32,10 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
// ~~ Subscription
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
+ const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
const subscriptions = [];
- [added, updated].forEach(evt => {
+ [added, updated, removed].forEach(evt => {
const { unsubscribe } = segmentationService.subscribe(evt, () => {
const segmentations = segmentationService.getSegmentations();
setSegmentations(segmentations);
@@ -133,109 +50,222 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
};
}, []);
- useEffect(() => {
- const { unsubscribe } = segmentationService.subscribe(
- segmentationService.EVENTS.SEGMENTATION_REMOVED,
- () => {
- const segmentations = segmentationService.getSegmentations();
- setSegmentations(segmentations);
+ const onSegmentationClick = (segmentationId: string) => {
+ segmentationService.setActiveSegmentationForToolGroup(segmentationId);
+ setSelectedSegmentationId(segmentationId);
+ };
- if (segmentations.length > 0) {
- setSelectedSegmentationId(segmentations[0].id);
- handleTMTVCalculation();
- } else {
- setSelectedSegmentationId(null);
- setTmtvValue(null);
- }
+ const onSegmentationAdd = async () => {
+ runCommand('createNewLabelmapFromPT').then(segmentationId => {
+ setSelectedSegmentationId(segmentationId);
+ });
+ };
+
+ const onSegmentAdd = segmentationId => {
+ segmentationService.addSegment(segmentationId);
+ };
+
+ const getToolGroupIds = segmentationId => {
+ const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
+
+ return toolGroupIds;
+ };
+
+ const onSegmentClick = (segmentationId, segmentIndex) => {
+ segmentationService.setActiveSegment(segmentationId, segmentIndex);
+
+ const toolGroupIds = getToolGroupIds(segmentationId);
+
+ toolGroupIds.forEach(toolGroupId => {
+ // const toolGroupId =
+ segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
+ segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
+ });
+ };
+
+ const _setSegmentationConfiguration = useCallback(
+ (segmentationId, key, value) => {
+ segmentationService.setConfiguration({
+ segmentationId,
+ [key]: value,
+ });
+ },
+ [segmentationService]
+ );
+
+ const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
+ const segmentation = segmentationService.getSegmentation(segmentationId);
+ const segmentInfo = segmentation.segments[segmentIndex];
+ const isVisible = !segmentInfo.isVisible;
+ const toolGroupIds = getToolGroupIds(segmentationId);
+ toolGroupIds.forEach(toolGroupId => {
+ segmentationService.setSegmentVisibility(
+ segmentationId,
+ segmentIndex,
+ isVisible,
+ toolGroupId
+ );
+ });
+ };
+
+ const onSegmentDelete = (segmentationId, segmentIndex) => {
+ segmentationService.removeSegment(segmentationId, segmentIndex);
+ };
+
+ const onSegmentEdit = (segmentationId, segmentIndex) => {
+ const segmentation = segmentationService.getSegmentation(segmentationId);
+
+ const segment = segmentation.segments[segmentIndex];
+ const { label } = segment;
+
+ callInputDialog(uiDialogService, label, (label, actionId) => {
+ if (label === '') {
+ return;
}
- );
- return () => {
- unsubscribe();
+ segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
+ });
+ };
+
+ const onToggleSegmentLock = (segmentationId, segmentIndex) => {
+ segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
+ };
+
+ const onSegmentColorClick = (segmentationId, segmentIndex) => {
+ const segmentation = segmentationService.getSegmentation(segmentationId);
+
+ const segment = segmentation.segments[segmentIndex];
+ const { color, opacity } = segment;
+
+ const rgbaColor = {
+ r: color[0],
+ g: color[1],
+ b: color[2],
+ a: opacity / 255.0,
};
- }, []);
- /**
- * Whenever the segmentations change, update the TMTV calculations
- */
- useEffect(() => {
- if (!selectedSegmentationId && segmentations.length > 0) {
- setSelectedSegmentationId(segmentations[0].id);
+ callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
+ if (actionId === 'cancel') {
+ return;
+ }
+
+ segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [
+ newRgbaColor.r,
+ newRgbaColor.g,
+ newRgbaColor.b,
+ newRgbaColor.a * 255.0,
+ ]);
+ });
+ };
+
+ const storeSegmentation = async segmentationId => {
+ const datasources = extensionManager.getActiveDataSource();
+
+ const displaySetInstanceUIDs = await createReportAsync({
+ servicesManager,
+ getReport: () =>
+ commandsManager.runCommand('storeSegmentation', {
+ segmentationId,
+ dataSource: datasources[0],
+ }),
+ reportType: 'Segmentation',
+ });
+
+ // Show the exported report in the active viewport as read only (similar to SR)
+ if (displaySetInstanceUIDs) {
+ // clear the segmentation that we exported, similar to the storeMeasurement
+ // where we remove the measurements and prompt again the user if they would like
+ // to re-read the measurements in a SR read only viewport
+ segmentationService.remove(segmentationId);
+
+ viewportGridService.setDisplaySetsForViewport({
+ viewportId: viewportGridService.getActiveViewportId(),
+ displaySetInstanceUIDs,
+ });
}
+ };
- handleTMTVCalculation();
- }, [segmentations, selectedSegmentationId]);
+ const onSegmentationDownloadRTSS = segmentationId => {
+ commandsManager.runCommand('downloadRTSS', {
+ segmentationId,
+ });
+ };
+
+ const onSegmentationDownload = segmentationId => {
+ commandsManager.runCommand('downloadSegmentation', {
+ segmentationId,
+ });
+ };
+
+ const tmtvValue = segmentations?.[0]?.cachedStats?.tmtv?.value || null;
+ const config = segmentations?.[0]?.cachedStats?.tmtv?.config || {};
return (
<>
-
-
{
- setLabelmapLoading(true);
- setTimeout(() => {
- runCommand('createNewLabelmapFromPT').then(segmentationId => {
- setLabelmapLoading(false);
- setSelectedSegmentationId(segmentationId);
- });
+ {/* show segmentation table */}
+
+ {
+ segmentationService.toggleSegmentationVisibility(id);
+ }}
+ onToggleSegmentVisibility={onToggleSegmentVisibility}
+ onSegmentationDelete={id => {
+ segmentationService.remove(id);
+ }}
+ onSegmentationEdit={id => {
+ segmentationEditHandler({
+ id,
+ servicesManager,
});
}}
- >
- {labelmapLoading ? 'loading ...' : 'New Label'}
-
- Run
-
- {
- setShowConfig(!showConfig);
- }}
- >
-
{t('ROI Threshold Configuration')}
-
- {showConfig && (
-
+ _setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
+ }
+ setOutlineOpacityActive={value =>
+ _setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
+ }
+ setRenderFill={value =>
+ _setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
+ }
+ setRenderInactiveSegmentations={value =>
+ _setSegmentationConfiguration(
+ selectedSegmentationId,
+ 'renderInactiveSegmentations',
+ value
+ )
+ }
+ setOutlineWidthActive={value =>
+ _setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
+ }
+ setFillAlpha={value =>
+ _setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
+ }
+ setFillAlphaInactive={value =>
+ _setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
+ }
/>
- )}
- {/* show segmentation table */}
-
- {segmentations?.length ? (
- {
- runCommand('setSegmentationActiveForToolGroups', {
- segmentationId: id,
- });
- setSelectedSegmentationId(id);
- }}
- onToggleVisibility={id => {
- segmentationService.toggleSegmentationVisibility(id);
- }}
- onToggleVisibilityAll={ids => {
- ids.map(id => {
- segmentationService.toggleSegmentationVisibility(id);
- });
- }}
- onDelete={id => {
- segmentationService.remove(id);
- }}
- onEdit={id => {
- segmentationEditHandler({
- id,
- servicesManager,
- });
- }}
- />
- ) : null}
{tmtvValue !== null ? (
-
+
{'TMTV:'}
@@ -251,7 +281,7 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
{
// navigate to a url in a new tab
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx
index 274ae720d..1f650b1c2 100644
--- a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx
+++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx
@@ -14,7 +14,7 @@ function ROIThresholdConfiguration({ config, dispatch, runCommand }) {
const { t } = useTranslation('ROIThresholdConfiguration');
return (
-
+
diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/callInputDialog.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/callInputDialog.tsx
new file mode 100644
index 000000000..6de2470d8
--- /dev/null
+++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/callInputDialog.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { Input, Dialog, ButtonEnums } from '@ohif/ui';
+
+function callInputDialog(uiDialogService, label, callback) {
+ const dialogId = 'enter-segment-label';
+
+ const onSubmitHandler = ({ action, value }) => {
+ switch (action.id) {
+ case 'save':
+ callback(value.label, action.id);
+ break;
+ case 'cancel':
+ callback('', action.id);
+ break;
+ }
+ uiDialogService.dismiss({ id: dialogId });
+ };
+
+ if (uiDialogService) {
+ uiDialogService.create({
+ id: dialogId,
+ centralize: true,
+ isDraggable: false,
+ showOverlay: true,
+ content: Dialog,
+ contentProps: {
+ title: 'Segment',
+ value: { label },
+ noCloseButton: true,
+ onClose: () => uiDialogService.dismiss({ id: dialogId }),
+ actions: [
+ { id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
+ { id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
+ ],
+ onSubmit: onSubmitHandler,
+ body: ({ value, setValue }) => {
+ return (
+ {
+ event.persist();
+ setValue(value => ({ ...value, label: event.target.value }));
+ }}
+ onKeyPress={event => {
+ if (event.key === 'Enter') {
+ onSubmitHandler({ value, action: { id: 'save' } });
+ }
+ }}
+ />
+ );
+ },
+ },
+ });
+ }
+}
+
+export default callInputDialog;
diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/colorPickerDialog.css b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/colorPickerDialog.css
new file mode 100644
index 000000000..1c6bb2067
--- /dev/null
+++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/colorPickerDialog.css
@@ -0,0 +1,3 @@
+.chrome-picker {
+ background: #090c29 !important;
+}
diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/colorPickerDialog.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/colorPickerDialog.tsx
new file mode 100644
index 000000000..38e85efb2
--- /dev/null
+++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/colorPickerDialog.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { Dialog } from '@ohif/ui';
+import { ChromePicker } from 'react-color';
+
+import './colorPickerDialog.css';
+
+function callColorPickerDialog(uiDialogService, rgbaColor, callback) {
+ const dialogId = 'pick-color';
+
+ const onSubmitHandler = ({ action, value }) => {
+ switch (action.id) {
+ case 'save':
+ callback(value.rgbaColor, action.id);
+ break;
+ case 'cancel':
+ callback('', action.id);
+ break;
+ }
+ uiDialogService.dismiss({ id: dialogId });
+ };
+
+ if (uiDialogService) {
+ uiDialogService.create({
+ id: dialogId,
+ centralize: true,
+ isDraggable: false,
+ showOverlay: true,
+ content: Dialog,
+ contentProps: {
+ title: 'Segment Color',
+ value: { rgbaColor },
+ noCloseButton: true,
+ onClose: () => uiDialogService.dismiss({ id: dialogId }),
+ actions: [
+ { id: 'cancel', text: 'Cancel', type: 'primary' },
+ { id: 'save', text: 'Save', type: 'secondary' },
+ ],
+ onSubmit: onSubmitHandler,
+ body: ({ value, setValue }) => {
+ const handleChange = color => {
+ setValue({ rgbaColor: color.rgb });
+ };
+
+ return (
+
+ );
+ },
+ },
+ });
+ }
+}
+
+export default callColorPickerDialog;
diff --git a/extensions/tmtv/src/Panels/RectangleROIOptions.tsx b/extensions/tmtv/src/Panels/RectangleROIOptions.tsx
new file mode 100644
index 000000000..602bb5531
--- /dev/null
+++ b/extensions/tmtv/src/Panels/RectangleROIOptions.tsx
@@ -0,0 +1,214 @@
+import React, { useState, useCallback, useReducer, useEffect } from 'react';
+import { Button } from '@ohif/ui';
+import ROIThresholdConfiguration, {
+ ROI_STAT,
+} from './PanelROIThresholdSegmentation/ROIThresholdConfiguration';
+import * as cs3dTools from '@cornerstonejs/tools';
+
+const LOWER_CT_THRESHOLD_DEFAULT = -1024;
+const UPPER_CT_THRESHOLD_DEFAULT = 1024;
+const LOWER_PT_THRESHOLD_DEFAULT = 2.5;
+const UPPER_PT_THRESHOLD_DEFAULT = 100;
+const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature
+const DEFAULT_STRATEGY = ROI_STAT;
+
+function reducer(state, action) {
+ const { payload } = action;
+ const { strategy, ctLower, ctUpper, ptLower, ptUpper, weight } = payload;
+
+ switch (action.type) {
+ case 'setStrategy':
+ return {
+ ...state,
+ strategy,
+ };
+ case 'setThreshold':
+ return {
+ ...state,
+ ctLower: ctLower ? ctLower : state.ctLower,
+ ctUpper: ctUpper ? ctUpper : state.ctUpper,
+ ptLower: ptLower ? ptLower : state.ptLower,
+ ptUpper: ptUpper ? ptUpper : state.ptUpper,
+ };
+ case 'setWeight':
+ return {
+ ...state,
+ weight,
+ };
+ default:
+ return state;
+ }
+}
+
+function RectangleROIOptions({ servicesManager, commandsManager }) {
+ const { segmentationService } = servicesManager.services;
+ const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
+
+ const runCommand = useCallback(
+ (commandName, commandOptions = {}) => {
+ return commandsManager.runCommand(commandName, commandOptions);
+ },
+ [commandsManager]
+ );
+
+ const [config, dispatch] = useReducer(reducer, {
+ strategy: DEFAULT_STRATEGY,
+ ctLower: LOWER_CT_THRESHOLD_DEFAULT,
+ ctUpper: UPPER_CT_THRESHOLD_DEFAULT,
+ ptLower: LOWER_PT_THRESHOLD_DEFAULT,
+ ptUpper: UPPER_PT_THRESHOLD_DEFAULT,
+ weight: WEIGHT_DEFAULT,
+ });
+
+ const handleROIThresholding = useCallback(() => {
+ const segmentationId = selectedSegmentationId;
+
+ const segmentation = segmentationService.getSegmentation(segmentationId);
+ const activeSegmentIndex =
+ cs3dTools.segmentation.segmentIndex.getActiveSegmentIndex(segmentationId);
+
+ // run the threshold based on the active segment index
+ // Todo: later find a way to associate each rectangle with a segment (e.g., maybe with color?)
+ const labelmap = runCommand('thresholdSegmentationByRectangleROITool', {
+ segmentationId,
+ config,
+ segmentIndex: activeSegmentIndex,
+ });
+
+ // re-calculating the cached stats for the active segmentation
+ const updatedPerSegmentCachedStats = {};
+ segmentation.segments = segmentation.segments.map(segment => {
+ if (!segment || !segment.segmentIndex) {
+ return segment;
+ }
+
+ const segmentIndex = segment.segmentIndex;
+
+ const lesionStats = runCommand('getLesionStats', { labelmap, segmentIndex });
+ const suvPeak = runCommand('calculateSuvPeak', { labelmap, segmentIndex });
+ const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
+
+ // update segDetails with the suv peak for the active segmentation
+ const cachedStats = {
+ lesionStats,
+ suvPeak,
+ lesionGlyoclysisStats,
+ };
+
+ segment.cachedStats = cachedStats;
+ segment.displayText = [
+ `SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`,
+ `Volume: ${lesionStats.volume.toFixed(2)} mm3`,
+ ];
+ updatedPerSegmentCachedStats[segmentIndex] = cachedStats;
+
+ return segment;
+ });
+
+ const notYetUpdatedAtSource = true;
+
+ const segmentations = segmentationService.getSegmentations();
+ const tmtv = runCommand('calculateTMTV', { segmentations });
+
+ segmentation.cachedStats = Object.assign(
+ segmentation.cachedStats,
+ updatedPerSegmentCachedStats,
+ {
+ tmtv: {
+ value: tmtv.toFixed(3),
+ config: { ...config },
+ },
+ }
+ );
+
+ segmentationService.addOrUpdateSegmentation(
+ {
+ ...segmentation,
+ },
+ false, // don't suppress events
+ notYetUpdatedAtSource
+ );
+ }, [selectedSegmentationId, config]);
+
+ useEffect(() => {
+ const segmentations = segmentationService.getSegmentations();
+
+ if (!segmentations.length) {
+ return;
+ }
+
+ const isActive = segmentations.find(seg => seg.isActive);
+ setSelectedSegmentationId(isActive.id);
+ }, []);
+
+ /**
+ * Update UI based on segmentation changes (added, removed, updated)
+ */
+ useEffect(() => {
+ // ~~ Subscription
+ const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
+ const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
+ const subscriptions = [];
+
+ [added, updated].forEach(evt => {
+ const { unsubscribe } = segmentationService.subscribe(evt, () => {
+ const segmentations = segmentationService.getSegmentations();
+
+ if (!segmentations.length) {
+ return;
+ }
+
+ const isActive = segmentations.find(seg => seg.isActive);
+ setSelectedSegmentationId(isActive.id);
+ });
+ subscriptions.push(unsubscribe);
+ });
+
+ return () => {
+ subscriptions.forEach(unsub => {
+ unsub();
+ });
+ };
+ }, []);
+
+ useEffect(() => {
+ const { unsubscribe } = segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_REMOVED,
+ () => {
+ const segmentations = segmentationService.getSegmentations();
+
+ if (segmentations.length > 0) {
+ setSelectedSegmentationId(segmentations[0].id);
+ handleROIThresholding();
+ } else {
+ setSelectedSegmentationId(null);
+ handleROIThresholding();
+ }
+ }
+ );
+
+ return () => {
+ unsubscribe();
+ };
+ }, []);
+
+ return (
+
+
+ {selectedSegmentationId !== null && (
+
+ Run
+
+ )}
+
+ );
+}
+
+export default RectangleROIOptions;
diff --git a/extensions/tmtv/src/commandsModule.js b/extensions/tmtv/src/commandsModule.js
index d00b33c89..765ab7f5b 100644
--- a/extensions/tmtv/src/commandsModule.js
+++ b/extensions/tmtv/src/commandsModule.js
@@ -112,6 +112,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
// Create a segmentation of the same resolution as the source data
// using volumeLoader.createAndCacheDerivedVolume.
const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
+
const ptDisplaySet = actions.getMatchingPTDisplaySet({
viewportMatchDetails,
});
@@ -121,8 +122,11 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
return;
}
+ const currentSegmentations = segmentationService.getSegmentations();
+
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
- ptDisplaySet.displaySetInstanceUID
+ ptDisplaySet.displaySetInstanceUID,
+ { label: `Segmentation ${currentSegmentations.length + 1}` }
);
// Add Segmentation to all toolGroupIds in the viewer
@@ -141,6 +145,12 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
}
+ segmentationService.addSegment(segmentationId, {
+ segmentIndex: 1,
+ properties: {
+ label: 'Segment 1',
+ },
+ });
return segmentationId;
},
setSegmentationActiveForToolGroups: ({ segmentationId }) => {
@@ -150,7 +160,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
});
},
- thresholdSegmentationByRectangleROITool: ({ segmentationId, config }) => {
+ thresholdSegmentationByRectangleROITool: ({ segmentationId, config, segmentIndex }) => {
const segmentation = csTools.segmentation.state.getSegmentation(segmentationId);
const { representationData } = segmentation;
@@ -201,12 +211,11 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
{ volume: referencedVolume, lower: ptLower, upper: ptUpper },
{ volume: ctReferencedVolume, lower: ctLower, upper: ctUpper },
],
- { overwrite: true }
+ { overwrite: true, segmentIndex }
);
},
- calculateSuvPeak: ({ labelmap }) => {
+ calculateSuvPeak: ({ labelmap, segmentIndex }) => {
const { referencedVolumeId } = labelmap;
-
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
const annotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
@@ -217,7 +226,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
csTools.annotation.state.getAnnotation(annotationUID)
);
- const suvPeak = calculateSuvPeak(labelmap, referencedVolume, annotations);
+ const suvPeak = calculateSuvPeak(labelmap, referencedVolume, annotations, segmentIndex);
return {
suvPeak: suvPeak.mean,
suvMax: suvPeak.max,
diff --git a/extensions/tmtv/src/getPanelModule.tsx b/extensions/tmtv/src/getPanelModule.tsx
index 0449ebe48..39a955df3 100644
--- a/extensions/tmtv/src/getPanelModule.tsx
+++ b/extensions/tmtv/src/getPanelModule.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { PanelPetSUV, PanelROIThresholdSegmentation } from './Panels';
+import { Toolbox } from '@ohif/ui';
// TODO:
// - No loading UI exists yet
@@ -12,18 +13,26 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
);
};
const wrappedROIThresholdSeg = () => {
return (
-
+ <>
+
+
+ >
);
};
@@ -31,15 +40,15 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
{
name: 'petSUV',
iconName: 'tab-patient-info',
- iconLabel: 'PET SUV',
- label: 'PET SUV',
+ iconLabel: 'Patient Info',
+ label: 'Patient Info',
component: wrappedPanelPetSuv,
},
{
name: 'ROIThresholdSeg',
- iconName: 'tab-roi-threshold',
- iconLabel: 'ROI Threshold',
- label: 'ROI Threshold',
+ iconName: 'tab-segmentation',
+ iconLabel: 'Segmentation',
+ label: 'Segmentation',
component: wrappedROIThresholdSeg,
},
];
diff --git a/extensions/tmtv/src/getToolbarModule.tsx b/extensions/tmtv/src/getToolbarModule.tsx
new file mode 100644
index 000000000..33c178bd9
--- /dev/null
+++ b/extensions/tmtv/src/getToolbarModule.tsx
@@ -0,0 +1,10 @@
+import RectangleROIOptions from './Panels/RectangleROIOptions';
+
+export default function getToolbarModule({ commandsManager, servicesManager }) {
+ return [
+ {
+ name: 'tmtv.RectangleROIThresholdOptions',
+ defaultComponent: () => RectangleROIOptions({ commandsManager, servicesManager }),
+ },
+ ];
+}
diff --git a/extensions/tmtv/src/index.tsx b/extensions/tmtv/src/index.tsx
index 3922955bb..9d853c996 100644
--- a/extensions/tmtv/src/index.tsx
+++ b/extensions/tmtv/src/index.tsx
@@ -3,6 +3,7 @@ import getHangingProtocolModule from './getHangingProtocolModule';
import getPanelModule from './getPanelModule';
import init from './init';
import commandsModule from './commandsModule';
+import getToolbarModule from './getToolbarModule';
/**
*
@@ -15,6 +16,7 @@ const tmtvExtension = {
preRegistration({ servicesManager, commandsManager, extensionManager, configuration = {} }) {
init({ servicesManager, commandsManager, extensionManager, configuration });
},
+ getToolbarModule,
getPanelModule,
getHangingProtocolModule,
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js b/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js
index a6131e2ac..539f45f85 100644
--- a/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js
+++ b/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js
@@ -43,8 +43,6 @@ const RectangleROIStartEndThreshold = {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
- const { cachedStats } = data;
-
return {
uid: annotationUID,
SOPInstanceUID,
@@ -56,10 +54,8 @@ const RectangleROIStartEndThreshold = {
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: metadata.label,
- // displayText: displayText,
data: data.cachedStats,
type: 'RectangleROIStartEndThreshold',
- // getReport,
};
},
};
diff --git a/modes/longitudinal/src/toolbarButtons.ts b/modes/longitudinal/src/toolbarButtons.ts
index bce66af83..ebb6b7654 100644
--- a/modes/longitudinal/src/toolbarButtons.ts
+++ b/modes/longitudinal/src/toolbarButtons.ts
@@ -119,7 +119,10 @@ const toolbarButtons: Button[] = [
icon: 'tool-3d-rotate',
label: '3D Rotate',
commands: setToolActiveToolbar,
- evaluate: 'evaluate.cornerstoneTool',
+ evaluate: {
+ name: 'evaluate.cornerstoneTool',
+ disabledText: 'Select a 3D viewport to enable this tool',
+ },
},
},
{
@@ -154,7 +157,10 @@ const toolbarButtons: Button[] = [
toolGroupIds: ['mpr'],
},
},
- evaluate: 'evaluate.cornerstoneTool',
+ evaluate: {
+ name: 'evaluate.cornerstoneTool',
+ disabledText: 'Select an MPR viewport to enable this tool',
+ },
},
},
];
diff --git a/modes/segmentation/src/segmentationButtons.ts b/modes/segmentation/src/segmentationButtons.ts
index 012988ea2..51de3487d 100644
--- a/modes/segmentation/src/segmentationButtons.ts
+++ b/modes/segmentation/src/segmentationButtons.ts
@@ -25,6 +25,7 @@ const toolbarButtons: Button[] = [
evaluate: {
name: 'evaluate.cornerstone.segmentation',
options: { toolNames: ['CircularBrush', 'SphereBrush'] },
+ disabledText: 'Create new segmentation to enable this tool.',
},
commands: _createSetToolActiveCommands('CircularBrush'),
options: [
@@ -95,7 +96,7 @@ const toolbarButtons: Button[] = [
{
id: 'Threshold',
icon: 'icon-tool-threshold',
- label: 'Eraser',
+ label: 'Threshold Tool',
evaluate: {
name: 'evaluate.cornerstone.segmentation',
options: { toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'] },
diff --git a/modes/segmentation/src/toolbarButtons.ts b/modes/segmentation/src/toolbarButtons.ts
index 3255570d1..b732aff8e 100644
--- a/modes/segmentation/src/toolbarButtons.ts
+++ b/modes/segmentation/src/toolbarButtons.ts
@@ -75,7 +75,10 @@ const toolbarButtons: Button[] = [
icon: 'tool-3d-rotate',
label: '3D Rotate',
commands: setToolActiveToolbar,
- evaluate: 'evaluate.cornerstoneTool',
+ evaluate: {
+ name: 'evaluate.cornerstoneTool',
+ disabledText: 'Select a 3D viewport to enable this tool',
+ },
},
},
{
@@ -110,7 +113,10 @@ const toolbarButtons: Button[] = [
toolGroupIds: ['mpr'],
},
},
- evaluate: 'evaluate.cornerstoneTool',
+ evaluate: {
+ name: 'evaluate.cornerstoneTool',
+ disabledText: 'Select an MPR viewport to enable this tool',
+ },
},
},
{
diff --git a/modes/tmtv/src/index.js b/modes/tmtv/src/index.js
index 9643fa971..e3cbb1967 100644
--- a/modes/tmtv/src/index.js
+++ b/modes/tmtv/src/index.js
@@ -89,8 +89,8 @@ function modeFactory({ modeConfiguration }) {
'Crosshairs',
'Pan',
'SyncToggle',
- 'RectangleROIStartEndThreshold',
]);
+ toolbarService.createButtonSection('tmtvToolbox', ['RectangleROIStartEndThreshold']);
// For the hanging protocol we need to decide on the window level
// based on whether the SUV is corrected or not, hence we can't hard
diff --git a/modes/tmtv/src/toolbarButtons.js b/modes/tmtv/src/toolbarButtons.js
index c605b4966..042a54b59 100644
--- a/modes/tmtv/src/toolbarButtons.js
+++ b/modes/tmtv/src/toolbarButtons.js
@@ -1,42 +1,8 @@
import { defaults, ToolbarService } from '@ohif/core';
-import { WindowLevelMenuItem } from '@ohif/ui';
import { toolGroupIds } from './initToolGroups';
const { windowLevelPresets } = defaults;
-function _createColormap(label, colormap) {
- return {
- id: label,
- label,
- type: 'action',
- commands: [
- {
- commandName: 'setFusionPTColormap',
- commandOptions: {
- toolGroupId: toolGroupIds.Fusion,
- colormap,
- },
- },
- ],
- };
-}
-function _createWwwcPreset(preset, title, subtitle) {
- return {
- id: preset.toString(),
- title,
- subtitle,
- commands: [
- {
- commandName: 'setWindowLevel',
- commandOptions: {
- ...windowLevelPresets[preset],
- },
- context: 'CORNERSTONE',
- },
- ],
- };
-}
-
const setToolActiveToolbar = {
commandName: 'setToolActiveToolbar',
commandOptions: {
@@ -141,7 +107,11 @@ const toolbarButtons = [
icon: 'tool-create-threshold',
label: 'Rectangle ROI Threshold',
commands: setToolActiveToolbar,
- evaluate: 'evaluate.cornerstoneTool',
+ evaluate: {
+ name: 'evaluate.cornerstoneTool',
+ disabledText: 'Select the PT Axial to enable this tool',
+ },
+ options: 'tmtv.RectangleROIThresholdOptions',
},
},
];
diff --git a/platform/app/public/html-templates/index.html b/platform/app/public/html-templates/index.html
index eb53fc851..dd61a8af9 100644
--- a/platform/app/public/html-templates/index.html
+++ b/platform/app/public/html-templates/index.html
@@ -233,7 +233,8 @@
You need to enable JavaScript to run this app.
-
-
+
+
+