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:
parent
f14e23cb8b
commit
7a591b87a2
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user