feat(toolbox): Refactor Toolbar and Toolbox to enable sections (#4825)

This commit is contained in:
Alireza authored and GitHub committed 2025-03-06 22:19:47 -05:00
1 parent 86c12246ac
commit cc5cdfb081
79 files changed
+3527 -2992

No files matched your search

@@ -1,9 +1,11 @@
import React, { useState, useCallback, useReducer, useEffect } from 'react';
import React, { useCallback, useReducer } from 'react';
import { Button } from '@ohif/ui';
import ROIThresholdConfiguration, {
ROI_STAT,
} from './PanelROIThresholdSegmentation/ROIThresholdConfiguration';
import * as cs3dTools from '@cornerstonejs/tools';
import { useSystem } from '@ohif/core';
import { useSegmentations } from '@ohif/extension-cornerstone';
const LOWER_CT_THRESHOLD_DEFAULT = -1024;
const UPPER_CT_THRESHOLD_DEFAULT = 1024;
@@ -40,9 +42,10 @@ function reducer(state, action) {
}
}
function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes) {
const { segmentationService } = servicesManager.services;
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
function RectangleROIOptions() {
const { commandsManager } = useSystem();
const segmentations = useSegmentations();
const activeSegmentation = segmentations[0];
const runCommand = useCallback(
(commandName, commandOptions = {}) => {
@@ -61,58 +64,20 @@ function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes)
});
const handleROIThresholding = useCallback(() => {
const segmentationId = selectedSegmentationId;
if (!activeSegmentation) {
return;
}
const segmentationId = activeSegmentation.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?)
runCommand('thresholdSegmentationByRectangleROITool', {
segmentationId,
config,
segmentIndex: activeSegmentIndex,
});
}, [selectedSegmentationId, config]);
useEffect(() => {
const segmentations = segmentationService.getSegmentationRepresentations();
if (!segmentations.length) {
return;
}
const isActive = segmentations.find(seg => seg.isActive);
setSelectedSegmentationId(isActive.id);
}, []);
/**
* Update UI based on segmentation changes (added, removed, updated)
*/
useEffect(() => {
// ~~ Subscription
const updated = segmentationService.EVENTS.SEGMENTATION_MODIFIED;
const subscriptions = [];
[updated].forEach(evt => {
const { unsubscribe } = segmentationService.subscribe(evt, () => {
const segmentations = segmentationService.getSegmentationRepresentations();
if (!segmentations.length) {
return;
}
const isActive = segmentations.find(seg => seg.isActive);
setSelectedSegmentationId(isActive.id);
});
subscriptions.push(unsubscribe);
});
return () => {
subscriptions.forEach(unsub => {
unsub();
});
};
}, []);
}, [activeSegmentation, config]);
return (
<div className="invisible-scrollbar mb-2 flex flex-col overflow-y-auto overflow-x-hidden">
@@ -121,7 +86,7 @@ function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes)
dispatch={dispatch}
runCommand={runCommand}
/>
{selectedSegmentationId !== null && (
{activeSegmentation && (
<Button
className="mt-2 !h-[26px] !w-[75px]"
onClick={handleROIThresholding}
+35 -60
View File
@@ -102,7 +102,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
// corrected PT vs the non-attenuation correct PT)
let ptDisplaySet = null;
for (const [viewportId, viewportDetails] of viewportMatchDetails) {
for (const [, viewportDetails] of viewportMatchDetails) {
const { displaySetsInfo } = viewportDetails;
const displaySets = displaySetsInfo.map(({ displaySetInstanceUID }) =>
displaySetService.getDisplaySetByUID(displaySetInstanceUID)
@@ -199,15 +199,21 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
const { representationData } = segmentation;
const { displaySetMatchDetails: matchDetails } = hangingProtocolService.getMatchDetails();
const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use
const ctDisplaySetMatch = matchDetails.get('ctDisplaySet');
const ptDisplaySetMatch = matchDetails.get('ptDisplaySet');
const ctDisplaySet = matchDetails.get('ctDisplaySet');
const ctVolumeId = `${volumeLoaderScheme}:${ctDisplaySet.displaySetInstanceUID}`; // VolumeId with loader id + volume id
const ctDisplaySet = displaySetService.getDisplaySetByUID(
ctDisplaySetMatch.displaySetInstanceUID
);
const ptDisplaySet = displaySetService.getDisplaySetByUID(
ptDisplaySetMatch.displaySetInstanceUID
);
const { volumeId: segVolumeId } = representationData[
SegmentationRepresentations.Labelmap
] as csTools.Types.LabelmapToolOperationDataVolume;
const { referencedVolumeId } = cs.cache.getVolume(segVolumeId);
const labelmapVolume = cs.cache.getVolume(segVolumeId);
const annotationUIDs = _getAnnotationsSelectedByToolNames(ROI_THRESHOLD_MANUAL_TOOL_IDS);
@@ -220,71 +226,40 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
return;
}
const labelmapVolume = cs.cache.getVolume(segmentationId);
let referencedVolume = cs.cache.getVolume(referencedVolumeId);
const ctReferencedVolume = cs.cache.getVolume(ctVolumeId);
// check if viewport is
if (!referencedVolume) {
throw new Error('No Reference volume found');
}
if (!labelmapVolume) {
throw new Error('No Reference labelmap found');
}
const annotation = csTools.annotation.state.getAnnotation(annotationUIDs[0]);
const {
metadata: {
enabledElement: { viewport },
},
} = annotation;
const showingReferenceVolume = viewport.hasVolumeId(referencedVolumeId);
if (!showingReferenceVolume) {
// if the reference volume is not being displayed, we can't
// rely on it for thresholding, we have couple of options here
// 1. We choose whatever volume is being displayed
// 2. We check if it is a fusion viewport, we pick the volume
// that matches the size and dimensions of the labelmap. This might
// happen if the 4D PT is converted to a computed volume and displayed
// and wants to threshold the labelmap
// 3. We throw an error
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
viewport.id
);
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
const volume = cs.cache
.getVolumes()
.find(volume => volume.volumeId.includes(displaySetInstanceUID));
if (
cs.utilities.isEqual(volume.dimensions, labelmapVolume.dimensions) &&
cs.utilities.isEqual(volume.spacing, labelmapVolume.spacing)
) {
referencedVolume = volume;
}
});
}
const { ptLower, ptUpper, ctLower, ctUpper } = getThresholdValues(
annotationUIDs,
[referencedVolume, ctReferencedVolume],
ptDisplaySet,
config
);
const { imageIds: ptImageIds } = ptDisplaySet;
const ptVolumeInfo = cs.cache.getVolumeContainingImageId(ptImageIds[0]);
if (!ptVolumeInfo) {
uiNotificationService.error('No PT volume found');
return;
}
const { imageIds: ctImageIds } = ctDisplaySet;
const ctVolumeInfo = cs.cache.getVolumeContainingImageId(ctImageIds[0]);
if (!ctVolumeInfo) {
uiNotificationService.error('No CT volume found');
return;
}
const ptVolume = ptVolumeInfo.volume;
const ctVolume = ctVolumeInfo.volume;
return csTools.utilities.segmentation.rectangleROIThresholdVolumeByRange(
annotationUIDs,
labelmapVolume,
[
{ volume: referencedVolume, lower: ptLower, upper: ptUpper },
{ volume: ctReferencedVolume, lower: ctLower, upper: ctUpper },
{ volume: ptVolume, lower: ptLower, upper: ptUpper },
{ volume: ctVolume, lower: ctLower, upper: ctUpper },
],
{ overwrite: true, segmentIndex }
{ overwrite: true, segmentIndex, segmentationId }
);
},
calculateSuvPeak: async ({ segmentationId, segmentIndex }) => {
+1 -7
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { PanelPetSUV, PanelROIThresholdExport } from './Panels';
import { Toolbox } from '@ohif/ui-next';
import { Toolbox } from '@ohif/extension-default';
import PanelTMTV from './Panels/PanelTMTV';
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
@@ -17,9 +17,6 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
const wrappedROIThresholdToolbox = () => {
return (
<Toolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
buttonSectionId="ROIThresholdToolbox"
title="Threshold Tools"
/>
@@ -39,9 +36,6 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
return (
<>
<Toolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
buttonSectionId="ROIThresholdToolbox"
title="Threshold Tools"
/>
+2 -2
View File
@@ -1,10 +1,10 @@
import RectangleROIOptions from './Panels/RectangleROIOptions';
export default function getToolbarModule({ commandsManager, servicesManager }) {
export default function getToolbarModule() {
return [
{
name: 'tmtv.RectangleROIThresholdOptions',
defaultComponent: () => RectangleROIOptions({ commandsManager, servicesManager }),
defaultComponent: RectangleROIOptions,
},
];
}
+24 -16
View File
@@ -1,9 +1,17 @@
import { cache } from '@cornerstonejs/core';
import * as csTools from '@cornerstonejs/tools';
function getRoiStats(referencedVolume, annotations) {
// roiStats
const { imageData } = referencedVolume;
const values = imageData.getPointData().getScalars().getData();
function getRoiStats(displaySet, annotations) {
const { imageIds } = displaySet;
const ptVolumeInfo = cache.getVolumeContainingImageId(imageIds[0]);
if (!ptVolumeInfo) {
throw new Error('No volume found for display set');
}
const { volume } = ptVolumeInfo;
const { voxelManager } = volume;
// Todo: add support for other strategies
const { fn, baseValue } = _getStrategyFn('max');
@@ -11,25 +19,25 @@ function getRoiStats(referencedVolume, annotations) {
const boundsIJK = csTools.utilities.rectangleROITool.getBoundsIJKFromRectangleAnnotations(
annotations,
referencedVolume
volume
);
const [[iMin, iMax], [jMin, jMax], [kMin, kMax]] = boundsIJK;
for (let i = iMin; i <= iMax; i++) {
for (let j = jMin; j <= jMax; j++) {
for (let k = kMin; k <= kMax; k++) {
const offset = imageData.computeOffsetIndex([i, j, k]);
value = fn(values[offset], value);
}
// Use the voxelManager's forEach method to iterate over the bounds
voxelManager.forEach(
({ value: voxelValue }) => {
value = fn(voxelValue, value);
},
{
boundsIJK,
}
}
);
return value;
}
function getThresholdValues(
annotationUIDs,
referencedVolumes,
ptDisplaySet,
config
): { ptLower: number; ptUpper: number; ctLower: number; ctUpper: number } {
if (config.strategy === 'range') {
@@ -46,7 +54,7 @@ function getThresholdValues(
csTools.annotation.state.getAnnotation(annotationUID)
);
const ptValue = getRoiStats(referencedVolumes[0], annotations);
const ptValue = getRoiStats(ptDisplaySet, annotations);
return {
ctLower: -Infinity,