Skip to content

Commit

Permalink
feat(tmtv-mode): Add Brush tools and move SUV peak calculation to web…
Browse files Browse the repository at this point in the history
… worker (#4053)
  • Loading branch information
sedghi authored Apr 19, 2024
1 parent 8c5ab94 commit 8192e34
Show file tree
Hide file tree
Showing 27 changed files with 593 additions and 212 deletions.
4 changes: 2 additions & 2 deletions extensions/cornerstone-dicom-seg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.70.9",
"@cornerstonejs/core": "^1.70.9",
"@cornerstonejs/adapters": "^1.70.10",
"@cornerstonejs/core": "^1.70.10",
"@kitware/vtk.js": "30.3.3",
"react-color": "^2.19.3"
}
Expand Down
6 changes: 3 additions & 3 deletions extensions/cornerstone-dicom-sr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.70.9",
"@cornerstonejs/core": "^1.70.9",
"@cornerstonejs/tools": "^1.70.9",
"@cornerstonejs/adapters": "^1.70.10",
"@cornerstonejs/core": "^1.70.10",
"@cornerstonejs/tools": "^1.70.10",
"classnames": "^2.3.2"
}
}
6 changes: 3 additions & 3 deletions extensions/cornerstone-dynamic-volume/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^1.70.9",
"@cornerstonejs/streaming-image-volume-loader": "^1.70.9",
"@cornerstonejs/tools": "^1.70.9",
"@cornerstonejs/core": "^1.70.10",
"@cornerstonejs/streaming-image-volume-loader": "^1.70.10",
"@cornerstonejs/tools": "^1.70.10",
"classnames": "^2.3.2"
}
}
6 changes: 3 additions & 3 deletions extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,21 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
return [
{
name: 'dynamic-volume',
iconName: 'group-layers',
iconName: 'tab-4d',
iconLabel: '4D Workflow',
label: '4D Workflow',
component: wrappedDynamicDataPanel,
},
{
name: 'dynamic-toolbox',
iconName: 'group-layers',
iconName: 'tab-4d',
iconLabel: '4D Workflow',
label: 'Dynamic Toolbox',
component: wrappedDynamicToolbox,
},
{
name: 'dynamic-export',
iconName: 'group-layers',
iconName: 'tab-4d',
iconLabel: '4D Workflow',
label: '4D Workflow',
component: wrappedDynamicExport,
Expand Down
10 changes: 5 additions & 5 deletions extensions/cornerstone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.70.9",
"@cornerstonejs/dicom-image-loader": "^1.70.10",
"@icr/polyseg-wasm": "^0.4.0",
"@ohif/core": "3.8.0-beta.86",
"@ohif/ui": "3.8.0-beta.86",
Expand All @@ -55,10 +55,10 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.70.9",
"@cornerstonejs/core": "^1.70.9",
"@cornerstonejs/streaming-image-volume-loader": "^1.70.9",
"@cornerstonejs/tools": "^1.70.9",
"@cornerstonejs/adapters": "^1.70.10",
"@cornerstonejs/core": "^1.70.10",
"@cornerstonejs/streaming-image-volume-loader": "^1.70.10",
"@cornerstonejs/tools": "^1.70.10",
"@icr/polyseg-wasm": "^0.4.0",
"@kitware/vtk.js": "30.3.3",
"html2canvas": "^1.4.1",
Expand Down
4 changes: 2 additions & 2 deletions extensions/cornerstone/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,9 +346,9 @@ function commandsModule({
}
}
},
setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => {
setToolActiveToolbar: ({ value, itemId, toolName, toolGroupIds = [] }) => {
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
const toolName = itemId || value;
toolName = toolName || itemId || value;

toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { getWebWorkerManager } from '@cornerstonejs/core';

const workerManager = getWebWorkerManager();

const options = {
// maxWorkerInstances: 1,
// overwrite: false
autoTerminationOnIdle: 1000,
const WorkerOptions = {
maxWorkerInstances: 1,
autoTerminateOnIdle: {
enabled: true,
idleTimeThreshold: 1000,
},
};

// Register the task
Expand All @@ -15,9 +17,9 @@ const workerFn = () => {
});
};

workerManager.registerWorker('histogram-worker', workerFn, options);

const getViewportVolumeHistogram = async (viewport, volume, options?) => {
workerManager.registerWorker('histogram-worker', workerFn, WorkerOptions);

if (!volume?.loadStatus.loaded) {
return undefined;
}
Expand Down
23 changes: 11 additions & 12 deletions extensions/cornerstone/src/init.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,19 +279,18 @@ export default async function init({
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
colormaps.forEach(registerColormap);

// Create a debounced function that shows the notification
const debouncedShowNotification = debounce(detail => {
uiNotificationService.show({
title: detail.type,
message: detail.message,
type: 'error',
});
}, 300);

// Event listener
eventTarget.addEventListener(EVENTS.ERROR_EVENT, ({ detail }) => {
debouncedShowNotification(detail);
});
eventTarget.addEventListenerDebounced(
EVENTS.ERROR_EVENT,
({ detail }) => {
uiNotificationService.show({
title: detail.type,
message: detail.message,
type: 'error',
});
},
1000
);
}

function CPUModal() {
Expand Down
4 changes: 2 additions & 2 deletions extensions/measurement-tracking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@cornerstonejs/core": "^1.70.9",
"@cornerstonejs/tools": "^1.70.9",
"@cornerstonejs/core": "^1.70.10",
"@cornerstonejs/tools": "^1.70.10",
"@ohif/core": "3.8.0-beta.86",
"@ohif/extension-cornerstone-dicom-sr": "3.8.0-beta.86",
"@ohif/ui": "3.8.0-beta.86",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
});

Expand All @@ -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 || {};

segmentations.forEach(segmentation => {
const { cachedStats } = segmentation;
if (!cachedStats) {
return;
}
useEffect(() => {
const callback = async evt => {
const { detail } = evt;
const { segmentationId } = detail;

// segment 1
const suvPeak = cachedStats?.['1']?.suvPeak?.suvPeak;
if (!segmentationId) {
return;
}

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 = [
{
Expand All @@ -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');
},
Expand All @@ -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>
Expand Down
79 changes: 1 addition & 78 deletions extensions/tmtv/src/Panels/RectangleROIOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 8192e34

Please sign in to comment.