feat(tmtv-mode): Add Brush tools and move SUV peak calculation to web worker (#4053)
This commit is contained in:
1 parent
8c5ab942b9
commit
8192e348ec
27 files changed
+593
-212
No files matched your search
+55
-19
@@ -2,11 +2,16 @@ import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon, ActionButtons } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { eventTarget } from '@cornerstonejs/core';
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
import { handleROIThresholding } from '../../utils/handleROIThresholding';
|
||||
|
||||
export default function PanelRoiThresholdSegmentation({ servicesManager, commandsManager }) {
|
||||
const { segmentationService, uiNotificationService } = servicesManager.services;
|
||||
const { t } = useTranslation('PanelSUVExport');
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
const [activeSegmentation, setActiveSegmentation] = useState(null);
|
||||
|
||||
/**
|
||||
* Update UI based on segmentation changes (added, removed, updated)
|
||||
@@ -22,7 +27,11 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
|
||||
const activeSegmentation = segmentations.filter(seg => seg.isActive);
|
||||
setActiveSegmentation(activeSegmentation[0]);
|
||||
});
|
||||
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
@@ -33,26 +42,53 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
|
||||
};
|
||||
}, []);
|
||||
|
||||
const tmtvValue = segmentations?.[0]?.cachedStats?.tmtv?.value || null;
|
||||
const config = segmentations?.[0]?.cachedStats?.tmtv?.config || {};
|
||||
useEffect(() => {
|
||||
const callback = async evt => {
|
||||
const { detail } = evt;
|
||||
const { segmentationId } = detail;
|
||||
|
||||
segmentations.forEach(segmentation => {
|
||||
const { cachedStats } = segmentation;
|
||||
if (!cachedStats) {
|
||||
return;
|
||||
}
|
||||
if (!segmentationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// segment 1
|
||||
const suvPeak = cachedStats?.['1']?.suvPeak?.suvPeak;
|
||||
|
||||
if (Number.isNaN(suvPeak)) {
|
||||
uiNotificationService.show({
|
||||
title: 'SUV Peak',
|
||||
message: 'Segmented volume does not allow SUV Peak calculation',
|
||||
type: 'warning',
|
||||
await handleROIThresholding({
|
||||
segmentationId,
|
||||
commandsManager,
|
||||
segmentationService,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
const { cachedStats } = segmentation;
|
||||
if (!cachedStats) {
|
||||
return;
|
||||
}
|
||||
|
||||
// segment 1
|
||||
const suvPeak = cachedStats?.['1']?.suvPeak?.suvPeak;
|
||||
|
||||
if (Number.isNaN(suvPeak)) {
|
||||
uiNotificationService.show({
|
||||
title: 'SUV Peak',
|
||||
message: 'Segmented volume does not allow SUV Peak calculation',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
eventTarget.addEventListenerDebounced(Enums.Events.SEGMENTATION_DATA_MODIFIED, callback, 300);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListenerDebounced(Enums.Events.SEGMENTATION_DATA_MODIFIED, callback);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!activeSegmentation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tmtvValue = activeSegmentation.cachedStats?.tmtv?.value || null;
|
||||
const config = activeSegmentation.cachedStats?.tmtv?.config || {};
|
||||
|
||||
const actions = [
|
||||
{
|
||||
@@ -67,7 +103,7 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
|
||||
disabled: tmtvValue === null,
|
||||
},
|
||||
{
|
||||
label: 'Create RT Report',
|
||||
label: 'Export RT Report',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('createTMTVRTReport');
|
||||
},
|
||||
@@ -80,7 +116,7 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
|
||||
<div className="mt-2 mb-10 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">
|
||||
<div className="bg-secondary-dark flex items-baseline justify-between px-2 py-1">
|
||||
<span className="text-base font-bold uppercase tracking-widest text-white">
|
||||
{'TMTV:'}
|
||||
</span>
|
||||
|
||||
@@ -62,72 +62,16 @@ function RectangleROIOptions({ servicesManager, commandsManager }) {
|
||||
|
||||
const handleROIThresholding = useCallback(() => {
|
||||
const segmentationId = selectedSegmentationId;
|
||||
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
const activeSegmentIndex =
|
||||
cs3dTools.segmentation.segmentIndex.getActiveSegmentIndex(segmentationId);
|
||||
|
||||
// run the threshold based on the active segment index
|
||||
// Todo: later find a way to associate each rectangle with a segment (e.g., maybe with color?)
|
||||
const labelmap = runCommand('thresholdSegmentationByRectangleROITool', {
|
||||
runCommand('thresholdSegmentationByRectangleROITool', {
|
||||
segmentationId,
|
||||
config,
|
||||
segmentIndex: activeSegmentIndex,
|
||||
});
|
||||
|
||||
// re-calculating the cached stats for the active segmentation
|
||||
const updatedPerSegmentCachedStats = {};
|
||||
segmentation.segments = segmentation.segments.map(segment => {
|
||||
if (!segment || !segment.segmentIndex) {
|
||||
return segment;
|
||||
}
|
||||
|
||||
const segmentIndex = segment.segmentIndex;
|
||||
|
||||
const lesionStats = runCommand('getLesionStats', { labelmap, segmentIndex });
|
||||
const suvPeak = runCommand('calculateSuvPeak', { labelmap, segmentIndex });
|
||||
const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
|
||||
|
||||
// update segDetails with the suv peak for the active segmentation
|
||||
const cachedStats = {
|
||||
lesionStats,
|
||||
suvPeak,
|
||||
lesionGlyoclysisStats,
|
||||
};
|
||||
|
||||
segment.cachedStats = cachedStats;
|
||||
segment.displayText = [
|
||||
`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`,
|
||||
`Volume: ${lesionStats.volume.toFixed(2)} mm3`,
|
||||
];
|
||||
updatedPerSegmentCachedStats[segmentIndex] = cachedStats;
|
||||
|
||||
return segment;
|
||||
});
|
||||
|
||||
const notYetUpdatedAtSource = true;
|
||||
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const tmtv = runCommand('calculateTMTV', { segmentations });
|
||||
|
||||
segmentation.cachedStats = Object.assign(
|
||||
segmentation.cachedStats,
|
||||
updatedPerSegmentCachedStats,
|
||||
{
|
||||
tmtv: {
|
||||
value: tmtv.toFixed(3),
|
||||
config: { ...config },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
segmentationService.addOrUpdateSegmentation(
|
||||
{
|
||||
...segmentation,
|
||||
},
|
||||
false, // don't suppress events
|
||||
notYetUpdatedAtSource
|
||||
);
|
||||
}, [selectedSegmentationId, config]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -171,27 +115,6 @@ function RectangleROIOptions({ servicesManager, commandsManager }) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
() => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
|
||||
if (segmentations.length > 0) {
|
||||
setSelectedSegmentationId(segmentations[0].id);
|
||||
handleROIThresholding();
|
||||
} else {
|
||||
setSelectedSegmentationId(null);
|
||||
handleROIThresholding();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="invisible-scrollbar mb-2 flex flex-col overflow-y-auto overflow-x-hidden">
|
||||
<ROIThresholdConfiguration
|
||||
|
||||
Reference in new issue
Block a user