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,6 +176,7 @@ export default function PanelRoiThresholdSegmentation({
}, [segmentations, selectedSegmentationId]); }, [segmentations, selectedSegmentationId]);
return ( return (
<>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar"> <div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
<div className="flex mx-4 my-4 mb-4 space-x-4"> <div className="flex mx-4 my-4 mb-4 space-x-4">
@ -257,6 +263,7 @@ export default function PanelRoiThresholdSegmentation({
commandsManager={commandsManager} 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"
onClick={() => { onClick={() => {
@ -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,40 +78,102 @@ function ROIThresholdConfiguration({ config, dispatch, runCommand }) {
/> />
)} )}
{config.strategy !== ROI_STAT && ( {config.strategy !== ROI_STAT && (
<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"> <div className="flex justify-between">
<Input <Input
label={t('Lower')} 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.lower} value={config.ctLower}
onChange={e => { onChange={e => {
dispatch({ dispatch({
type: 'setThreshold', type: 'setThreshold',
payload: { payload: {
lower: e.target.value, ctLower: e.target.value,
}, },
}); });
}} }}
/> />
<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.ctUpper}
onChange={e => { onChange={e => {
dispatch({ dispatch({
type: 'setThreshold', type: 'setThreshold',
payload: { payload: {
upper: e.target.value, ctUpper: e.target.value,
}, },
}); });
}} }}
/> />
</div> </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>
); );

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