feat(4D): Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode (#3664)

Co-authored-by: Alireza <ar.sedghi@gmail.com>
Co-authored-by: Neil <neil.a.macphee@gmail.com>
This commit is contained in:
authored and GitHub committed 2024-04-09 23:57:34 -04:00
1 parent dc0b183dd4
commit d57e8bc157
200 files changed
+9589 -2178

No files matched your search

@@ -1,51 +0,0 @@
import React from 'react';
import { LegacyButton, LegacyButtonGroup } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
function ExportReports({ segmentations, tmtvValue, config, commandsManager }) {
const { t } = useTranslation('PanelSUVExport');
return (
<>
{segmentations?.length ? (
<div className="mt-4 flex justify-center space-x-2">
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
<LegacyButtonGroup
color="black"
size="inherit"
>
<LegacyButton
className="px-2 py-2 text-base"
disabled={tmtvValue === null}
onClick={() => {
commandsManager.runCommand('exportTMTVReportCSV', {
segmentations,
tmtv: tmtvValue,
config,
});
}}
>
{t('Export CSV')}
</LegacyButton>
</LegacyButtonGroup>
<LegacyButtonGroup
color="black"
size="inherit"
>
<LegacyButton
className="px-2 py-2 text-base"
onClick={() => {
commandsManager.runCommand('createTMTVRTReport');
}}
disabled={tmtvValue === null}
>
{t('Create RT Report')}
</LegacyButton>
</LegacyButtonGroup>
</div>
) : null}
</>
);
}
export default ExportReports;
@@ -1,287 +0,0 @@
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 (
<>
<div className="flex flex-col">
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
<div className="mx-4 my-4 mb-4 flex space-x-4">
<Button
onClick={() => {
setLabelmapLoading(true);
setTimeout(() => {
runCommand('createNewLabelmapFromPT').then(segmentationId => {
setLabelmapLoading(false);
setSelectedSegmentationId(segmentationId);
});
});
}}
>
{labelmapLoading ? 'loading ...' : 'New Label'}
</Button>
<Button onClick={handleROIThresholding}>Run</Button>
</div>
<div
className="bg-secondary-dark border-secondary-light mb-2 flex h-8 cursor-pointer select-none items-center justify-around border-t outline-none first:border-0"
onClick={() => {
setShowConfig(!showConfig);
}}
>
<div className="px-4 text-base text-white">{t('ROI Threshold Configuration')}</div>
</div>
{showConfig && (
<ROIThresholdConfiguration
config={config}
dispatch={dispatch}
runCommand={runCommand}
/>
)}
{/* show segmentation table */}
<div className="mt-4">
{segmentations?.length ? (
<SegmentationTable
title={t('Segmentations')}
segmentations={segmentations}
activeSegmentationId={selectedSegmentationId}
onClick={id => {
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}
</div>
{tmtvValue !== null ? (
<div className="bg-secondary-dark mt-4 flex items-baseline justify-between px-2 py-1">
<span className="text-base font-bold uppercase tracking-widest text-white">
{'TMTV:'}
</span>
<div className="text-white">{`${tmtvValue} mL`}</div>
</div>
) : null}
<ExportReports
segmentations={segmentations}
tmtvValue={tmtvValue}
config={config}
commandsManager={commandsManager}
/>
</div>
</div>
<div
className="mt-auto mb-4 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
onClick={() => {
// navigate to a url in a new tab
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
}}
>
<Icon
width="15px"
height="15px"
name={'info'}
className={'text-primary-active ml-4 mr-3'}
/>
<span>{'User Guide'}</span>
</div>
</>
);
}
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,
};
@@ -0,0 +1,114 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Icon, ActionButtons } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
export default function PanelRoiThresholdSegmentation({ servicesManager, commandsManager }) {
const { segmentationService } = servicesManager.services;
const { t } = useTranslation('PanelSUVExport');
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
/**
* Update UI based on segmentation changes (added, removed, updated)
*/
useEffect(() => {
// ~~ Subscription
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
const subscriptions = [];
[added, updated, removed].forEach(evt => {
const { unsubscribe } = segmentationService.subscribe(evt, () => {
const segmentations = segmentationService.getSegmentations();
setSegmentations(segmentations);
});
subscriptions.push(unsubscribe);
});
return () => {
subscriptions.forEach(unsub => {
unsub();
});
};
}, []);
const tmtvValue = segmentations?.[0]?.cachedStats?.tmtv?.value || null;
const config = segmentations?.[0]?.cachedStats?.tmtv?.config || {};
const actions = [
{
label: 'Export CSV',
onClick: () => {
commandsManager.runCommand('exportTMTVReportCSV', {
segmentations,
tmtv: tmtvValue,
config,
});
},
disabled: tmtvValue === null,
},
{
label: 'Create RT Report',
onClick: () => {
commandsManager.runCommand('createTMTVRTReport');
},
disabled: tmtvValue === null,
},
];
return (
<>
<div className="mt-1 flex flex-col">
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
{tmtvValue !== null ? (
<div className="bg-secondary-dark mt-1 flex items-baseline justify-between px-2 py-1">
<span className="text-base font-bold uppercase tracking-widest text-white">
{'TMTV:'}
</span>
<div className="text-white">{`${tmtvValue} mL`}</div>
</div>
) : null}
<div className="mt-1 flex justify-center">
<ActionButtons
actions={actions}
t={t}
/>
</div>
</div>
</div>
<div
className="absolute bottom-1 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
onClick={() => {
// navigate to a url in a new tab
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
}}
>
<Icon
width="15px"
height="15px"
name={'info'}
className={'text-primary-active ml-4 mr-3'}
/>
<span>{'User Guide'}</span>
</div>
</>
);
}
PanelRoiThresholdSegmentation.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,
};
@@ -1,317 +0,0 @@
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { SegmentationGroupTableExpanded, Icon } from '@ohif/ui';
import { createReportAsync } from '@ohif/extension-default';
import segmentationEditHandler from './segmentationEditHandler';
import ExportReports from './ExportReports';
import callInputDialog from './callInputDialog';
import callColorPickerDialog from './colorPickerDialog';
export default function PanelRoiThresholdSegmentation({
servicesManager,
commandsManager,
extensionManager,
}) {
const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
const runCommand = useCallback(
(commandName, commandOptions = {}) => {
return commandsManager.runCommand(commandName, commandOptions);
},
[commandsManager]
);
/**
* Update UI based on segmentation changes (added, removed, updated)
*/
useEffect(() => {
// ~~ Subscription
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
const subscriptions = [];
[added, updated, removed].forEach(evt => {
const { unsubscribe } = segmentationService.subscribe(evt, () => {
const segmentations = segmentationService.getSegmentations();
setSegmentations(segmentations);
});
subscriptions.push(unsubscribe);
});
return () => {
subscriptions.forEach(unsub => {
unsub();
});
};
}, []);
const onSegmentationClick = (segmentationId: string) => {
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
setSelectedSegmentationId(segmentationId);
};
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;
}
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,
};
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,
});
}
};
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 (
<>
<div className="flex flex-col">
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
{/* show segmentation table */}
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
<SegmentationGroupTableExpanded
disableEditing={false}
showAddSegmentation={true}
showAddSegment={false}
showDeleteSegment={true}
segmentations={segmentations}
onSegmentationAdd={onSegmentationAdd}
onSegmentationClick={onSegmentationClick}
onToggleSegmentationVisibility={id => {
segmentationService.toggleSegmentationVisibility(id);
}}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onSegmentationDelete={id => {
segmentationService.remove(id);
}}
onSegmentationEdit={id => {
segmentationEditHandler({
id,
servicesManager,
});
}}
segmentationConfig={{ initialConfig: segmentationService.getConfiguration() }}
onSegmentAdd={onSegmentAdd}
onSegmentClick={onSegmentClick}
onSegmentDelete={onSegmentDelete}
onSegmentEdit={onSegmentEdit}
onToggleSegmentLock={onToggleSegmentLock}
onSegmentColorClick={onSegmentColorClick}
storeSegmentation={storeSegmentation}
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
onSegmentationDownload={onSegmentationDownload}
setRenderOutline={value =>
_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)
}
/>
</div>
{tmtvValue !== null ? (
<div className="bg-secondary-dark mt-1 flex items-baseline justify-between px-2 py-1">
<span className="text-base font-bold uppercase tracking-widest text-white">
{'TMTV:'}
</span>
<div className="text-white">{`${tmtvValue} mL`}</div>
</div>
) : null}
<ExportReports
segmentations={segmentations}
tmtvValue={tmtvValue}
config={config}
commandsManager={commandsManager}
/>
</div>
</div>
<div
className="absolute bottom-1 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
onClick={() => {
// navigate to a url in a new tab
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
}}
>
<Icon
width="15px"
height="15px"
name={'info'}
className={'text-primary-active ml-4 mr-3'}
/>
<span>{'User Guide'}</span>
</div>
</>
);
}
PanelRoiThresholdSegmentation.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,
};
@@ -1,62 +0,0 @@
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 (
<Input
label="Enter the segment label"
labelClassName="text-white text-[14px] leading-[1.2]"
autoFocus
className="border-primary-main bg-black"
type="text"
value={value.label}
onChange={event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
}}
onKeyPress={event => {
if (event.key === 'Enter') {
onSubmitHandler({ value, action: { id: 'save' } });
}
}}
/>
);
},
},
});
}
}
export default callInputDialog;
@@ -1,3 +0,0 @@
.chrome-picker {
background: #090c29 !important;
}
@@ -1,58 +0,0 @@
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 (
<ChromePicker
color={value.rgbaColor}
onChange={handleChange}
presetColors={[]}
width={300}
/>
);
},
},
});
}
}
export default callColorPickerDialog;
@@ -1,3 +1,3 @@
import PanelROIThresholdSegmentation from './PanelROIThresholdSegmentation';
import PanelROIThresholdExport from './PanelROIThresholdExport';
export default PanelROIThresholdSegmentation;
export default PanelROIThresholdExport;
+2 -2
View File
@@ -1,4 +1,4 @@
import PanelPetSUV from './PanelPetSUV';
import PanelROIThresholdSegmentation from './PanelROIThresholdSegmentation';
import PanelROIThresholdExport from './PanelROIThresholdSegmentation';
export { PanelPetSUV, PanelROIThresholdSegmentation };
export { PanelPetSUV, PanelROIThresholdExport };
+92 -32
View File
@@ -11,7 +11,10 @@ import createAndDownloadTMTVReport from './utils/createAndDownloadTMTVReport';
import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructureSet';
const metadataProvider = classes.MetadataProvider;
const RECTANGLE_ROI_THRESHOLD_MANUAL = 'RectangleROIStartEndThreshold';
const RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS = [
'RectangleROIStartEndThreshold',
'RectangleROIThreshold',
];
const LABELMAP = csTools.Enums.SegmentationRepresentations.Labelmap;
const commandsModule = ({ servicesManager, commandsManager, extensionManager }) => {
@@ -52,6 +55,15 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
return toolGroupIds;
}
function _getAnnotationsSelectedByToolNames(toolNames) {
return toolNames.reduce((allAnnotationUIDs, toolName) => {
const annotationUIDs =
csTools.annotation.selection.getAnnotationsSelectedByToolName(toolName);
return allAnnotationUIDs.concat(annotationUIDs);
}, []);
}
const actions = {
getMatchingPTDisplaySet: ({ viewportMatchDetails }) => {
// Todo: this is assuming that the hanging protocol has successfully matched
@@ -108,7 +120,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
return metadata;
},
createNewLabelmapFromPT: async () => {
createNewLabelmapFromPT: async ({ label }) => {
// Create a segmentation of the same resolution as the source data
// using volumeLoader.createAndCacheDerivedVolume.
const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
@@ -173,20 +185,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
const { volumeId: segVolumeId } = representationData[LABELMAP];
const { referencedVolumeId } = cs.cache.getVolume(segVolumeId);
const labelmapVolume = cs.cache.getVolume(segmentationId);
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
const ctReferencedVolume = cs.cache.getVolume(ctVolumeId);
if (!referencedVolume) {
throw new Error('No Reference volume found');
}
if (!labelmapVolume) {
throw new Error('No Reference labelmap found');
}
const annotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
RECTANGLE_ROI_THRESHOLD_MANUAL
const annotationUIDs = _getAnnotationsSelectedByToolNames(
RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS
);
if (annotationUIDs.length === 0) {
@@ -198,6 +198,57 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
return;
}
const labelmapVolume = cs.cache.getVolume(segmentationId);
let referencedVolume = cs.cache.getVolume(referencedVolumeId);
const ctReferencedVolume = cs.cache.getVolume(ctVolumeId);
// check if viewport is
if (!referencedVolume) {
throw new Error('No Reference volume found');
}
if (!labelmapVolume) {
throw new Error('No Reference labelmap found');
}
const annotation = csTools.annotation.state.getAnnotation(annotationUIDs[0]);
const {
metadata: {
enabledElement: { viewport },
},
} = annotation;
const showingReferenceVolume = viewport.hasVolumeId(referencedVolumeId);
if (!showingReferenceVolume) {
// if the reference volume is not being displayed, we can't
// rely on it for thresholding, we have couple of options here
// 1. We choose whatever volume is being displayed
// 2. We check if it is a fusion viewport, we pick the volume
// that matches the size and dimensions of the labelmap. This might
// happen if the 4D PT is converted to a computed volume and displayed
// and wants to threshold the labelmap
// 3. We throw an error
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
viewport.id
);
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
const volume = cs.cache
.getVolumes()
.find(volume => volume.volumeId.includes(displaySetInstanceUID));
if (
cs.utilities.isEqual(volume.dimensions, labelmapVolume.dimensions) &&
cs.utilities.isEqual(volume.spacing, labelmapVolume.spacing)
) {
referencedVolume = volume;
}
});
}
const { ptLower, ptUpper, ctLower, ctUpper } = getThresholdValues(
annotationUIDs,
[referencedVolume, ctReferencedVolume],
@@ -218,8 +269,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
const { referencedVolumeId } = labelmap;
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
const annotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
RECTANGLE_ROI_THRESHOLD_MANUAL
const annotationUIDs = _getAnnotationsSelectedByToolNames(
RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS
);
const annotations = annotationUIDs.map(annotationUID =>
@@ -236,8 +287,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
},
getLesionStats: ({ labelmap, segmentIndex = 1 }) => {
const { scalarData, spacing } = labelmap;
const { scalarData: referencedScalarData } = cs.cache.getVolume(labelmap.referencedVolumeId);
const referencedScalarData = cs.cache.getVolume(labelmap.referencedVolumeId).getScalarData();
let segmentationMax = -Infinity;
let segmentationMin = Infinity;
@@ -287,19 +337,25 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
return calculateTMTV(labelmaps);
},
exportTMTVReportCSV: ({ segmentations, tmtv, config }) => {
exportTMTVReportCSV: ({ segmentations, tmtv, config, options }) => {
const segReport = commandsManager.runCommand('getSegmentationCSVReport', {
segmentations,
});
const tlg = actions.getTotalLesionGlycolysis({ segmentations });
const additionalReportRows = [
{ key: 'Total Metabolic Tumor Volume', value: { tmtv } },
{ key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } },
{ key: 'Threshold Configuration', value: { ...config } },
];
createAndDownloadTMTVReport(segReport, additionalReportRows);
if (tmtv !== undefined) {
additionalReportRows.unshift({
key: 'Total Metabolic Tumor Volume',
value: { tmtv },
});
}
createAndDownloadTMTVReport(segReport, additionalReportRows, options);
},
getTotalLesionGlycolysis: ({ segmentations }) => {
const labelmapVolumes = segmentations.map(s => segmentationService.getLabelmapVolume(s.id));
@@ -323,9 +379,9 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
}
const ptVolume = cs.cache.getVolume(referencedVolumeId);
const mergedLabelData = mergedLabelmap.scalarData;
const mergedLabelData = mergedLabelmap.getScalarData();
if (mergedLabelData.length !== ptVolume.scalarData.length) {
if (mergedLabelData.length !== ptVolume.getScalarData().length) {
console.error(
'commandsModule::getTotalLesionGlycolysis:Labelmap and ptVolume are not the same size'
);
@@ -336,7 +392,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
for (let i = 0; i < mergedLabelData.length; i++) {
// if not background
if (mergedLabelData[i] !== 0) {
suv += ptVolume.scalarData[i];
suv += ptVolume.getScalarData()[i];
totalLesionVoxelCount += 1;
}
}
@@ -351,8 +407,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
const { viewport } = _getActiveViewportsEnabledElement();
const { focalPoint, viewPlaneNormal } = viewport.getCamera();
const selectedAnnotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
RECTANGLE_ROI_THRESHOLD_MANUAL
const selectedAnnotationUIDs = _getAnnotationsSelectedByToolNames(
RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS
);
const annotationUID = selectedAnnotationUIDs[0];
@@ -389,8 +445,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
setEndSliceForROIThresholdTool: () => {
const { viewport } = _getActiveViewportsEnabledElement();
const selectedAnnotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
RECTANGLE_ROI_THRESHOLD_MANUAL
const selectedAnnotationUIDs = _getAnnotationsSelectedByToolNames(
RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS
);
const annotationUID = selectedAnnotationUIDs[0];
@@ -415,7 +471,11 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
Object.keys(stateManager.annotations).forEach(frameOfReferenceUID => {
const forAnnotations = stateManager.annotations[frameOfReferenceUID];
const ROIAnnotations = forAnnotations[RECTANGLE_ROI_THRESHOLD_MANUAL];
const ROIAnnotations = RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS.reduce(
(annotations, toolName) => [...annotations, ...(forAnnotations[toolName] ?? [])],
[]
);
annotations.push(...ROIAnnotations);
});
@@ -483,7 +543,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
report[id] = {
...segReport,
PatientID: instance.PatientID,
PatientID: instance.PatientID ?? '000000',
PatientName: instance.PatientName.Alphabetic,
StudyInstanceUID: instance.StudyInstanceUID,
SeriesInstanceUID: instance.SeriesInstanceUID,
+21 -8
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { PanelPetSUV, PanelROIThresholdSegmentation } from './Panels';
import { PanelPetSUV, PanelROIThresholdExport } from './Panels';
import { Toolbox } from '@ohif/ui';
// TODO:
@@ -17,20 +17,26 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
);
};
const wrappedROIThresholdSeg = () => {
const wrappedROIThresholdToolbox = () => {
return (
<>
<Toolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
buttonSectionId="tmtvToolbox"
buttonSectionId="ROIThresholdToolbox"
title="Threshold Tools"
/>
<PanelROIThresholdSegmentation
</>
);
};
const wrappedROIThresholdExport = () => {
return (
<>
<PanelROIThresholdExport
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
/>
</>
);
@@ -45,11 +51,18 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
component: wrappedPanelPetSuv,
},
{
name: 'ROIThresholdSeg',
name: 'tmtvBox',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedROIThresholdSeg,
label: 'Segmentation Toolbox',
component: wrappedROIThresholdToolbox,
},
{
name: 'tmtvExport',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation Export',
component: wrappedROIThresholdExport,
},
];
}
@@ -40,17 +40,19 @@ function calculateSuvPeak(
return;
}
if (labelmap.scalarData.length !== referenceVolume.scalarData.length) {
const labelmapData = labelmap.getScalarData();
const referenceVolumeData = referenceVolume.getScalarData();
if (labelmapData.length !== referenceVolumeData.length) {
throw new Error('labelmap and referenceVolume must have the same number of pixels');
}
const { scalarData: labelmapData, dimensions, imageData: labelmapImageData } = labelmap;
const { scalarData: referenceVolumeData, imageData: referenceVolumeImageData } = referenceVolume;
const { dimensions, imageData: labelmapImageData } = labelmap;
const { imageData: referenceVolumeImageData } = referenceVolume;
let boundsIJK;
// Todo: using the first annotation for now
if (annotations && annotations[0].data?.cachedStats) {
if (annotations?.length && annotations[0].data?.cachedStats) {
const { projectionPoints } = annotations[0].data.cachedStats;
const pointsToUse = [].concat(...projectionPoints); // cannot use flat() because of typescript compiler right now
@@ -1,4 +1,4 @@
export default function createAndDownloadTMTVReport(segReport, additionalReportRows) {
export default function createAndDownloadTMTVReport(segReport, additionalReportRows, options = {}) {
const firstReport = segReport[Object.keys(segReport)[0]];
const columns = Object.keys(firstReport);
const csv = [columns.join(',')];
@@ -40,6 +40,6 @@ export default function createAndDownloadTMTVReport(segReport, additionalReportR
const a = document.createElement('a');
a.href = url;
a.download = `${firstReport.PatientID}_tmtv.csv`;
a.download = options.filename ?? `${firstReport.PatientID}_tmtv.csv`;
a.click();
}