feat(tmtv): add ct ranges for thresholding (#3225)

* Add CT volume in PET ROI Thresholding (#3053)

* Add CT volume in PET ROI Thresholding

* Change UI ROI Threshold config

* Refactoring ROI Threshold configuration Panel

* fix menus

* try to fix build preview

* try to fix build preview

---------

Co-authored-by: rodrigobasilio2022 <114958722+rodrigobasilio2022@users.noreply.github.com>
This commit is contained in:
Alireza 2023-03-07 22:47:16 -05:00 committed by GitHub
parent f14e23cb8b
commit 7a591b87a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 240 additions and 156 deletions

View File

@ -1,7 +1,6 @@
import React, { useEffect, useState, useCallback, useReducer } from 'react'; import React, { useEffect, useState, useCallback, useReducer } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { SegmentationTable, Button, Icon } from '@ohif/ui'; import { SegmentationTable, Button, Icon } from '@ohif/ui';
import classnames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import segmentationEditHandler from './segmentationEditHandler'; import segmentationEditHandler from './segmentationEditHandler';
@ -10,14 +9,16 @@ import ROIThresholdConfiguration, {
ROI_STAT, ROI_STAT,
} from './ROIThresholdConfiguration'; } from './ROIThresholdConfiguration';
const LOWER_THRESHOLD_DEFAULT = 0; const LOWER_CT_THRESHOLD_DEFAULT = -1000;
const UPPER_THRESHOLD_DEFAULT = 100; const UPPER_CT_THRESHOLD_DEFAULT = 500;
const LOWER_PT_THRESHOLD_DEFAULT = 5;
const UPPER_PT_THRESHOLD_DEFAULT = 50;
const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature
const DEFAULT_STRATEGY = ROI_STAT; const DEFAULT_STRATEGY = ROI_STAT;
function reducer(state, action) { function reducer(state, action) {
const { payload } = action; const { payload } = action;
const { strategy, lower, upper, weight } = payload; const { strategy, ctLower, ctUpper, ptLower, ptUpper, weight } = payload;
switch (action.type) { switch (action.type) {
case 'setStrategy': case 'setStrategy':
@ -28,8 +29,10 @@ function reducer(state, action) {
case 'setThreshold': case 'setThreshold':
return { return {
...state, ...state,
lower: lower ? lower : state.lower, ctLower: ctLower ? ctLower : state.ctLower,
upper: upper ? upper : state.upper, ctUpper: ctUpper ? ctUpper : state.ctUpper,
ptLower: ptLower ? ptLower : state.ptLower,
ptUpper: ptUpper ? ptUpper : state.ptUpper,
}; };
case 'setWeight': case 'setWeight':
return { return {
@ -57,8 +60,10 @@ export default function PanelRoiThresholdSegmentation({
const [config, dispatch] = useReducer(reducer, { const [config, dispatch] = useReducer(reducer, {
strategy: DEFAULT_STRATEGY, strategy: DEFAULT_STRATEGY,
lower: LOWER_THRESHOLD_DEFAULT, ctLower: LOWER_CT_THRESHOLD_DEFAULT,
upper: UPPER_THRESHOLD_DEFAULT, ctUpper: UPPER_CT_THRESHOLD_DEFAULT,
ptLower: LOWER_PT_THRESHOLD_DEFAULT,
ptUpper: UPPER_PT_THRESHOLD_DEFAULT,
weight: WEIGHT_DEFAULT, weight: WEIGHT_DEFAULT,
}); });
@ -171,91 +176,93 @@ export default function PanelRoiThresholdSegmentation({
}, [segmentations, selectedSegmentationId]); }, [segmentations, selectedSegmentationId]);
return ( return (
<div className="flex flex-col"> <>
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar"> <div className="flex flex-col">
<div className="flex mx-4 my-4 mb-4 space-x-4"> <div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
<Button <div className="flex mx-4 my-4 mb-4 space-x-4">
color="primary" <Button
onClick={() => { color="primary"
setLabelmapLoading(true); onClick={() => {
setTimeout(() => { setLabelmapLoading(true);
runCommand('createNewLabelmapFromPT').then(segmentationId => { setTimeout(() => {
setLabelmapLoading(false); runCommand('createNewLabelmapFromPT').then(segmentationId => {
setSelectedSegmentationId(segmentationId); setLabelmapLoading(false);
setSelectedSegmentationId(segmentationId);
});
}); });
}); }}
>
{labelmapLoading ? 'loading ...' : 'New Label'}
</Button>
<Button color="primary" onClick={handleROIThresholding}>
Run
</Button>
</div>
<div
className="flex items-center justify-around h-8 mb-2 border-t outline-none cursor-pointer select-none bg-secondary-dark first:border-0 border-secondary-light"
onClick={() => {
setShowConfig(!showConfig);
}} }}
> >
{labelmapLoading ? 'loading ...' : 'New Label'} <div className="px-4 text-base text-white">
</Button> {t('ROI Threshold Configuration')}
<Button color="primary" onClick={handleROIThresholding}> </div>
Run
</Button>
</div>
<div
className="flex items-center justify-around h-8 mb-2 border-t outline-none cursor-pointer select-none bg-secondary-dark first:border-0 border-secondary-light"
onClick={() => {
setShowConfig(!showConfig);
}}
>
<div className="px-4 text-base text-white">
{t('ROI Threshold Configuration')}
</div> </div>
</div> {showConfig && (
{showConfig && ( <ROIThresholdConfiguration
<ROIThresholdConfiguration config={config}
config={config} dispatch={dispatch}
dispatch={dispatch} runCommand={runCommand}
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> {/* show segmentation table */}
{tmtvValue !== null ? ( <div className="mt-4">
<div className="flex items-baseline justify-between px-2 py-1 mt-4 bg-secondary-dark"> {segmentations?.length ? (
<span className="text-base font-bold tracking-widest text-white uppercase"> <SegmentationTable
{'TMTV:'} title={t('Segmentations')}
</span> segmentations={segmentations}
<div className="text-white">{`${tmtvValue} mL`}</div> 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> </div>
) : null} {tmtvValue !== null ? (
<ExportReports <div className="flex items-baseline justify-between px-2 py-1 mt-4 bg-secondary-dark">
segmentations={segmentations} <span className="text-base font-bold tracking-widest text-white uppercase">
tmtvValue={tmtvValue} {'TMTV:'}
config={config} </span>
commandsManager={commandsManager} <div className="text-white">{`${tmtvValue} mL`}</div>
/> </div>
) : null}
<ExportReports
segmentations={segmentations}
tmtvValue={tmtvValue}
config={config}
commandsManager={commandsManager}
/>
</div>
</div> </div>
<div <div
className="opacity-50 hover:opacity-80 flex items-center justify-center text-blue-400 mt-auto cursor-pointer mb-4" className="opacity-50 hover:opacity-80 flex items-center justify-center text-blue-400 mt-auto cursor-pointer mb-4"
@ -271,11 +278,11 @@ export default function PanelRoiThresholdSegmentation({
width="15px" width="15px"
height="15px" height="15px"
name={'info'} name={'info'}
className={'ml-4 mr-3 text-primary-active'} className={'text-primary-active ml-4 mr-3'}
/> />
<span>{'User Guide'}</span> <span>{'User Guide'}</span>
</div> </div>
</div> </>
); );
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Input, Select, Button, ButtonGroup } from '@ohif/ui'; import { Input, Label, Select, Button, ButtonGroup } from '@ohif/ui';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const ROI_STAT = 'roi_stat'; export const ROI_STAT = 'roi_stat';
@ -14,9 +14,9 @@ function ROIThresholdConfiguration({ config, dispatch, runCommand }) {
const { t } = useTranslation('ROIThresholdConfiguration'); const { t } = useTranslation('ROIThresholdConfiguration');
return ( return (
<div className="flex flex-col px-4 space-y-4 bg-primary-dark"> <div className="flex flex-col px-4 space-y-4 bg-primary-dark py-2">
<div className="flex items-end space-x-2"> <div className="flex items-end space-x-2">
<div className="flex flex-col w-1/2 mt-2 "> <div className="flex flex-col w-1/2">
<Select <Select
label={t('Strategy')} label={t('Strategy')}
closeMenuOnSelect={true} closeMenuOnSelect={true}
@ -78,39 +78,101 @@ function ROIThresholdConfiguration({ config, dispatch, runCommand }) {
/> />
)} )}
{config.strategy !== ROI_STAT && ( {config.strategy !== ROI_STAT && (
<div className="flex justify-between"> <div className="text-sm mr-2">
<Input <table>
label={t('Lower')} <tbody>
labelClassName="text-white" <tr className="mt-2">
className="mt-2 bg-black border-primary-main" <td className="pr-4 pt-2" colSpan="3">
type="text" <Label
containerClassName="mr-2" className="text-white"
value={config.lower} text="Lower & Upper Ranges"
onChange={e => { ></Label>
dispatch({ </td>
type: 'setThreshold', </tr>
payload: { <tr className="mt-2">
lower: e.target.value, <td className="text-center pr-4 pt-2">
}, <Label className="text-white" text="CT"></Label>
}); </td>
}} <td>
/> <div className="flex justify-between">
<Input <Input
label={t('Upper')} label={t('')}
labelClassName="text-white" labelClassName="text-white"
className="mt-2 bg-black border-primary-main" className="mt-2 bg-black border-primary-main"
type="text" type="text"
containerClassName="mr-2" containerClassName="mr-2"
value={config.upper} value={config.ctLower}
onChange={e => { onChange={e => {
dispatch({ dispatch({
type: 'setThreshold', type: 'setThreshold',
payload: { payload: {
upper: e.target.value, ctLower: e.target.value,
}, },
}); });
}} }}
/> />
<Input
label={t('')}
labelClassName="text-white"
className="mt-2 bg-black border-primary-main"
type="text"
containerClassName="mr-2"
value={config.ctUpper}
onChange={e => {
dispatch({
type: 'setThreshold',
payload: {
ctUpper: e.target.value,
},
});
}}
/>
</div>
</td>
</tr>
<tr>
<td className="text-center pr-4 pt-2">
<Label className="text-white" text="PT"></Label>
</td>
<td>
<div className="flex justify-between">
<Input
label={t('')}
labelClassName="text-white"
className="mt-2 bg-black border-primary-main"
type="text"
containerClassName="mr-2"
value={config.ptLower}
onChange={e => {
dispatch({
type: 'setThreshold',
payload: {
ptLower: e.target.value,
},
});
}}
/>
<Input
label={t('')}
labelClassName="text-white"
className="mt-2 bg-black border-primary-main"
type="text"
containerClassName="mr-2"
value={config.ptUpper}
onChange={e => {
dispatch({
type: 'setThreshold',
payload: {
ptUpper: e.target.value,
},
});
}}
/>
</div>
</td>
</tr>
</tbody>
</table>
</div> </div>
)} )}
</div> </div>

View File

@ -173,12 +173,20 @@ const commandsModule = ({
); );
const { representationData } = segmentation; const { representationData } = segmentation;
const { volumeId: segVolumeId } = representationData[LABELMAP]; const {
displaySetMatchDetails: matchDetails,
} = hangingProtocolService.getMatchDetails();
const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use
const ctDisplaySet = matchDetails.get('ctDisplaySet');
const ctVolumeId = `${volumeLoaderScheme}:${ctDisplaySet.displaySetInstanceUID}`; // VolumeId with loader id + volume id
const { volumeId: segVolumeId } = representationData[LABELMAP];
const { referencedVolumeId } = cs.cache.getVolume(segVolumeId); const { referencedVolumeId } = cs.cache.getVolume(segVolumeId);
const labelmapVolume = cs.cache.getVolume(segmentationId); const labelmapVolume = cs.cache.getVolume(segmentationId);
const referencedVolume = cs.cache.getVolume(referencedVolumeId); const referencedVolume = cs.cache.getVolume(referencedVolumeId);
const ctReferencedVolume = cs.cache.getVolume(ctVolumeId);
if (!referencedVolume) { if (!referencedVolume) {
throw new Error('No Reference volume found'); throw new Error('No Reference volume found');
@ -201,23 +209,20 @@ const commandsModule = ({
return; return;
} }
const { lower, upper } = getThresholdValues( const { ptLower, ptUpper, ctLower, ctUpper } = getThresholdValues(
annotationUIDs, annotationUIDs,
referencedVolume, [referencedVolume, ctReferencedVolume],
config config
); );
const configToUse = {
lower,
upper,
overwrite: true,
};
return csTools.utilities.segmentation.rectangleROIThresholdVolumeByRange( return csTools.utilities.segmentation.rectangleROIThresholdVolumeByRange(
annotationUIDs, annotationUIDs,
labelmapVolume, labelmapVolume,
[referencedVolume], [
configToUse { volume: referencedVolume, lower: ptLower, upper: ptUpper },
{ volume: ctReferencedVolume, lower: ctLower, upper: ctUpper },
],
{ overwrite: true }
); );
}, },
calculateSuvPeak: ({ labelmap }) => { calculateSuvPeak: ({ labelmap }) => {

View File

@ -1,19 +1,7 @@
import * as csTools from '@cornerstonejs/tools'; import * as csTools from '@cornerstonejs/tools';
function getThresholdValues( function getRoiStats(referencedVolume, annotations) {
annotationUIDs,
referencedVolume,
config
): { lower: number; upper: number } {
if (config.strategy === 'range') {
return {
lower: Number(config.lower),
upper: Number(config.upper),
};
}
// roiStats // roiStats
const { weight } = config;
const { imageData } = referencedVolume; const { imageData } = referencedVolume;
const values = imageData const values = imageData
.getPointData() .getPointData()
@ -24,10 +12,6 @@ function getThresholdValues(
const { fn, baseValue } = _getStrategyFn('max'); const { fn, baseValue } = _getStrategyFn('max');
let value = baseValue; let value = baseValue;
const annotations = annotationUIDs.map(annotationUID =>
csTools.annotation.state.getAnnotation(annotationUID)
);
const boundsIJK = csTools.utilities.rectangleROITool.getBoundsIJKFromRectangleAnnotations( const boundsIJK = csTools.utilities.rectangleROITool.getBoundsIJKFromRectangleAnnotations(
annotations, annotations,
referencedVolume referencedVolume
@ -43,10 +27,36 @@ function getThresholdValues(
} }
} }
} }
return value;
}
function getThresholdValues(
annotationUIDs,
referencedVolumes,
config
): { ptLower: number; ptUpper: number; ctLower: number; ctUpper: number } {
if (config.strategy === 'range') {
return {
ptLower: Number(config.ptLower),
ptUpper: Number(config.ptUpper),
ctLower: Number(config.ctLower),
ctUpper: Number(config.ctUpper),
};
}
const { weight } = config;
const annotations = annotationUIDs.map(annotationUID =>
csTools.annotation.state.getAnnotation(annotationUID)
);
const ptValue = getRoiStats(referencedVolumes[0], annotations);
const ctValue = getRoiStats(referencedVolumes[1], annotations);
return { return {
lower: weight * value, ctLower: weight * ctValue,
upper: +Infinity, ctUpper: +Infinity,
ptLower: weight * ptValue,
ptUpper: +Infinity,
}; };
} }

View File

@ -12,7 +12,7 @@
base = "" base = ""
build = "yarn run build:viewer:ci" build = "yarn run build:viewer:ci"
publish = "dist" publish = "dist"
ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF . ../ui/ ../core/ ../i18n ../../extensions/ ../../modes" ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF . ../ui/ ../core/ ../i18n"
# NODE_VERSION in root `.nvmrc` takes priority # NODE_VERSION in root `.nvmrc` takes priority