OHIF with Cornerstone3D 2.0 (#4347)
This commit is contained in:
1 parent
8981a61b8c
commit
7ce7127d7c
803 files changed
+32760
-15893
No files matched your search
+45
-140
@@ -1,171 +1,76 @@
|
||||
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 React, { useEffect } from 'react';
|
||||
import { useActiveViewportSegmentationRepresentations } from '@ohif/extension-cornerstone';
|
||||
import { handleROIThresholding } from '../../utils/handleROIThresholding';
|
||||
import { debounce } from '@ohif/core/src/utils';
|
||||
|
||||
export default function PanelRoiThresholdSegmentation({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}: withAppTypes) {
|
||||
const { segmentationService, uiNotificationService } = servicesManager.services;
|
||||
const { t } = useTranslation('PanelSUVExport');
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const { segmentationsWithRepresentations: segmentationsInfo } =
|
||||
useActiveViewportSegmentationRepresentations({ servicesManager });
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
const [activeSegmentation, setActiveSegmentation] = useState(null);
|
||||
|
||||
/**
|
||||
* Update UI based on segmentation changes (added, removed, updated)
|
||||
*/
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
||||
const subscriptions = [];
|
||||
const segmentationIds = segmentationsInfo.map(
|
||||
segmentationInfo => segmentationInfo.segmentation.segmentationId
|
||||
);
|
||||
|
||||
[added, updated, removed].forEach(evt => {
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
|
||||
const activeSegmentation = segmentations.filter(seg => seg.isActive);
|
||||
setActiveSegmentation(activeSegmentation[0]);
|
||||
});
|
||||
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
const initialRun = async () => {
|
||||
for (const segmentationId of segmentationIds) {
|
||||
await handleROIThresholding({
|
||||
segmentationId,
|
||||
commandsManager,
|
||||
segmentationService,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
initialRun();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = async evt => {
|
||||
const { detail } = evt;
|
||||
const { segmentationId } = detail;
|
||||
|
||||
if (!segmentationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const debouncedHandleROIThresholding = debounce(async eventDetail => {
|
||||
const { segmentationId } = eventDetail;
|
||||
await handleROIThresholding({
|
||||
segmentationId,
|
||||
commandsManager,
|
||||
segmentationService,
|
||||
});
|
||||
}, 100);
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
const dataModifiedCallback = eventDetail => {
|
||||
debouncedHandleROIThresholding(eventDetail);
|
||||
};
|
||||
|
||||
eventTarget.addEventListenerDebounced(Enums.Events.SEGMENTATION_DATA_MODIFIED, callback, 250);
|
||||
const dataModifiedSubscription = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
|
||||
dataModifiedCallback
|
||||
);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListenerDebounced(Enums.Events.SEGMENTATION_DATA_MODIFIED, callback);
|
||||
dataModifiedSubscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
}, [commandsManager, segmentationService]);
|
||||
|
||||
if (!activeSegmentation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tmtvValue = activeSegmentation.cachedStats?.tmtv?.value || null;
|
||||
const config = activeSegmentation.cachedStats?.tmtv?.config || {};
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Export CSV',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTMTVReportCSV', {
|
||||
segmentations,
|
||||
tmtv: tmtvValue,
|
||||
config,
|
||||
});
|
||||
},
|
||||
disabled: tmtvValue === null,
|
||||
},
|
||||
{
|
||||
label: 'Export RT Report',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('createTMTVRTReport');
|
||||
},
|
||||
disabled: tmtvValue === null,
|
||||
},
|
||||
];
|
||||
// Find the first segmentation with a TMTV value since all of them have the same value
|
||||
const tmtvSegmentation = segmentationsInfo.find(
|
||||
info => info.segmentation.cachedStats?.tmtv !== undefined
|
||||
);
|
||||
const tmtvValue = tmtvSegmentation?.segmentation.cachedStats?.tmtv;
|
||||
|
||||
return (
|
||||
<>
|
||||
<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 flex items-baseline justify-between px-2 py-1">
|
||||
<span className="text-base font-bold uppercase tracking-widest text-white">
|
||||
{'TMTV:'}
|
||||
</span>
|
||||
<div className="text-white">{`${tmtvValue} mL`}</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-2 flex justify-center">
|
||||
<ActionButtons
|
||||
actions={actions}
|
||||
t={t}
|
||||
/>
|
||||
<div className="mt-2 mb-10 flex flex-col">
|
||||
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
||||
{tmtvValue !== null && tmtvValue !== undefined ? (
|
||||
<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>
|
||||
<div className="text-white">{`${tmtvValue.toFixed(3)} mL`}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className="absolute bottom-1 left-[50px] flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
|
||||
onClick={() => {
|
||||
// navigate to a url in a new tab
|
||||
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
width="15px"
|
||||
height="15px"
|
||||
name={'info'}
|
||||
className={'text-primary-active ml-4 mr-3'}
|
||||
/>
|
||||
<span>{'User Guide'}</span>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PanelRoiThresholdSegmentation.propTypes = {
|
||||
commandsManager: PropTypes.shape({
|
||||
runCommand: PropTypes.func.isRequired,
|
||||
}),
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
segmentationService: PropTypes.shape({
|
||||
getSegmentation: PropTypes.func.isRequired,
|
||||
getSegmentations: PropTypes.func.isRequired,
|
||||
toggleSegmentationVisibility: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
+4
-8
@@ -9,14 +9,10 @@ function segmentationItemEditHandler({ id, servicesManager }: withAppTypes) {
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save': {
|
||||
segmentationService.addOrUpdateSegmentation(
|
||||
{
|
||||
...segmentation,
|
||||
...value,
|
||||
},
|
||||
false, // don't suppress event
|
||||
true // it should update cornerstone
|
||||
);
|
||||
segmentationService.addOrUpdateSegmentation({
|
||||
...segmentation,
|
||||
...value,
|
||||
});
|
||||
}
|
||||
}
|
||||
uiDialogService.dismiss({ id: 'enter-annotation' });
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
PanelSegmentation,
|
||||
useActiveViewportSegmentationRepresentations,
|
||||
} from '@ohif/extension-cornerstone';
|
||||
import { Button, Icons } from '@ohif/ui-next';
|
||||
|
||||
export default function PanelTMTV({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
}: withAppTypes) {
|
||||
return (
|
||||
<>
|
||||
<PanelSegmentation
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={configuration}
|
||||
>
|
||||
<ExportCSV
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
</PanelSegmentation>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const ExportCSV = ({ servicesManager, commandsManager }: withAppTypes) => {
|
||||
const { segmentationsWithRepresentations: representations } =
|
||||
useActiveViewportSegmentationRepresentations({ servicesManager });
|
||||
|
||||
const tmtv = representations[0]?.segmentation.cachedStats?.tmtv;
|
||||
|
||||
const segmentations = representations.map(representation => representation.segmentation);
|
||||
|
||||
if (!segmentations.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-8 w-full items-center rounded pr-0.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pl-1.5"
|
||||
onClick={() => {
|
||||
commandsManager.runCommand('exportTMTVReportCSV', {
|
||||
segmentations,
|
||||
tmtv,
|
||||
config: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icons.Download />
|
||||
<span className="pl-1">CSV</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -75,7 +75,7 @@ function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes)
|
||||
}, [selectedSegmentationId, config]);
|
||||
|
||||
useEffect(() => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const segmentations = segmentationService.getSegmentationRepresentations();
|
||||
|
||||
if (!segmentations.length) {
|
||||
return;
|
||||
@@ -90,13 +90,12 @@ function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes)
|
||||
*/
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||
const updated = segmentationService.EVENTS.SEGMENTATION_MODIFIED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, updated].forEach(evt => {
|
||||
[updated].forEach(evt => {
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const segmentations = segmentationService.getSegmentationRepresentations();
|
||||
|
||||
if (!segmentations.length) {
|
||||
return;
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import OHIF from '@ohif/core';
|
||||
import * as cs from '@cornerstonejs/core';
|
||||
import * as csTools from '@cornerstonejs/tools';
|
||||
import { classes } from '@ohif/core';
|
||||
import getThresholdValues from './utils/getThresholdValue';
|
||||
import calculateTMTV from './utils/calculateTMTV';
|
||||
import createAndDownloadTMTVReport from './utils/createAndDownloadTMTVReport';
|
||||
|
||||
import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructureSet';
|
||||
|
||||
import { getWebWorkerManager } from '@cornerstonejs/core';
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
|
||||
const { SegmentationRepresentations } = Enums;
|
||||
|
||||
const metadataProvider = classes.MetadataProvider;
|
||||
const ROI_THRESHOLD_MANUAL_TOOL_IDS = [
|
||||
'RectangleROIStartEndThreshold',
|
||||
'RectangleROIThreshold',
|
||||
'CircleROIStartEndThreshold'
|
||||
'CircleROIStartEndThreshold',
|
||||
];
|
||||
const LABELMAP = csTools.Enums.SegmentationRepresentations.Labelmap;
|
||||
|
||||
const workerManager = getWebWorkerManager();
|
||||
|
||||
@@ -36,6 +36,28 @@ const workerFn = () => {
|
||||
});
|
||||
};
|
||||
|
||||
function getVolumesFromSegmentation(segmentationId) {
|
||||
const csSegmentation = csTools.segmentation.state.getSegmentation(segmentationId);
|
||||
const labelmapData = csSegmentation.representationData[
|
||||
SegmentationRepresentations.Labelmap
|
||||
] as csTools.Types.LabelmapToolOperationDataVolume;
|
||||
|
||||
const { volumeId, referencedVolumeId } = labelmapData;
|
||||
const labelmapVolume = cs.cache.getVolume(volumeId);
|
||||
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
||||
|
||||
return { labelmapVolume, referencedVolume };
|
||||
}
|
||||
|
||||
function getLabelmapVolumeFromSegmentation(segmentation) {
|
||||
const { representationData } = segmentation;
|
||||
const { volumeId } = representationData[
|
||||
SegmentationRepresentations.Labelmap
|
||||
] as csTools.Types.LabelmapToolOperationDataVolume;
|
||||
|
||||
return cs.cache.getVolume(volumeId);
|
||||
}
|
||||
|
||||
const commandsModule = ({ servicesManager, commandsManager, extensionManager }: withAppTypes) => {
|
||||
const {
|
||||
viewportGridService,
|
||||
@@ -60,20 +82,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
return enabledElement;
|
||||
}
|
||||
|
||||
function _getMatchedViewportsToolGroupIds() {
|
||||
const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
|
||||
const toolGroupIds = [];
|
||||
viewportMatchDetails.forEach(viewport => {
|
||||
const { viewportOptions } = viewport;
|
||||
const { toolGroupId } = viewportOptions;
|
||||
if (toolGroupIds.indexOf(toolGroupId) === -1) {
|
||||
toolGroupIds.push(toolGroupId);
|
||||
}
|
||||
});
|
||||
|
||||
return toolGroupIds;
|
||||
}
|
||||
|
||||
function _getAnnotationsSelectedByToolNames(toolNames) {
|
||||
return toolNames.reduce((allAnnotationUIDs, toolName) => {
|
||||
const annotationUIDs =
|
||||
@@ -102,7 +110,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
}
|
||||
|
||||
ptDisplaySet = displaySets.find(displaySet => displaySet.Modality === 'PT');
|
||||
|
||||
if (ptDisplaySet) {
|
||||
break;
|
||||
}
|
||||
@@ -142,55 +149,48 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
createNewLabelmapFromPT: async ({ label }) => {
|
||||
// Create a segmentation of the same resolution as the source data
|
||||
// using volumeLoader.createAndCacheDerivedVolume.
|
||||
|
||||
const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
|
||||
|
||||
const ptDisplaySet = actions.getMatchingPTDisplaySet({
|
||||
viewportMatchDetails,
|
||||
});
|
||||
|
||||
let withPTViewportId = null;
|
||||
|
||||
for (const [viewportId, { displaySetsInfo }] of viewportMatchDetails.entries()) {
|
||||
const isPT = displaySetsInfo.some(
|
||||
({ displaySetInstanceUID }) =>
|
||||
displaySetInstanceUID === ptDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (isPT) {
|
||||
withPTViewportId = viewportId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ptDisplaySet) {
|
||||
uiNotificationService.error('No matching PT display set found');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSegmentations = segmentationService.getSegmentations();
|
||||
const currentSegmentations =
|
||||
segmentationService.getSegmentationRepresentations(withPTViewportId);
|
||||
|
||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||
ptDisplaySet.displaySetInstanceUID,
|
||||
{ label: `Segmentation ${currentSegmentations.length + 1}` }
|
||||
);
|
||||
const displaySet = displaySetService.getDisplaySetByUID(ptDisplaySet.displaySetInstanceUID);
|
||||
|
||||
// Add Segmentation to all toolGroupIds in the viewer
|
||||
const toolGroupIds = _getMatchedViewportsToolGroupIds();
|
||||
const representationType = LABELMAP;
|
||||
|
||||
for (const toolGroupId of toolGroupIds) {
|
||||
const hydrateSegmentation = true;
|
||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId,
|
||||
hydrateSegmentation,
|
||||
representationType
|
||||
);
|
||||
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||
}
|
||||
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
segmentIndex: 1,
|
||||
properties: {
|
||||
label: 'Segment 1',
|
||||
},
|
||||
const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
|
||||
label: `Segmentation ${currentSegmentations.length + 1}`,
|
||||
segments: { 1: { label: 'Segment 1', active: true } },
|
||||
});
|
||||
|
||||
segmentationService.addSegmentationRepresentation(withPTViewportId, {
|
||||
segmentationId,
|
||||
});
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
setSegmentationActiveForToolGroups: ({ segmentationId }) => {
|
||||
const toolGroupIds = _getMatchedViewportsToolGroupIds();
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||
});
|
||||
},
|
||||
thresholdSegmentationByRectangleROITool: ({ segmentationId, config, segmentIndex }) => {
|
||||
const segmentation = csTools.segmentation.state.getSegmentation(segmentationId);
|
||||
|
||||
@@ -201,12 +201,12 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
const ctDisplaySet = matchDetails.get('ctDisplaySet');
|
||||
const ctVolumeId = `${volumeLoaderScheme}:${ctDisplaySet.displaySetInstanceUID}`; // VolumeId with loader id + volume id
|
||||
|
||||
const { volumeId: segVolumeId } = representationData[LABELMAP];
|
||||
const { volumeId: segVolumeId } = representationData[
|
||||
SegmentationRepresentations.Labelmap
|
||||
] as csTools.Types.LabelmapToolOperationDataVolume;
|
||||
const { referencedVolumeId } = cs.cache.getVolume(segVolumeId);
|
||||
|
||||
const annotationUIDs = _getAnnotationsSelectedByToolNames(
|
||||
ROI_THRESHOLD_MANUAL_TOOL_IDS
|
||||
);
|
||||
const annotationUIDs = _getAnnotationsSelectedByToolNames(ROI_THRESHOLD_MANUAL_TOOL_IDS);
|
||||
|
||||
if (annotationUIDs.length === 0) {
|
||||
uiNotificationService.show({
|
||||
@@ -284,16 +284,21 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
{ overwrite: true, segmentIndex }
|
||||
);
|
||||
},
|
||||
calculateSuvPeak: async ({ labelmap, segmentIndex }) => {
|
||||
calculateSuvPeak: async ({ segmentationId, segmentIndex }) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
const { representationData } = segmentation;
|
||||
const { volumeId, referencedVolumeId } = representationData[
|
||||
SegmentationRepresentations.Labelmap
|
||||
] as csTools.Types.LabelmapToolOperationDataVolume;
|
||||
|
||||
const labelmap = cs.cache.getVolume(volumeId);
|
||||
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
||||
|
||||
// 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);
|
||||
|
||||
const annotationUIDs = _getAnnotationsSelectedByToolNames(
|
||||
ROI_THRESHOLD_MANUAL_TOOL_IDS
|
||||
);
|
||||
const annotationUIDs = _getAnnotationsSelectedByToolNames(ROI_THRESHOLD_MANUAL_TOOL_IDS);
|
||||
|
||||
const annotations = annotationUIDs.map(annotationUID =>
|
||||
csTools.annotation.state.getAnnotation(annotationUID)
|
||||
@@ -304,8 +309,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
origin: labelmap.origin,
|
||||
direction: labelmap.direction,
|
||||
spacing: labelmap.spacing,
|
||||
scalarData: labelmap.scalarData,
|
||||
metadata: labelmap.metadata,
|
||||
scalarData: labelmap.voxelManager.getCompleteScalarDataArray(),
|
||||
};
|
||||
|
||||
const referenceVolumeProps = {
|
||||
@@ -313,8 +318,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
origin: referencedVolume.origin,
|
||||
direction: referencedVolume.direction,
|
||||
spacing: referencedVolume.spacing,
|
||||
scalarData: referencedVolume.scalarData,
|
||||
metadata: referencedVolume.metadata,
|
||||
scalarData: referencedVolume.voxelManager.getCompleteScalarDataArray(),
|
||||
};
|
||||
|
||||
// metadata in annotations has enabledElement which is not serializable
|
||||
@@ -335,12 +340,13 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
};
|
||||
});
|
||||
|
||||
const suvPeak = await workerManager.executeTask('suv-peak-worker', 'calculateSuvPeak', {
|
||||
labelmapProps,
|
||||
referenceVolumeProps,
|
||||
annotations: annotationsToSend,
|
||||
segmentIndex,
|
||||
});
|
||||
const suvPeak =
|
||||
(await workerManager.executeTask('suv-peak-worker', 'calculateSuvPeak', {
|
||||
labelmapProps,
|
||||
referenceVolumeProps,
|
||||
annotations: annotationsToSend,
|
||||
segmentIndex,
|
||||
})) || {};
|
||||
|
||||
return {
|
||||
suvPeak: suvPeak.mean,
|
||||
@@ -349,38 +355,40 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
suvMaxLPS: suvPeak.maxLPS,
|
||||
};
|
||||
},
|
||||
getLesionStats: ({ labelmap, segmentIndex = 1 }) => {
|
||||
const { scalarData, spacing } = labelmap;
|
||||
const referencedScalarData = cs.cache.getVolume(labelmap.referencedVolumeId).getScalarData();
|
||||
getLesionStats: ({ segmentationId, segmentIndex = 1 }) => {
|
||||
const { labelmapVolume, referencedVolume } = getVolumesFromSegmentation(segmentationId);
|
||||
const { voxelManager: segVoxelManager, imageData, spacing } = labelmapVolume;
|
||||
const { voxelManager: refVoxelManager } = referencedVolume;
|
||||
|
||||
let segmentationMax = -Infinity;
|
||||
let segmentationMin = Infinity;
|
||||
const segmentationValues = [];
|
||||
|
||||
let voxelCount = 0;
|
||||
for (let i = 0; i < scalarData.length; i++) {
|
||||
if (scalarData[i] === segmentIndex) {
|
||||
const value = referencedScalarData[i];
|
||||
segmentationValues.push(value);
|
||||
if (value > segmentationMax) {
|
||||
segmentationMax = value;
|
||||
|
||||
const callback = ({ value, index }) => {
|
||||
if (value === segmentIndex) {
|
||||
const refValue = refVoxelManager.getAtIndex(index) as number;
|
||||
segmentationValues.push(refValue);
|
||||
if (refValue > segmentationMax) {
|
||||
segmentationMax = refValue;
|
||||
}
|
||||
if (value < segmentationMin) {
|
||||
segmentationMin = value;
|
||||
if (refValue < segmentationMin) {
|
||||
segmentationMin = refValue;
|
||||
}
|
||||
voxelCount++;
|
||||
}
|
||||
}
|
||||
const mean = segmentationValues.reduce((a, b) => a + b, 0) / voxelCount;
|
||||
};
|
||||
|
||||
segVoxelManager.forEach(callback, { imageData });
|
||||
const mean = segmentationValues.reduce((a, b) => a + b, 0) / voxelCount;
|
||||
const stats = {
|
||||
minValue: segmentationMin,
|
||||
maxValue: segmentationMax,
|
||||
meanValue: mean,
|
||||
stdValue: Math.sqrt(
|
||||
segmentationValues
|
||||
.map((k) => (k - mean) ** 2)
|
||||
.reduce((acc, curr) => acc + curr, 0) / voxelCount),
|
||||
segmentationValues.map(k => (k - mean) ** 2).reduce((acc, curr) => acc + curr, 0) /
|
||||
voxelCount
|
||||
),
|
||||
volume: voxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3,
|
||||
};
|
||||
|
||||
@@ -393,21 +401,30 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
lesionGlyoclysisStats: volume * meanValue,
|
||||
};
|
||||
},
|
||||
calculateTMTV: ({ segmentations }) => {
|
||||
const labelmaps = segmentations.map(s => segmentationService.getLabelmapVolume(s.id));
|
||||
calculateTMTV: async ({ segmentations }) => {
|
||||
const labelmapProps = segmentations.map(segmentation => {
|
||||
const labelmap = getLabelmapVolumeFromSegmentation(segmentation);
|
||||
return {
|
||||
dimensions: labelmap.dimensions,
|
||||
spacing: labelmap.spacing,
|
||||
scalarData: labelmap.voxelManager.getCompleteScalarDataArray(),
|
||||
origin: labelmap.origin,
|
||||
direction: labelmap.direction,
|
||||
};
|
||||
});
|
||||
|
||||
if (!labelmaps.length) {
|
||||
if (!labelmapProps.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return calculateTMTV(labelmaps);
|
||||
return await workerManager.executeTask('suv-peak-worker', 'calculateTMTV', labelmapProps);
|
||||
},
|
||||
exportTMTVReportCSV: ({ segmentations, tmtv, config, options }) => {
|
||||
exportTMTVReportCSV: async ({ segmentations, tmtv, config, options }) => {
|
||||
const segReport = commandsManager.runCommand('getSegmentationCSVReport', {
|
||||
segmentations,
|
||||
});
|
||||
|
||||
const tlg = actions.getTotalLesionGlycolysis({ segmentations });
|
||||
const tlg = await actions.getTotalLesionGlycolysis({ segmentations });
|
||||
const additionalReportRows = [
|
||||
{ key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } },
|
||||
{ key: 'Threshold Configuration', value: { ...config } },
|
||||
@@ -422,51 +439,34 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
|
||||
createAndDownloadTMTVReport(segReport, additionalReportRows, options);
|
||||
},
|
||||
getTotalLesionGlycolysis: ({ segmentations }) => {
|
||||
const labelmapVolumes = segmentations.map(s => segmentationService.getLabelmapVolume(s.id));
|
||||
getTotalLesionGlycolysis: async ({ segmentations }) => {
|
||||
const labelmapProps = segmentations.map(segmentation => {
|
||||
const labelmap = getLabelmapVolumeFromSegmentation(segmentation);
|
||||
return {
|
||||
dimensions: labelmap.dimensions,
|
||||
spacing: labelmap.spacing,
|
||||
scalarData: labelmap.voxelManager.getCompleteScalarDataArray(),
|
||||
origin: labelmap.origin,
|
||||
direction: labelmap.direction,
|
||||
};
|
||||
});
|
||||
|
||||
let mergedLabelmap;
|
||||
// merge labelmap will through an error if labels maps are not the same size
|
||||
// or same direction or ....
|
||||
try {
|
||||
mergedLabelmap =
|
||||
csTools.utilities.segmentation.createMergedLabelmapForIndex(labelmapVolumes);
|
||||
} catch (e) {
|
||||
console.error('commandsModule::getTotalLesionGlycolysis', e);
|
||||
return;
|
||||
}
|
||||
const { referencedVolume: ptVolume } = getVolumesFromSegmentation(
|
||||
segmentations[0].segmentationId
|
||||
);
|
||||
|
||||
// grabbing the first labelmap referenceVolume since it will be the same for all
|
||||
const { referencedVolumeId, spacing } = labelmapVolumes[0];
|
||||
const ptVolumeProps = {
|
||||
dimensions: ptVolume.dimensions,
|
||||
spacing: ptVolume.spacing,
|
||||
scalarData: ptVolume.voxelManager.getCompleteScalarDataArray(),
|
||||
origin: ptVolume.origin,
|
||||
direction: ptVolume.direction,
|
||||
};
|
||||
|
||||
if (!referencedVolumeId) {
|
||||
console.error('commandsModule::getTotalLesionGlycolysis:No referencedVolumeId found');
|
||||
}
|
||||
|
||||
const ptVolume = cs.cache.getVolume(referencedVolumeId);
|
||||
const mergedLabelData = mergedLabelmap.getScalarData();
|
||||
|
||||
if (mergedLabelData.length !== ptVolume.getScalarData().length) {
|
||||
console.error(
|
||||
'commandsModule::getTotalLesionGlycolysis:Labelmap and ptVolume are not the same size'
|
||||
);
|
||||
}
|
||||
|
||||
let suv = 0;
|
||||
let totalLesionVoxelCount = 0;
|
||||
for (let i = 0; i < mergedLabelData.length; i++) {
|
||||
// if not background
|
||||
if (mergedLabelData[i] !== 0) {
|
||||
suv += ptVolume.getScalarData()[i];
|
||||
totalLesionVoxelCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Average SUV for the merged labelmap
|
||||
const averageSuv = suv / totalLesionVoxelCount;
|
||||
|
||||
// total Lesion Glycolysis [suv * ml]
|
||||
return averageSuv * totalLesionVoxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3;
|
||||
return await workerManager.executeTask('suv-peak-worker', 'getTotalLesionGlycolysis', {
|
||||
labelmapProps,
|
||||
referenceVolumeProps: ptVolumeProps,
|
||||
});
|
||||
},
|
||||
setStartSliceForROIThresholdTool: () => {
|
||||
const { viewport } = _getActiveViewportsEnabledElement();
|
||||
@@ -480,7 +480,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
|
||||
const annotation = csTools.annotation.state.getAnnotation(annotationUID);
|
||||
|
||||
// set the current focalpoint
|
||||
// set the current focal point
|
||||
annotation.data.startCoordinate = focalPoint;
|
||||
// IMPORTANT: invalidate the toolData for the cached stat to get updated
|
||||
// and re-calculate the projection points
|
||||
@@ -498,7 +498,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
|
||||
const annotation = csTools.annotation.state.getAnnotation(annotationUID);
|
||||
|
||||
// get the current focalpoint
|
||||
// get the current focal point
|
||||
const focalPointToEnd = viewport.getCamera().focalPoint;
|
||||
annotation.data.endCoordinate = focalPointToEnd;
|
||||
|
||||
@@ -536,27 +536,34 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
const report = {};
|
||||
|
||||
for (const segmentation of segmentations) {
|
||||
const { id, label, cachedStats: data } = segmentation;
|
||||
const { label, segmentationId, representationData } =
|
||||
segmentation as csTools.Types.Segmentation;
|
||||
const id = segmentationId;
|
||||
|
||||
const segReport = { id, label };
|
||||
|
||||
if (!data) {
|
||||
if (!representationData) {
|
||||
report[id] = segReport;
|
||||
continue;
|
||||
}
|
||||
|
||||
Object.keys(data).forEach(key => {
|
||||
if (typeof data[key] !== 'object') {
|
||||
segReport[key] = data[key];
|
||||
} else {
|
||||
Object.keys(data[key]).forEach(subKey => {
|
||||
const newKey = `${key}_${subKey}`;
|
||||
segReport[newKey] = data[key][subKey];
|
||||
});
|
||||
}
|
||||
});
|
||||
const { cachedStats } = segmentation.segments[1] || {}; // Assuming we want stats from the first segment
|
||||
|
||||
const labelmapVolume = segmentationService.getLabelmapVolume(id);
|
||||
if (cachedStats) {
|
||||
Object.entries(cachedStats).forEach(([key, value]) => {
|
||||
if (typeof value !== 'object') {
|
||||
segReport[key] = value;
|
||||
} else {
|
||||
Object.entries(value).forEach(([subKey, subValue]) => {
|
||||
const newKey = `${key}_${subKey}`;
|
||||
segReport[newKey] = subValue;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const labelmapVolume =
|
||||
segmentation.representationData[SegmentationRepresentations.Labelmap];
|
||||
|
||||
if (!labelmapVolume) {
|
||||
report[id] = segReport;
|
||||
@@ -564,9 +571,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
}
|
||||
|
||||
const referencedVolumeId = labelmapVolume.referencedVolumeId;
|
||||
segReport.referencedVolumeId = referencedVolumeId;
|
||||
|
||||
const referencedVolume = segmentationService.getLabelmapVolume(referencedVolumeId);
|
||||
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
||||
|
||||
if (!referencedVolume) {
|
||||
report[id] = segReport;
|
||||
@@ -603,6 +609,11 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
},
|
||||
setFusionPTColormap: ({ toolGroupId, colormap }) => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
|
||||
|
||||
const ptDisplaySet = actions.getMatchingPTDisplaySet({
|
||||
@@ -650,9 +661,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
createNewLabelmapFromPT: {
|
||||
commandFn: actions.createNewLabelmapFromPT,
|
||||
},
|
||||
setSegmentationActiveForToolGroups: {
|
||||
commandFn: actions.setSegmentationActiveForToolGroups,
|
||||
},
|
||||
thresholdSegmentationByRectangleROITool: {
|
||||
commandFn: actions.thresholdSegmentationByRectangleROITool,
|
||||
},
|
||||
|
||||
+20
-5
@@ -18,7 +18,7 @@ import {
|
||||
* image, covering all three rows. It has synchronizers for windowLevel for all CT and PT images, and
|
||||
* also camera synchronizer for each orientation
|
||||
*/
|
||||
const stage1 = {
|
||||
const stage1: AppTypes.HangingProtocol.ProtocolStage = {
|
||||
name: 'default',
|
||||
id: 'default',
|
||||
viewportStructure: {
|
||||
@@ -131,7 +131,7 @@ const stage2 = {
|
||||
* The layout follows a simple grid pattern with 2 rows and 3 columns.
|
||||
* It includes synchronizers as well.
|
||||
*/
|
||||
const stage3 = {
|
||||
const stage3: AppTypes.HangingProtocol.ProtocolStage = {
|
||||
name: '2x3-layout',
|
||||
id: '2x3-layout',
|
||||
viewportStructure: {
|
||||
@@ -153,7 +153,7 @@ const stage3 = {
|
||||
* from the top row spans the full height of both rows.
|
||||
* It includes synchronizers as well.
|
||||
*/
|
||||
const stage4 = {
|
||||
const stage4: AppTypes.HangingProtocol.ProtocolStage = {
|
||||
name: '2x4-layout',
|
||||
id: '2x4-layout',
|
||||
viewportStructure: {
|
||||
@@ -218,7 +218,23 @@ const stage4 = {
|
||||
],
|
||||
};
|
||||
|
||||
const ptCT = {
|
||||
/**
|
||||
* This layout displays three fusion viewports: axial, sagittal, and coronal.
|
||||
* It follows a simple grid pattern with 1 row and 3 columns.
|
||||
*/
|
||||
// const stage0: AppTypes.HangingProtocol.ProtocolStage = {
|
||||
// name: 'Fusion 1x3',
|
||||
// viewportStructure: {
|
||||
// layoutType: 'grid',
|
||||
// properties: {
|
||||
// rows: 1,
|
||||
// columns: 3,
|
||||
// },
|
||||
// },
|
||||
// viewports: [fusionAXIAL, fusionSAGITTAL, fusionCORONAL],
|
||||
// };
|
||||
|
||||
const ptCT: AppTypes.HangingProtocol.Protocol = {
|
||||
id: '@ohif/extension-tmtv.hangingProtocolModule.ptCT',
|
||||
locked: true,
|
||||
name: 'Default',
|
||||
@@ -318,7 +334,6 @@ const ptCT = {
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
stages: [stage1, stage2, stage3, stage4],
|
||||
numberOfPriorsReferenced: -1,
|
||||
};
|
||||
@@ -1,14 +1,10 @@
|
||||
import React from 'react';
|
||||
import { PanelPetSUV, PanelROIThresholdExport } from './Panels';
|
||||
import { Toolbox } from '@ohif/ui-next';
|
||||
|
||||
// TODO:
|
||||
// - No loading UI exists yet
|
||||
// - cancel promises when component is destroyed
|
||||
// - show errors in UI for thumbnails if promise fails
|
||||
import PanelTMTV from './Panels/PanelTMTV';
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||
const wrappedPanelPetSuv = ({}) => {
|
||||
const wrappedPanelPetSuv = () => {
|
||||
return (
|
||||
<PanelPetSUV
|
||||
commandsManager={commandsManager}
|
||||
@@ -18,7 +14,28 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedROIThresholdToolbox = ({}: withAppTypes) => {
|
||||
const wrappedROIThresholdToolbox = () => {
|
||||
return (
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="ROIThresholdToolbox"
|
||||
title="Threshold Tools"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedROIThresholdExport = () => {
|
||||
return (
|
||||
<PanelROIThresholdExport
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedPanelTMTV = () => {
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
@@ -28,13 +45,10 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
buttonSectionId="ROIThresholdToolbox"
|
||||
title="Threshold Tools"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedROIThresholdExport = () => {
|
||||
return (
|
||||
<>
|
||||
<PanelTMTV
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
<PanelROIThresholdExport
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
@@ -51,6 +65,12 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
label: 'Patient Info',
|
||||
component: wrappedPanelPetSuv,
|
||||
},
|
||||
{
|
||||
name: 'tmtv',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
component: wrappedPanelTMTV,
|
||||
},
|
||||
{
|
||||
name: 'tmtvBox',
|
||||
iconName: 'tab-segmentation',
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
import { utilities as cstUtils } 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';
|
||||
|
||||
const createVolume = ({ dimensions, origin, direction, spacing, scalarData, metadata }) => {
|
||||
const createVolume = ({ dimensions, origin, direction, spacing, metadata, scalarData }) => {
|
||||
const imageData = vtkImageData.newInstance();
|
||||
imageData.setDimensions(dimensions);
|
||||
imageData.setOrigin(origin);
|
||||
@@ -21,12 +22,21 @@ const createVolume = ({ dimensions, origin, direction, spacing, scalarData, meta
|
||||
|
||||
imageData.modified();
|
||||
|
||||
const voxelManager = utilities.VoxelManager.createScalarVolumeVoxelManager({
|
||||
scalarData,
|
||||
dimensions,
|
||||
numberOfComponents: 1,
|
||||
});
|
||||
return {
|
||||
imageData,
|
||||
spacing,
|
||||
origin,
|
||||
direction,
|
||||
metadata,
|
||||
getScalarData: () => scalarData,
|
||||
voxelManager,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -40,22 +50,15 @@ const createVolume = ({ dimensions, origin, direction, spacing, scalarData, meta
|
||||
* @returns
|
||||
*/
|
||||
function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, segmentIndex = 1 }) {
|
||||
const labelmap = createVolume(labelmapProps);
|
||||
const referenceVolume = createVolume(referenceVolumeProps);
|
||||
const labelmapInfo = createVolume(labelmapProps);
|
||||
const referenceInfo = createVolume(referenceVolumeProps);
|
||||
|
||||
if (referenceVolume.metadata.Modality !== 'PT') {
|
||||
if (referenceInfo.metadata.Modality !== 'PT') {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelmapData = labelmap.getScalarData();
|
||||
const referenceVolumeData = referenceVolume.getScalarData();
|
||||
|
||||
if (labelmapData.length !== referenceVolumeData.length) {
|
||||
throw new Error('labelmap and referenceVolume must have the same number of pixels');
|
||||
}
|
||||
|
||||
const { dimensions, imageData: labelmapImageData } = labelmap;
|
||||
const { imageData: referenceVolumeImageData } = referenceVolume;
|
||||
const { dimensions, imageData: labelmapImageData } = labelmapInfo;
|
||||
const { imageData: referenceVolumeImageData } = referenceInfo;
|
||||
|
||||
let boundsIJK;
|
||||
// Todo: using the first annotation for now
|
||||
@@ -69,7 +72,7 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se
|
||||
return ijk;
|
||||
});
|
||||
|
||||
boundsIJK = utilities.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions);
|
||||
boundsIJK = cstUtils.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions);
|
||||
}
|
||||
|
||||
let max = 0;
|
||||
@@ -77,14 +80,13 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se
|
||||
let maxLPS = [0, 0, 0];
|
||||
|
||||
const callback = ({ pointIJK, pointLPS }) => {
|
||||
const offset = referenceVolumeImageData.computeOffsetIndex(pointIJK);
|
||||
const value = labelmapData[offset];
|
||||
const value = labelmapInfo.voxelManager.getAtIJKPoint(pointIJK);
|
||||
|
||||
if (value !== segmentIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const referenceValue = referenceVolumeData[offset];
|
||||
const referenceValue = referenceInfo.voxelManager.getAtIJKPoint(pointIJK);
|
||||
|
||||
if (referenceValue > max) {
|
||||
max = referenceValue;
|
||||
@@ -93,7 +95,12 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se
|
||||
}
|
||||
};
|
||||
|
||||
utilities.pointInShapeCallback(labelmapImageData, () => true, callback, boundsIJK);
|
||||
labelmapInfo.voxelManager.forEach(callback, {
|
||||
boundsIJK,
|
||||
imageData: labelmapImageData,
|
||||
isInObject: () => true,
|
||||
returnPoints: true,
|
||||
});
|
||||
|
||||
const direction = labelmapImageData.getDirection().slice(0, 3);
|
||||
|
||||
@@ -123,7 +130,7 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se
|
||||
count += 1;
|
||||
};
|
||||
|
||||
utilities.pointInSurroundingSphereCallback(
|
||||
cstUtils.pointInSurroundingSphereCallback(
|
||||
referenceVolumeImageData,
|
||||
suvPeakCirclePoints,
|
||||
suvPeakMeanCallback
|
||||
@@ -139,8 +146,64 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se
|
||||
};
|
||||
}
|
||||
|
||||
function calculateTMTV(labelmapProps, segmentIndex = 1) {
|
||||
const labelmaps = labelmapProps.map(props => createVolume(props));
|
||||
|
||||
const mergedLabelmap =
|
||||
labelmaps.length === 1
|
||||
? labelmaps[0]
|
||||
: cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps);
|
||||
|
||||
const { imageData, spacing } = mergedLabelmap;
|
||||
const values = imageData.getPointData().getScalars().getData();
|
||||
|
||||
// count non-zero values inside the outputData, this would
|
||||
// consider the overlapping regions to be only counted once
|
||||
const numVoxels = values.reduce((acc, curr) => {
|
||||
if (curr > 0) {
|
||||
return acc + 1;
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2];
|
||||
}
|
||||
|
||||
function getTotalLesionGlycolysis({ labelmapProps, referenceVolumeProps }) {
|
||||
const labelmaps = labelmapProps.map(props => createVolume(props));
|
||||
|
||||
const mergedLabelmap =
|
||||
labelmaps.length === 1
|
||||
? labelmaps[0]
|
||||
: cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps);
|
||||
|
||||
// grabbing the first labelmap referenceVolume since it will be the same for all
|
||||
const { spacing } = labelmaps[0];
|
||||
|
||||
const ptVolume = createVolume(referenceVolumeProps);
|
||||
|
||||
let suv = 0;
|
||||
let totalLesionVoxelCount = 0;
|
||||
const scalarDataLength = mergedLabelmap.voxelManager.getScalarDataLength();
|
||||
for (let i = 0; i < scalarDataLength; i++) {
|
||||
// if not background
|
||||
if (mergedLabelmap.voxelManager.getAtIndex(i) !== 0) {
|
||||
suv += ptVolume.voxelManager.getAtIndex(i);
|
||||
totalLesionVoxelCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Average SUV for the merged labelmap
|
||||
const averageSuv = suv / totalLesionVoxelCount;
|
||||
|
||||
// total Lesion Glycolysis [suv * ml]
|
||||
return averageSuv * totalLesionVoxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3;
|
||||
}
|
||||
|
||||
const obj = {
|
||||
calculateSuvPeak,
|
||||
calculateTMTV,
|
||||
getTotalLesionGlycolysis,
|
||||
};
|
||||
|
||||
expose(obj);
|
||||
@@ -20,17 +20,21 @@ function calculateTMTV(labelmaps: Array<Types.IImageVolume>, segmentIndex = 1):
|
||||
volumeId
|
||||
);
|
||||
|
||||
const { imageData, spacing } = mergedLabelmap;
|
||||
const values = imageData.getPointData().getScalars().getData();
|
||||
const { imageData, spacing, voxelManager } = mergedLabelmap;
|
||||
|
||||
// count non-zero values inside the outputData, this would
|
||||
// consider the overlapping regions to be only counted once
|
||||
const numVoxels = values.reduce((acc, curr) => {
|
||||
if (curr > 0) {
|
||||
return acc + 1;
|
||||
let numVoxels = 0;
|
||||
const callback = ({ value }) => {
|
||||
if (value > 0) {
|
||||
numVoxels += 1;
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
voxelManager.forEach(callback, {
|
||||
imageData,
|
||||
isInObject: () => true,
|
||||
});
|
||||
|
||||
return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2];
|
||||
}
|
||||
|
||||
@@ -1,64 +1,97 @@
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
import { Segment, Segmentation } from '@cornerstonejs/tools/types';
|
||||
import { triggerEvent, eventTarget, Enums } from '@cornerstonejs/core';
|
||||
|
||||
export const handleROIThresholding = async ({
|
||||
segmentationId,
|
||||
commandsManager,
|
||||
segmentationService,
|
||||
config = {},
|
||||
}) => {
|
||||
}: withAppTypes<{
|
||||
segmentationId: string;
|
||||
}>) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, {
|
||||
progress: 0,
|
||||
type: 'Calculate Lesion Stats',
|
||||
id: 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;
|
||||
}
|
||||
for (const [segmentIndex, segment] of Object.entries(segmentation.segments)) {
|
||||
if (!segment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const labelmap = cache.getVolume(segmentationId);
|
||||
const numericSegmentIndex = Number(segmentIndex);
|
||||
|
||||
const segmentIndex = segment.segmentIndex;
|
||||
const lesionStats = await commandsManager.run('getLesionStats', {
|
||||
segmentationId,
|
||||
segmentIndex: numericSegmentIndex,
|
||||
});
|
||||
|
||||
const lesionStats = commandsManager.run('getLesionStats', { labelmap, segmentIndex });
|
||||
const suvPeak = await commandsManager.run('calculateSuvPeak', { labelmap, segmentIndex });
|
||||
const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
|
||||
const suvPeak = await commandsManager.run('calculateSuvPeak', {
|
||||
segmentationId,
|
||||
segmentIndex: numericSegmentIndex,
|
||||
});
|
||||
|
||||
// update segDetails with the suv peak for the active segmentation
|
||||
const cachedStats = {
|
||||
lesionStats,
|
||||
suvPeak,
|
||||
lesionGlyoclysisStats,
|
||||
};
|
||||
const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
|
||||
|
||||
segment.cachedStats = cachedStats;
|
||||
segment.displayText = [
|
||||
`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`,
|
||||
`Volume: ${lesionStats.volume.toFixed(2)} mm3`,
|
||||
];
|
||||
updatedPerSegmentCachedStats[segmentIndex] = cachedStats;
|
||||
// update segDetails with the suv peak for the active segmentation
|
||||
const cachedStats = {
|
||||
lesionStats,
|
||||
suvPeak,
|
||||
lesionGlyoclysisStats,
|
||||
};
|
||||
|
||||
return segment;
|
||||
})
|
||||
);
|
||||
const updatedSegment: Segment = {
|
||||
...segment,
|
||||
cachedStats: {
|
||||
...segment.cachedStats,
|
||||
...cachedStats,
|
||||
},
|
||||
};
|
||||
|
||||
const notYetUpdatedAtSource = true;
|
||||
updatedPerSegmentCachedStats[numericSegmentIndex] = cachedStats;
|
||||
|
||||
segmentation.segments[segmentIndex] = updatedSegment;
|
||||
}
|
||||
|
||||
// all available segmentations
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const tmtv = commandsManager.run('calculateTMTV', { segmentations });
|
||||
const tmtv = await commandsManager.run('calculateTMTV', { segmentations });
|
||||
|
||||
segmentation.cachedStats = Object.assign(segmentation.cachedStats, updatedPerSegmentCachedStats, {
|
||||
tmtv: {
|
||||
value: tmtv.toFixed(3),
|
||||
config: { ...config },
|
||||
},
|
||||
triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, {
|
||||
progress: 100,
|
||||
type: 'Calculate Lesion Stats',
|
||||
id: segmentationId,
|
||||
});
|
||||
|
||||
segmentationService.addOrUpdateSegmentation(
|
||||
{
|
||||
// add the tmtv to all the segment cachedStats, although it is a global
|
||||
// value but we don't have any other way to display it for now
|
||||
// Update all segmentations with the calculated TMTV
|
||||
segmentations.forEach(segmentation => {
|
||||
segmentation.cachedStats = {
|
||||
...segmentation.cachedStats,
|
||||
tmtv,
|
||||
};
|
||||
|
||||
// Update each segment within the segmentation
|
||||
Object.keys(segmentation.segments).forEach(segmentIndex => {
|
||||
segmentation.segments[segmentIndex].cachedStats = {
|
||||
...segmentation.segments[segmentIndex].cachedStats,
|
||||
tmtv,
|
||||
};
|
||||
});
|
||||
|
||||
// Update the segmentation object
|
||||
const updatedSegmentation: Segmentation = {
|
||||
...segmentation,
|
||||
},
|
||||
false, // don't suppress events
|
||||
notYetUpdatedAtSource
|
||||
);
|
||||
segments: {
|
||||
...segmentation.segments,
|
||||
},
|
||||
};
|
||||
|
||||
segmentationService.addOrUpdateSegmentation(updatedSegmentation);
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,22 @@
|
||||
const ctAXIAL = {
|
||||
// Common sync group configurations
|
||||
const cameraPositionSync = (id: string) => ({
|
||||
type: 'cameraPosition',
|
||||
id,
|
||||
source: true,
|
||||
target: true,
|
||||
});
|
||||
|
||||
const hydrateSegSync = {
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
};
|
||||
|
||||
const ctAXIAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ctAXIAL',
|
||||
viewportType: 'volume',
|
||||
@@ -9,12 +27,7 @@ const ctAXIAL = {
|
||||
preset: 'first', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'axialSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
cameraPositionSync('axialSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
@@ -24,6 +37,7 @@ const ctAXIAL = {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
@@ -33,49 +47,14 @@ const ctAXIAL = {
|
||||
],
|
||||
};
|
||||
|
||||
const ctSAGITTAL = {
|
||||
const ctSAGITTAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ctSAGITTAL',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
toolGroupId: 'ctToolGroup',
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'sagittalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
};
|
||||
const ctCORONAL = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ctCORONAL',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: 'ctToolGroup',
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'coronalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
cameraPositionSync('sagittalSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
@@ -85,6 +64,7 @@ const ctCORONAL = {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
@@ -94,7 +74,34 @@ const ctCORONAL = {
|
||||
],
|
||||
};
|
||||
|
||||
const ptAXIAL = {
|
||||
const ctCORONAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ctCORONAL',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: 'ctToolGroup',
|
||||
syncGroups: [
|
||||
cameraPositionSync('coronalSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ptAXIAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ptAXIAL',
|
||||
viewportType: 'volume',
|
||||
@@ -106,12 +113,7 @@ const ptAXIAL = {
|
||||
preset: 'first', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'axialSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
cameraPositionSync('axialSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
@@ -131,6 +133,7 @@ const ptAXIAL = {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
@@ -146,7 +149,7 @@ const ptAXIAL = {
|
||||
],
|
||||
};
|
||||
|
||||
const ptSAGITTAL = {
|
||||
const ptSAGITTAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ptSAGITTAL',
|
||||
viewportType: 'volume',
|
||||
@@ -154,12 +157,7 @@ const ptSAGITTAL = {
|
||||
background: [1, 1, 1],
|
||||
toolGroupId: 'ptToolGroup',
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'sagittalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
cameraPositionSync('sagittalSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
@@ -179,6 +177,7 @@ const ptSAGITTAL = {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
@@ -194,7 +193,7 @@ const ptSAGITTAL = {
|
||||
],
|
||||
};
|
||||
|
||||
const ptCORONAL = {
|
||||
const ptCORONAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ptCORONAL',
|
||||
viewportType: 'volume',
|
||||
@@ -202,12 +201,7 @@ const ptCORONAL = {
|
||||
background: [1, 1, 1],
|
||||
toolGroupId: 'ptToolGroup',
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'coronalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
cameraPositionSync('coronalSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
@@ -227,6 +221,7 @@ const ptCORONAL = {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
@@ -242,7 +237,7 @@ const ptCORONAL = {
|
||||
],
|
||||
};
|
||||
|
||||
const fusionAXIAL = {
|
||||
const fusionAXIAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionAXIAL',
|
||||
viewportType: 'volume',
|
||||
@@ -253,12 +248,7 @@ const fusionAXIAL = {
|
||||
preset: 'first', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'axialSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
cameraPositionSync('axialSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
@@ -284,6 +274,7 @@ const fusionAXIAL = {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
@@ -297,8 +288,8 @@ const fusionAXIAL = {
|
||||
name: 'hsv',
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 0.9 },
|
||||
{ value: 1, opacity: 0.95 },
|
||||
{ value: 0.1, opacity: 0.8 },
|
||||
{ value: 1, opacity: 0.9 },
|
||||
],
|
||||
},
|
||||
voi: {
|
||||
@@ -320,12 +311,7 @@ const fusionSAGITTAL = {
|
||||
// preset: 'middle', // 'first', 'last', 'middle'
|
||||
// },
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'sagittalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
cameraPositionSync('sagittalSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
@@ -351,6 +337,7 @@ const fusionSAGITTAL = {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
@@ -364,8 +351,8 @@ const fusionSAGITTAL = {
|
||||
name: 'hsv',
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 0.9 },
|
||||
{ value: 1, opacity: 0.95 },
|
||||
{ value: 0.1, opacity: 0.8 },
|
||||
{ value: 1, opacity: 0.9 },
|
||||
],
|
||||
},
|
||||
voi: {
|
||||
@@ -387,12 +374,7 @@ const fusionCORONAL = {
|
||||
// preset: 'middle', // 'first', 'last', 'middle'
|
||||
// },
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'coronalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
cameraPositionSync('coronalSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
@@ -418,6 +400,7 @@ const fusionCORONAL = {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
@@ -431,8 +414,8 @@ const fusionCORONAL = {
|
||||
name: 'hsv',
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 0.9 },
|
||||
{ value: 1, opacity: 0.95 },
|
||||
{ value: 0.1, opacity: 0.8 },
|
||||
{ value: 1, opacity: 0.9 },
|
||||
],
|
||||
},
|
||||
voi: {
|
||||
@@ -443,7 +426,7 @@ const fusionCORONAL = {
|
||||
],
|
||||
};
|
||||
|
||||
const mipSAGITTAL = {
|
||||
const mipSAGITTAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'mipSagittal',
|
||||
viewportType: 'volume',
|
||||
@@ -470,6 +453,7 @@ const mipSAGITTAL = {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
|
||||
// Custom props can be used to set custom properties which extensions
|
||||
|
||||
Reference in new issue
Block a user