OHIF with Cornerstone3D 2.0 (#4347)

This commit is contained in:
Alireza authored and GitHub committed 2024-11-06 07:15:27 -05:00
1 parent 8981a61b8c
commit 7ce7127d7c
803 files changed
+32760 -15893

No files matched your search

+164 -156
View File
@@ -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,
},