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 PropTypes from 'prop-types';
import { SegmentationTable, Button, Icon } from '@ohif/ui';
import classnames from 'classnames';
import { useTranslation } from 'react-i18next';
import segmentationEditHandler from './segmentationEditHandler';
@ -10,14 +9,16 @@ import ROIThresholdConfiguration, {
ROI_STAT,
} from './ROIThresholdConfiguration';
const LOWER_THRESHOLD_DEFAULT = 0;
const UPPER_THRESHOLD_DEFAULT = 100;
const LOWER_CT_THRESHOLD_DEFAULT = -1000;
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 DEFAULT_STRATEGY = ROI_STAT;
function reducer(state, action) {
const { payload } = action;
const { strategy, lower, upper, weight } = payload;
const { strategy, ctLower, ctUpper, ptLower, ptUpper, weight } = payload;
switch (action.type) {
case 'setStrategy':
@ -28,8 +29,10 @@ function reducer(state, action) {
case 'setThreshold':
return {
...state,
lower: lower ? lower : state.lower,
upper: upper ? upper : state.upper,
ctLower: ctLower ? ctLower : state.ctLower,
ctUpper: ctUpper ? ctUpper : state.ctUpper,
ptLower: ptLower ? ptLower : state.ptLower,
ptUpper: ptUpper ? ptUpper : state.ptUpper,
};
case 'setWeight':
return {
@ -57,8 +60,10 @@ export default function PanelRoiThresholdSegmentation({
const [config, dispatch] = useReducer(reducer, {
strategy: DEFAULT_STRATEGY,
lower: LOWER_THRESHOLD_DEFAULT,
upper: UPPER_THRESHOLD_DEFAULT,
ctLower: LOWER_CT_THRESHOLD_DEFAULT,
ctUpper: UPPER_CT_THRESHOLD_DEFAULT,
ptLower: LOWER_PT_THRESHOLD_DEFAULT,
ptUpper: UPPER_PT_THRESHOLD_DEFAULT,
weight: WEIGHT_DEFAULT,
});
@ -171,91 +176,93 @@ export default function PanelRoiThresholdSegmentation({
}, [segmentations, selectedSegmentationId]);
return (
<div className="flex flex-col">
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
<div className="flex mx-4 my-4 mb-4 space-x-4">
<Button
color="primary"
onClick={() => {
setLabelmapLoading(true);
setTimeout(() => {
runCommand('createNewLabelmapFromPT').then(segmentationId => {
setLabelmapLoading(false);
setSelectedSegmentationId(segmentationId);
<>
<div className="flex flex-col">
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
<div className="flex mx-4 my-4 mb-4 space-x-4">
<Button
color="primary"
onClick={() => {
setLabelmapLoading(true);
setTimeout(() => {
runCommand('createNewLabelmapFromPT').then(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'}
</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);
}}
>
<div className="px-4 text-base text-white">
{t('ROI Threshold Configuration')}
<div className="px-4 text-base text-white">
{t('ROI Threshold Configuration')}
</div>
</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,
});
}}
{showConfig && (
<ROIThresholdConfiguration
config={config}
dispatch={dispatch}
runCommand={runCommand}
/>
) : null}
</div>
{tmtvValue !== null ? (
<div className="flex items-baseline justify-between px-2 py-1 mt-4 bg-secondary-dark">
<span className="text-base font-bold tracking-widest text-white uppercase">
{'TMTV:'}
</span>
<div className="text-white">{`${tmtvValue} mL`}</div>
)}
{/* 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>
) : null}
<ExportReports
segmentations={segmentations}
tmtvValue={tmtvValue}
config={config}
commandsManager={commandsManager}
/>
{tmtvValue !== null ? (
<div className="flex items-baseline justify-between px-2 py-1 mt-4 bg-secondary-dark">
<span className="text-base font-bold tracking-widest text-white uppercase">
{'TMTV:'}
</span>
<div className="text-white">{`${tmtvValue} mL`}</div>
</div>
) : null}
<ExportReports
segmentations={segmentations}
tmtvValue={tmtvValue}
config={config}
commandsManager={commandsManager}
/>
</div>
</div>
<div
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"
height="15px"
name={'info'}
className={'ml-4 mr-3 text-primary-active'}
className={'text-primary-active ml-4 mr-3'}
/>
<span>{'User Guide'}</span>
</div>
</div>
</>
);
}

View File

@ -1,5 +1,5 @@
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';
export const ROI_STAT = 'roi_stat';
@ -14,9 +14,9 @@ function ROIThresholdConfiguration({ config, dispatch, runCommand }) {
const { t } = useTranslation('ROIThresholdConfiguration');
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 flex-col w-1/2 mt-2 ">
<div className="flex flex-col w-1/2">
<Select
label={t('Strategy')}
closeMenuOnSelect={true}
@ -78,39 +78,101 @@ function ROIThresholdConfiguration({ config, dispatch, runCommand }) {
/>
)}
{config.strategy !== ROI_STAT && (
<div className="flex justify-between">
<Input
label={t('Lower')}
labelClassName="text-white"
className="mt-2 bg-black border-primary-main"
type="text"
containerClassName="mr-2"
value={config.lower}
onChange={e => {
dispatch({
type: 'setThreshold',
payload: {
lower: e.target.value,
},
});
}}
/>
<Input
label={t('Upper')}
labelClassName="text-white"
className="mt-2 bg-black border-primary-main"
type="text"
containerClassName="mr-2"
value={config.upper}
onChange={e => {
dispatch({
type: 'setThreshold',
payload: {
upper: e.target.value,
},
});
}}
/>
<div className="text-sm mr-2">
<table>
<tbody>
<tr className="mt-2">
<td className="pr-4 pt-2" colSpan="3">
<Label
className="text-white"
text="Lower & Upper Ranges"
></Label>
</td>
</tr>
<tr className="mt-2">
<td className="text-center pr-4 pt-2">
<Label className="text-white" text="CT"></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.ctLower}
onChange={e => {
dispatch({
type: 'setThreshold',
payload: {
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>

View File

@ -173,12 +173,20 @@ const commandsModule = ({
);
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 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');
@ -201,23 +209,20 @@ const commandsModule = ({
return;
}
const { lower, upper } = getThresholdValues(
const { ptLower, ptUpper, ctLower, ctUpper } = getThresholdValues(
annotationUIDs,
referencedVolume,
[referencedVolume, ctReferencedVolume],
config
);
const configToUse = {
lower,
upper,
overwrite: true,
};
return csTools.utilities.segmentation.rectangleROIThresholdVolumeByRange(
annotationUIDs,
labelmapVolume,
[referencedVolume],
configToUse
[
{ volume: referencedVolume, lower: ptLower, upper: ptUpper },
{ volume: ctReferencedVolume, lower: ctLower, upper: ctUpper },
],
{ overwrite: true }
);
},
calculateSuvPeak: ({ labelmap }) => {

View File

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

View File

@ -12,7 +12,7 @@
base = ""
build = "yarn run build:viewer:ci"
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