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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
+8
-6
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
+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
|
||||
|
||||
@@ -4,12 +4,13 @@ import * as cs from '@cornerstonejs/core';
|
||||
import * as csTools from '@cornerstonejs/tools';
|
||||
import { classes } from '@ohif/core';
|
||||
import getThresholdValues from './utils/getThresholdValue';
|
||||
import calculateSuvPeak from './utils/calculateSUVPeak';
|
||||
import calculateTMTV from './utils/calculateTMTV';
|
||||
import createAndDownloadTMTVReport from './utils/createAndDownloadTMTVReport';
|
||||
|
||||
import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructureSet';
|
||||
|
||||
import { getWebWorkerManager } from '@cornerstonejs/core';
|
||||
|
||||
const metadataProvider = classes.MetadataProvider;
|
||||
const RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS = [
|
||||
'RectangleROIStartEndThreshold',
|
||||
@@ -17,6 +18,23 @@ const RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS = [
|
||||
];
|
||||
const LABELMAP = csTools.Enums.SegmentationRepresentations.Labelmap;
|
||||
|
||||
const workerManager = getWebWorkerManager();
|
||||
|
||||
const options = {
|
||||
maxWorkerInstances: 1,
|
||||
autoTerminateOnIdle: {
|
||||
enabled: true,
|
||||
idleTimeThreshold: 3000,
|
||||
},
|
||||
};
|
||||
|
||||
// Register the task
|
||||
const workerFn = () => {
|
||||
return new Worker(new URL('./utils/calculateSUVPeakWorker.js', import.meta.url), {
|
||||
name: 'suv-peak-worker', // name used by the browser to name the worker
|
||||
});
|
||||
};
|
||||
|
||||
const commandsModule = ({ servicesManager, commandsManager, extensionManager }) => {
|
||||
const {
|
||||
viewportGridService,
|
||||
@@ -265,7 +283,10 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
{ overwrite: true, segmentIndex }
|
||||
);
|
||||
},
|
||||
calculateSuvPeak: ({ labelmap, segmentIndex }) => {
|
||||
calculateSuvPeak: async ({ labelmap, segmentIndex }) => {
|
||||
// if we put it in the top, it will appear in other modes
|
||||
workerManager.registerWorker('suv-peak-worker', workerFn, options);
|
||||
|
||||
const { referencedVolumeId } = labelmap;
|
||||
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
||||
|
||||
@@ -277,7 +298,31 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
csTools.annotation.state.getAnnotation(annotationUID)
|
||||
);
|
||||
|
||||
const suvPeak = calculateSuvPeak(labelmap, referencedVolume, annotations, segmentIndex);
|
||||
const labelmapProps = {
|
||||
dimensions: labelmap.dimensions,
|
||||
origin: labelmap.origin,
|
||||
direction: labelmap.direction,
|
||||
spacing: labelmap.spacing,
|
||||
scalarData: labelmap.scalarData,
|
||||
metadata: labelmap.metadata,
|
||||
};
|
||||
|
||||
const referenceVolumeProps = {
|
||||
dimensions: referencedVolume.dimensions,
|
||||
origin: referencedVolume.origin,
|
||||
direction: referencedVolume.direction,
|
||||
spacing: referencedVolume.spacing,
|
||||
scalarData: referencedVolume.scalarData,
|
||||
metadata: referencedVolume.metadata,
|
||||
};
|
||||
|
||||
const suvPeak = await workerManager.executeTask('suv-peak-worker', 'calculateSuvPeak', {
|
||||
labelmapProps,
|
||||
referenceVolumeProps,
|
||||
annotations,
|
||||
segmentIndex,
|
||||
});
|
||||
|
||||
return {
|
||||
suvPeak: suvPeak.mean,
|
||||
suvMax: suvPeak.max,
|
||||
|
||||
+37
-26
@@ -1,18 +1,32 @@
|
||||
import { Types } from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
||||
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
|
||||
import { expose } from 'comlink';
|
||||
|
||||
type AnnotationsForThresholding = {
|
||||
data: {
|
||||
handles: {
|
||||
points: Types.Point3[];
|
||||
};
|
||||
cachedStats?: {
|
||||
projectionPoints?: Types.Point3[][];
|
||||
};
|
||||
const createVolume = ({ dimensions, origin, direction, spacing, scalarData, metadata }) => {
|
||||
const imageData = vtkImageData.newInstance();
|
||||
imageData.setDimensions(dimensions);
|
||||
imageData.setOrigin(origin);
|
||||
imageData.setDirection(direction);
|
||||
imageData.setSpacing(spacing);
|
||||
|
||||
const scalarArray = vtkDataArray.newInstance({
|
||||
name: 'Pixels',
|
||||
numberOfComponents: 1,
|
||||
values: scalarData,
|
||||
});
|
||||
|
||||
imageData.getPointData().setScalars(scalarArray);
|
||||
|
||||
imageData.modified();
|
||||
|
||||
return {
|
||||
imageData,
|
||||
metadata,
|
||||
getScalarData: () => scalarData,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This method calculates the SUV peak on a segmented ROI from a reference PET
|
||||
* volume. If a rectangle annotation is provided, the peak is calculated within that
|
||||
@@ -25,17 +39,10 @@ type AnnotationsForThresholding = {
|
||||
* @param segmentIndex The index of the segment to use for masking
|
||||
* @returns
|
||||
*/
|
||||
function calculateSuvPeak(
|
||||
labelmap: Types.IImageVolume,
|
||||
referenceVolume: Types.IImageVolume,
|
||||
annotations?: AnnotationsForThresholding[],
|
||||
segmentIndex = 1
|
||||
): {
|
||||
max: number;
|
||||
maxIJK: Types.Point3;
|
||||
maxLPS: Types.Point3;
|
||||
mean: number;
|
||||
} {
|
||||
function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, segmentIndex = 1 }) {
|
||||
const labelmap = createVolume(labelmapProps);
|
||||
const referenceVolume = createVolume(referenceVolumeProps);
|
||||
|
||||
if (referenceVolume.metadata.Modality !== 'PT') {
|
||||
return;
|
||||
}
|
||||
@@ -59,7 +66,7 @@ function calculateSuvPeak(
|
||||
const rectangleCornersIJK = pointsToUse.map(world => {
|
||||
const ijk = vec3.fromValues(0, 0, 0);
|
||||
referenceVolumeImageData.worldToIndex(world, ijk);
|
||||
return ijk as Types.Point3;
|
||||
return ijk;
|
||||
});
|
||||
|
||||
boundsIJK = utilities.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions);
|
||||
@@ -88,7 +95,7 @@ function calculateSuvPeak(
|
||||
|
||||
utilities.pointInShapeCallback(labelmapImageData, () => true, callback, boundsIJK);
|
||||
|
||||
const direction = labelmapImageData.getDirection().slice(0, 3) as Types.Point3;
|
||||
const direction = labelmapImageData.getDirection().slice(0, 3);
|
||||
|
||||
/**
|
||||
* 2. Find the bottom and top of the great circle for the second sphere (1cc sphere)
|
||||
@@ -100,10 +107,10 @@ function calculateSuvPeak(
|
||||
const secondaryCircleWorld = vec3.create();
|
||||
const bottomWorld = vec3.create();
|
||||
const topWorld = vec3.create();
|
||||
referenceVolumeImageData.indexToWorld(maxIJK as vec3, secondaryCircleWorld);
|
||||
referenceVolumeImageData.indexToWorld(maxIJK, secondaryCircleWorld);
|
||||
vec3.scaleAndAdd(bottomWorld, secondaryCircleWorld, direction, -diameter / 2);
|
||||
vec3.scaleAndAdd(topWorld, secondaryCircleWorld, direction, diameter / 2);
|
||||
const suvPeakCirclePoints = [bottomWorld, topWorld] as [Types.Point3, Types.Point3];
|
||||
const suvPeakCirclePoints = [bottomWorld, topWorld];
|
||||
|
||||
/**
|
||||
* 3. Find the Mean and Max of the 1cc sphere centered on the suv Max of the previous
|
||||
@@ -132,4 +139,8 @@ function calculateSuvPeak(
|
||||
};
|
||||
}
|
||||
|
||||
export default calculateSuvPeak;
|
||||
const obj = {
|
||||
calculateSuvPeak,
|
||||
};
|
||||
|
||||
expose(obj);
|
||||
@@ -0,0 +1,64 @@
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
|
||||
export const handleROIThresholding = async ({
|
||||
segmentationId,
|
||||
commandsManager,
|
||||
segmentationService,
|
||||
config = {},
|
||||
}) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
// re-calculating the cached stats for the active segmentation
|
||||
const updatedPerSegmentCachedStats = {};
|
||||
segmentation.segments = await Promise.all(
|
||||
segmentation.segments.map(async segment => {
|
||||
if (!segment || !segment.segmentIndex) {
|
||||
return segment;
|
||||
}
|
||||
|
||||
const labelmap = cache.getVolume(segmentationId);
|
||||
|
||||
const segmentIndex = segment.segmentIndex;
|
||||
|
||||
const lesionStats = commandsManager.run('getLesionStats', { labelmap, segmentIndex });
|
||||
const suvPeak = await commandsManager.run('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 = commandsManager.run('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
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user