feat(segmentation): segment statistics, labelmap interpolation and segment bidirectional (#4865)

This commit is contained in:
Alireza authored and GitHub committed 2025-03-17 22:34:34 -04:00
1 parent 694f3e9611
commit ec79b5523e
73 files changed
+3603 -1563

No files matched your search

+6 -5
View File
@@ -38,7 +38,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.5.0",
"@cornerstonejs/dicom-image-loader": "^3.7.0",
"@ohif/core": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*",
@@ -51,10 +51,11 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.5.0",
"@cornerstonejs/core": "^3.5.0",
"@cornerstonejs/polymorphic-segmentation": "^3.5.0",
"@cornerstonejs/tools": "^3.5.0",
"@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/polymorphic-segmentation": "^3.7.0",
"@cornerstonejs/labelmap-interpolation": "^3.7.0",
"@cornerstonejs/tools": "^3.7.0",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",
+122 -3
View File
@@ -4,6 +4,7 @@ import {
VolumeViewport,
utilities as csUtils,
Types as CoreTypes,
cache,
BaseVolumeViewport,
} from '@cornerstonejs/core';
import {
@@ -14,6 +15,7 @@ import {
annotation,
} from '@cornerstonejs/tools';
import * as cornerstoneTools from '@cornerstonejs/tools';
import * as labelmapInterpolation from '@cornerstonejs/labelmap-interpolation';
import { Types as OhifTypes, utils } from '@ohif/core';
import i18n from '@ohif/i18n';
@@ -31,6 +33,8 @@ import toggleVOISliceSync from './utils/toggleVOISliceSync';
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
import { toolNames } from './initCornerstoneTools';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats';
import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport';
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync,
@@ -39,7 +43,6 @@ const toggleSyncFunctions = {
function commandsModule({
servicesManager,
extensionManager,
commandsManager,
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
const {
@@ -54,7 +57,9 @@ function commandsModule({
colorbarService,
hangingProtocolService,
syncGroupService,
} = servicesManager.services;
segmentationService,
displaySetService,
} = servicesManager.services as AppTypes.Services;
const { measurementServiceSource } = this;
@@ -67,7 +72,75 @@ function commandsModule({
return toolGroupService.getToolGroupForViewport(viewport.id);
}
function _getActiveSegmentationInfo() {
const viewportId = viewportGridService.getActiveViewportId();
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
const segmentationId = activeSegmentation?.segmentationId;
const activeSegmentIndex = segmentationService.getActiveSegment(viewportId).segmentIndex;
return {
segmentationId,
segmentIndex: activeSegmentIndex,
};
}
const actions = {
runSegmentBidirectional: async ({ segmentationId, segmentIndex } = {}) => {
// Get active segmentation if not specified
const targetSegmentation =
segmentationId && segmentIndex
? { segmentationId, segmentIndex }
: _getActiveSegmentationInfo();
const { segmentationId: targetId, segmentIndex: targetIndex } = targetSegmentation;
// Get bidirectional measurement data
const bidirectionalData = await cstUtils.segmentation.getSegmentLargestBidirectional({
segmentationId: targetId,
segmentIndices: [targetIndex],
});
const activeViewportId = viewportGridService.getActiveViewportId();
// Process each bidirectional measurement
bidirectionalData.forEach(measurement => {
const { segmentIndex, majorAxis, minorAxis } = measurement;
// Create annotation
const annotation = cornerstoneTools.SegmentBidirectionalTool.hydrate(
activeViewportId,
[majorAxis, minorAxis],
{
segmentIndex,
segmentationId: targetId,
}
);
// Update segmentation stats
const updatedSegmentation = updateSegmentBidirectionalStats({
segmentationId: targetId,
segmentIndex: targetIndex,
bidirectionalData: measurement,
segmentationService,
annotation,
});
// Save changes if needed
if (updatedSegmentation) {
segmentationService.addOrUpdateSegmentation({
segmentationId: targetId,
segments: updatedSegmentation.segments,
});
}
});
},
interpolateLabelmap: () => {
const { segmentationId, segmentIndex } = _getActiveSegmentationInfo();
labelmapInterpolation.interpolate({
segmentationId,
segmentIndex,
});
},
/**
* Generates the selector props for the context menu, specific to
* the cornerstone viewport, and then runs the context menu.
@@ -373,6 +446,44 @@ function commandsModule({
utils.downloadCSVReport(measurementService.getMeasurements(measurementFilter));
},
downloadCSVSegmentationReport: ({ segmentationId }) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const cachedStats = segmentation.cachedStats;
const { representationData } = segmentation;
const { Labelmap } = representationData;
const { referencedImageIds } = Labelmap;
const firstImageId = referencedImageIds[0];
// find displaySet for firstImageId
const displaySet = displaySetService
.getActiveDisplaySets()
.find(ds => ds.imageIds?.some(i => i === firstImageId));
const {
SeriesNumber,
SeriesInstanceUID,
StudyInstanceUID,
SeriesDate,
SeriesTime,
SeriesDescription,
} = displaySet;
const additionalInfo = {
reference: {
SeriesNumber,
SeriesInstanceUID,
StudyInstanceUID,
SeriesDate,
SeriesTime,
SeriesDescription,
},
};
generateSegmentationCSVReport(segmentation, additionalInfo);
},
// Retrieve value commands
getActiveViewportEnabledElement: _getActiveViewportEnabledElement,
@@ -1094,7 +1205,12 @@ function commandsModule({
* @param props.segmentIndex - The index of the segment to activate
*/
setActiveSegmentAndCenterCommand: ({ segmentationId, segmentIndex }) => {
const { segmentationService } = servicesManager.services;
const { segmentationService, viewportGridService } = servicesManager.services;
// set both active segmentation and active segment
segmentationService.setActiveSegmentation(
viewportGridService.getActiveViewportId(),
segmentationId
);
segmentationService.setActiveSegment(segmentationId, segmentIndex);
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex);
},
@@ -1636,6 +1752,9 @@ function commandsModule({
},
undo: actions.undo,
redo: actions.redo,
interpolateLabelmap: actions.interpolateLabelmap,
runSegmentBidirectional: actions.runSegmentBidirectional,
downloadCSVSegmentationReport: actions.downloadCSVSegmentationReport,
};
return {
@@ -0,0 +1,127 @@
import React from 'react';
import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
Icons,
useSegmentationTableContext,
useSegmentationExpanded,
} from '@ohif/ui-next';
import { useTranslation } from 'react-i18next';
import { useSystem } from '@ohif/core/src';
/**
* Custom dropdown menu component for segmentation panel that uses context for data
*/
export const CustomDropdownMenuContent = () => {
const { commandsManager } = useSystem();
const { t } = useTranslation('SegmentationTable');
const {
onSegmentationAdd,
onSegmentationRemoveFromViewport,
onSegmentationEdit,
onSegmentationDelete,
exportOptions,
activeSegmentation,
activeSegmentationId,
} = useSegmentationTableContext('CustomDropdownMenu');
// Try to get segmentation data from expanded context first, fall back to table context
let segmentation;
let segmentationId;
let allowExport = false;
try {
// Try to get from expanded context
const context = useSegmentationExpanded();
segmentation = context.segmentation;
segmentationId = segmentation.segmentationId;
} catch (e) {
// If not in expanded context, fallback to active segmentation from table context
segmentation = activeSegmentation;
segmentationId = activeSegmentationId;
}
// Determine if export is allowed for this segmentation
if (exportOptions && segmentationId) {
const exportOption = exportOptions.find(opt => opt.segmentationId === segmentationId);
allowExport = exportOption?.isExportable || false;
}
if (!segmentation || !segmentationId) {
return null;
}
const actions = {
storeSegmentation: async segmentationId => {
commandsManager.run({
commandName: 'storeSegmentation',
commandOptions: { segmentationId },
context: 'CORNERSTONE',
});
},
onSegmentationDownloadRTSS: segmentationId => {
commandsManager.run('downloadRTSS', { segmentationId });
},
onSegmentationDownload: segmentationId => {
commandsManager.run('downloadSegmentation', { segmentationId });
},
downloadCSVSegmentationReport: segmentationId => {
commandsManager.run('downloadCSVSegmentationReport', { segmentationId });
},
};
return (
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => onSegmentationAdd(segmentationId)}>
<Icons.Add className="text-foreground" />
<span className="pl-2">{t('Create New Segmentation')}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>{t('Manage Current Segmentation')}</DropdownMenuLabel>
<DropdownMenuItem onClick={() => onSegmentationRemoveFromViewport(segmentationId)}>
<Icons.Series className="text-foreground" />
<span className="pl-2">{t('Remove from Viewport')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onSegmentationEdit(segmentationId)}>
<Icons.Rename className="text-foreground" />
<span className="pl-2">{t('Rename')}</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger className="pl-1">
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Export & Download')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={() => actions.downloadCSVSegmentationReport(segmentationId)}>
{t('Download CSV Report')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => actions.storeSegmentation(segmentationId)}
disabled={!allowExport}
>
{t('Export DICOM SEG')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => actions.onSegmentationDownload(segmentationId)}
disabled={!allowExport}
>
{t('Download DICOM SEG')}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onSegmentationDelete(segmentationId)}>
<Icons.Delete className="text-red-600" />
<span className="pl-2 text-red-600">{t('Delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
);
};
@@ -0,0 +1,138 @@
import * as cornerstoneTools from '@cornerstonejs/tools';
import React from 'react';
import { Separator, Button, Tooltip, TooltipTrigger, TooltipContent, Icons } from '@ohif/ui-next';
import { useTranslation } from 'react-i18next';
import { roundNumber } from '@ohif/core/src/utils';
import { useSystem } from '@ohif/core/src';
interface CustomSegmentStatisticsHeaderProps {
segmentationId: string;
segmentIndex: number;
}
/**
* Custom header component for segment statistics
*/
export const CustomSegmentStatisticsHeader = ({
segmentationId,
segmentIndex,
}: CustomSegmentStatisticsHeaderProps) => {
const { servicesManager, commandsManager } = useSystem();
const { segmentationService } = servicesManager.services;
const { t } = useTranslation('SegmentationTable');
const segmentation = segmentationService.getSegmentation(segmentationId);
const segment = segmentation.segments[segmentIndex];
const cachedStats = segment.cachedStats;
const namedStats = cachedStats.namedStats;
if (!namedStats) {
return null;
}
const bidirectional = namedStats.bidirectional;
if (!bidirectional) {
return (
<div className="-mt-2 space-y-2">
<div className="flex">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-primary flex items-center gap-2"
onClick={() => {
commandsManager.run('runSegmentBidirectional', {
segmentationId,
segmentIndex,
});
}}
>
<Icons.ToolBidirectionalSegment className="h-5 w-5" />
<span>{t('Compute Largest Bidirectional')}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{t('Add bidirectional measurement')}</TooltipContent>
</Tooltip>
</div>
<Separator className="bg-input" />
</div>
);
}
const { value, unit } = bidirectional;
const maxMajor = value.maxMajor;
const maxMinor = value.maxMinor;
const max = Math.max(maxMajor, maxMinor);
const min = Math.min(maxMajor, maxMinor);
const isVisible = cornerstoneTools.annotation.visibility.isAnnotationVisible(
bidirectional.annotationUID
);
return (
<div className="-mt-2 space-y-2">
<div className="flex items-center justify-between">
<div className="text-foreground">
<div>
L: {roundNumber(max)} {unit}
</div>
<div>
W: {roundNumber(min)} {unit}
</div>
</div>
<div className="flex gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className={`h-6 w-6 transition-opacity`}
onClick={e => {
e.stopPropagation();
cornerstoneTools.annotation.visibility.setAnnotationVisibility(
bidirectional.annotationUID,
!isVisible
);
segmentationService.addOrUpdateSegmentation({
segmentationId,
segmentIndex,
});
}}
>
{isVisible ? (
<Icons.Hide className="h-6 w-6" />
) : (
<Icons.Show className="h-6 w-6" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{t('Toggle visibility')}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => {
if (bidirectional.annotationUID) {
commandsManager.run('jumpToMeasurement', {
uid: bidirectional.annotationUID,
});
}
}}
>
<Icons.JumpToSlice />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{t('Jump to measurement')}</TooltipContent>
</Tooltip>
</div>
</div>
<Separator className="bg-input" />
</div>
);
};
@@ -1,77 +1,10 @@
import React from 'react';
import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
Icons,
} from '@ohif/ui-next';
import { CustomDropdownMenuContent } from './CustomDropdownMenuContent';
import { CustomSegmentStatisticsHeader } from './CustomSegmentStatisticsHeader';
export default function getSegmentationPanelCustomization({ commandsManager, servicesManager }) {
return {
'panelSegmentation.customDropdownMenuContent': ({
activeSegmentation,
onSegmentationAdd,
onSegmentationRemoveFromViewport,
onSegmentationEdit,
onSegmentationDelete,
allowExport,
storeSegmentation,
onSegmentationDownload,
onSegmentationDownloadRTSS,
t,
}) => (
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => onSegmentationAdd(activeSegmentation.segmentationId)}>
<Icons.Add className="text-foreground" />
<span className="pl-2">{t('Create New Segmentation')}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>{t('Manage Current Segmentation')}</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => onSegmentationRemoveFromViewport(activeSegmentation.segmentationId)}
>
<Icons.Series className="text-foreground" />
<span className="pl-2">{t('Remove from Viewport')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onSegmentationEdit(activeSegmentation.segmentationId)}>
<Icons.Rename className="text-foreground" />
<span className="pl-2">{t('Rename')}</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger
disabled={!allowExport}
className="pl-1"
>
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Export & Download')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem
onClick={() => storeSegmentation(activeSegmentation.segmentationId)}
>
{t('Export DICOM SEG')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onSegmentationDownload(activeSegmentation.segmentationId)}
>
{t('Download DICOM SEG')}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onSegmentationDelete(activeSegmentation.segmentationId)}>
<Icons.Delete className="text-red-600" />
<span className="pl-2 text-red-600">{t('Delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
),
'panelSegmentation.customDropdownMenuContent': CustomDropdownMenuContent,
'panelSegmentation.customSegmentStatisticsHeader': CustomSegmentStatisticsHeader,
'panelSegmentation.disableEditing': false,
'panelSegmentation.showAddSegment': true,
'panelSegmentation.onSegmentationAdd': () => {
@@ -81,15 +14,21 @@ export default function getSegmentationPanelCustomization({ commandsManager, ser
},
'panelSegmentation.tableMode': 'collapsed',
'panelSegmentation.readableText': {
lesionStats: 'Lesion Statistics',
minValue: 'Minimum Value',
maxValue: 'Maximum Value',
meanValue: 'Mean Value',
volume: 'Volume (ml)',
suvPeak: 'SUV Peak',
suvMax: 'Maximum SUV',
suvMaxIJK: 'SUV Max IJK',
lesionGlyoclysisStats: 'Lesion Glycolysis',
// the values will appear in this order
min: 'Min Value',
minLPS: 'Min Coord',
max: 'Max Value',
maxLPS: 'Max Coord',
mean: 'Mean Value',
stdDev: 'Standard Deviation',
count: 'Voxel Count',
median: 'Median',
skewness: 'Skewness',
kurtosis: 'Kurtosis',
peakValue: 'Peak Value',
peakLPS: 'Peak Coord',
volume: 'Volume',
lesionGlycolysis: 'Lesion Glycolysis',
},
};
}
@@ -190,16 +190,16 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
},
},
{
name: 'evaluate.mpr',
name: 'evaluate.displaySetIsReconstructable',
evaluate: ({ viewportId, disabledText = 'Selected viewport is not reconstructable' }) => {
const { protocol } = hangingProtocolService.getActiveProtocol();
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
if (!viewport) {
return;
}
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const areReconstructable = displaySets.every(displaySet => {
@@ -210,11 +210,8 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
return getDisabledState(disabledText);
}
const isMpr = protocol?.id === 'mpr';
return {
disabled: false,
className: utils.getToggledClassName(isMpr),
};
},
},
+68 -29
View File
@@ -39,6 +39,7 @@ import { useLutPresentationStore } from './stores/useLutPresentationStore';
import { usePositionPresentationStore } from './stores/usePositionPresentationStore';
import { useSegmentationPresentationStore } from './stores/useSegmentationPresentationStore';
import { imageRetrieveMetadataProvider } from '@cornerstonejs/core/utilities';
import { updateSegmentationStats } from './utils/updateSegmentationStats';
const { registerColormap } = csUtilities.colormap;
@@ -90,6 +91,7 @@ export default async function init({
cornerstoneViewportService,
hangingProtocolService,
viewportGridService,
segmentationService,
} = servicesManager.services;
window.services = servicesManager.services;
@@ -150,8 +152,6 @@ export default async function init({
);
});
// ... existing code ...
// add metadata providers
metaData.addProvider(
csUtilities.calibratedPixelSpacingMetadataProvider.get.bind(
@@ -177,6 +177,45 @@ export default async function init({
initCineService(servicesManager);
initStudyPrefetcherService(servicesManager);
segmentationService.subscribeDebounced(
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
async ({ segmentationId }) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const readableText = customizationService.getCustomization('panelSegmentation.readableText');
const updatedSegmentation = await updateSegmentationStats({
segmentation,
segmentationId,
readableText,
});
if (updatedSegmentation) {
segmentationService.addOrUpdateSegmentation({
segmentationId,
segments: updatedSegmentation.segments,
});
}
// Check for segments with bidirectional measurements and update them
if (segmentation) {
const segmentIndices = Object.keys(segmentation.segments)
.map(index => parseInt(index))
.filter(index => index > 0);
for (const segmentIndex of segmentIndices) {
const segment = segmentation.segments[segmentIndex];
if (segment?.cachedStats?.namedStats?.bidirectional) {
// Run the command to update the bidirectional measurement
commandsManager.runCommand('runSegmentBidirectional', {
segmentationId,
segmentIndex,
});
}
}
}
},
1000
);
// When a custom image load is performed, update the relevant viewports
hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
@@ -278,35 +317,35 @@ export default async function init({
function initializeWebWorkerProgressHandler(uiNotificationService) {
const activeToasts = new Map();
eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => {
const { progress, type, id } = detail;
// eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => {
// const { progress, type, id } = detail;
const cacheKey = `${type}-${id}`;
if (progress === 0 && !activeToasts.has(cacheKey)) {
const progressPromise = new Promise((resolve, reject) => {
activeToasts.set(cacheKey, { resolve, reject });
});
// const cacheKey = `${type}-${id}`;
// if (progress === 0 && !activeToasts.has(cacheKey)) {
// const progressPromise = new Promise((resolve, reject) => {
// activeToasts.set(cacheKey, { resolve, reject });
// });
uiNotificationService.show({
id: cacheKey,
title: `${type}`,
message: `${type}: ${progress}%`,
autoClose: false,
promise: progressPromise,
promiseMessages: {
loading: `Computing...`,
success: `Completed successfully`,
error: 'Web Worker failed',
},
});
} else {
if (progress === 100) {
const { resolve } = activeToasts.get(cacheKey);
resolve({ progress, type });
activeToasts.delete(cacheKey);
}
}
});
// uiNotificationService.show({
// id: cacheKey,
// title: `${type}`,
// message: `${type}: ${progress}%`,
// autoClose: false,
// promise: progressPromise,
// promiseMessages: {
// loading: `Computing...`,
// success: `Completed successfully`,
// error: 'Web Worker failed',
// },
// });
// } else {
// if (progress === 100) {
// const { resolve } = activeToasts.get(cacheKey);
// resolve({ progress, type });
// activeToasts.delete(cacheKey);
// }
// }
// });
}
/**
@@ -1,6 +1,7 @@
import {
PanTool,
WindowLevelTool,
SegmentBidirectionalTool,
StackScrollTool,
VolumeRotateTool,
ZoomTool,
@@ -52,8 +53,14 @@ export default function initCornerstoneTools(configuration = {}) {
addons: {
polySeg,
},
computeWorker: {
autoTerminateOnIdle: {
enabled: false,
},
},
});
addTool(PanTool);
addTool(SegmentBidirectionalTool);
addTool(WindowLevelTool);
addTool(StackScrollTool);
addTool(VolumeRotateTool);
@@ -142,6 +149,7 @@ const toolNames = {
OrientationMarker: OrientationMarkerTool.toolName,
WindowLevelRegion: WindowLevelRegionTool.toolName,
PlanarFreehandContourSegmentation: PlanarFreehandContourSegmentationTool.toolName,
SegmentBidirectional: SegmentBidirectionalTool.toolName,
};
export { toolNames };
@@ -34,6 +34,7 @@ const initMeasurementService = (
LivewireContour,
Probe,
UltrasoundDirectional,
SegmentBidirectional,
} = measurementServiceMappingsFactory(
measurementService,
displaySetService,
@@ -172,6 +173,14 @@ const initMeasurementService = (
UltrasoundDirectional.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'SegmentBidirectional',
SegmentBidirectional.matchingCriteria,
SegmentBidirectional.toAnnotation,
SegmentBidirectional.toMeasurement
);
return csTools3DVer1MeasurementSource;
};
@@ -15,217 +15,191 @@ export default function PanelSegmentation({
servicesManager,
});
const handlers = {
onSegmentationClick: (segmentationId: string) => {
commandsManager.run('setActiveSegmentation', { segmentationId });
},
onSegmentAdd: segmentationId => {
commandsManager.run('addSegment', { segmentationId });
},
onSegmentClick: (segmentationId, segmentIndex) => {
commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex });
},
onSegmentEdit: (segmentationId, segmentIndex) => {
commandsManager.run('editSegmentLabel', { segmentationId, segmentIndex });
},
onSegmentationEdit: segmentationId => {
commandsManager.run('editSegmentationLabel', { segmentationId });
},
onSegmentColorClick: (segmentationId, segmentIndex) => {
commandsManager.run('editSegmentColor', { segmentationId, segmentIndex });
},
onSegmentDelete: (segmentationId, segmentIndex) => {
commandsManager.run('deleteSegment', { segmentationId, segmentIndex });
},
onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => {
commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type });
},
onToggleSegmentLock: (segmentationId, segmentIndex) => {
commandsManager.run('toggleSegmentLock', { segmentationId, segmentIndex });
},
onToggleSegmentationRepresentationVisibility: (segmentationId, type) => {
commandsManager.run('toggleSegmentationVisibility', { segmentationId, type });
},
onSegmentationDownload: segmentationId => {
commandsManager.run('downloadSegmentation', { segmentationId });
},
storeSegmentation: async segmentationId => {
commandsManager.run({
commandName: 'storeSegmentation',
commandOptions: { segmentationId },
context: 'CORNERSTONE',
});
},
onSegmentationDownloadRTSS: segmentationId => {
commandsManager.run('downloadRTSS', { segmentationId });
},
setStyle: (segmentationId, type, key, value) => {
commandsManager.run('setSegmentationStyle', { segmentationId, type, key, value });
},
toggleRenderInactiveSegmentations: () => {
commandsManager.run('toggleRenderInactiveSegmentations');
},
onSegmentationRemoveFromViewport: segmentationId => {
commandsManager.run('removeSegmentationFromViewport', { segmentationId });
},
onSegmentationDelete: segmentationId => {
commandsManager.run('deleteSegmentation', { segmentationId });
},
setFillAlpha: ({ type }, value) => {
commandsManager.run('setFillAlpha', { type, value });
},
setOutlineWidth: ({ type }, value) => {
commandsManager.run('setOutlineWidth', { type, value });
},
setRenderFill: ({ type }, value) => {
commandsManager.run('setRenderFill', { type, value });
},
setRenderOutline: ({ type }, value) => {
commandsManager.run('setRenderOutline', { type, value });
},
setFillAlphaInactive: ({ type }, value) => {
commandsManager.run('setFillAlphaInactive', { type, value });
},
getRenderInactiveSegmentations: () => {
return commandsManager.run('getRenderInactiveSegmentations');
},
};
// Extract customization options
const segmentationTableMode = customizationService.getCustomization(
'panelSegmentation.tableMode'
);
// custom onSegmentationAdd if provided
) as unknown as string;
const onSegmentationAdd = customizationService.getCustomization(
'panelSegmentation.onSegmentationAdd'
);
const disableEditing = customizationService.getCustomization('panelSegmentation.disableEditing');
const showAddSegment = customizationService.getCustomization('panelSegmentation.showAddSegment');
const CustomDropdownMenuContent = customizationService.getCustomization(
'panelSegmentation.customDropdownMenuContent'
);
const CustomSegmentStatisticsHeader = customizationService.getCustomization(
'panelSegmentation.customSegmentStatisticsHeader'
);
// Create handlers object for all command runs
const handlers = {
onSegmentationClick: (segmentationId: string) => {
commandsManager.run('setActiveSegmentation', { segmentationId });
},
onSegmentAdd: segmentationId => {
commandsManager.run('addSegment', { segmentationId });
},
onSegmentClick: (segmentationId, segmentIndex) => {
commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex });
},
onSegmentEdit: (segmentationId, segmentIndex) => {
commandsManager.run('editSegmentLabel', { segmentationId, segmentIndex });
},
onSegmentationEdit: segmentationId => {
commandsManager.run('editSegmentationLabel', { segmentationId });
},
onSegmentColorClick: (segmentationId, segmentIndex) => {
commandsManager.run('editSegmentColor', { segmentationId, segmentIndex });
},
onSegmentDelete: (segmentationId, segmentIndex) => {
commandsManager.run('deleteSegment', { segmentationId, segmentIndex });
},
onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => {
commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type });
},
onToggleSegmentLock: (segmentationId, segmentIndex) => {
commandsManager.run('toggleSegmentLock', { segmentationId, segmentIndex });
},
onToggleSegmentationRepresentationVisibility: (segmentationId, type) => {
commandsManager.run('toggleSegmentationVisibility', { segmentationId, type });
},
onSegmentationDownload: segmentationId => {
commandsManager.run('downloadSegmentation', { segmentationId });
},
setStyle: (segmentationId, type, key, value) => {
commandsManager.run('setSegmentationStyle', { segmentationId, type, key, value });
},
toggleRenderInactiveSegmentations: () => {
commandsManager.run('toggleRenderInactiveSegmentations');
},
onSegmentationRemoveFromViewport: segmentationId => {
commandsManager.run('removeSegmentationFromViewport', { segmentationId });
},
onSegmentationDelete: segmentationId => {
commandsManager.run('deleteSegmentation', { segmentationId });
},
setFillAlpha: ({ type }, value) => {
commandsManager.run('setFillAlpha', { type, value });
},
setOutlineWidth: ({ type }, value) => {
commandsManager.run('setOutlineWidth', { type, value });
},
setRenderFill: ({ type }, value) => {
commandsManager.run('setRenderFill', { type, value });
},
setRenderOutline: ({ type }, value) => {
commandsManager.run('setRenderOutline', { type, value });
},
setFillAlphaInactive: ({ type }, value) => {
commandsManager.run('setFillAlphaInactive', { type, value });
},
getRenderInactiveSegmentations: () => {
return commandsManager.run('getRenderInactiveSegmentations');
},
};
// Generate export options
const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => {
const { representationData, segmentationId } = segmentation;
const { Labelmap } = representationData;
if (!Labelmap) {
return {
segmentationId,
isExportable: true,
};
return { segmentationId, isExportable: true };
}
const referencedImageIds = Labelmap.referencedImageIds;
const firstImageId = referencedImageIds[0];
const instance = metaData.get('instance', firstImageId);
if (!instance) {
return {
segmentationId,
isExportable: false,
};
return { segmentationId, isExportable: false };
}
const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID;
const SeriesInstanceUID = instance.SeriesInstanceUID;
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
const isExportable = displaySet?.isReconstructable;
return {
segmentationId,
isExportable,
isExportable: displaySet?.isReconstructable,
};
});
return (
<>
<SegmentationTable
disabled={disabled}
data={segmentationsWithRepresentations}
mode={segmentationTableMode}
title="Segmentations"
exportOptions={exportOptions}
disableEditing={disableEditing}
onSegmentationAdd={onSegmentationAdd}
onSegmentationClick={handlers.onSegmentationClick}
onSegmentationDelete={handlers.onSegmentationDelete}
showAddSegment={showAddSegment}
onSegmentAdd={handlers.onSegmentAdd}
onSegmentClick={handlers.onSegmentClick}
onSegmentEdit={handlers.onSegmentEdit}
onSegmentationEdit={handlers.onSegmentationEdit}
onSegmentColorClick={handlers.onSegmentColorClick}
onSegmentDelete={handlers.onSegmentDelete}
onToggleSegmentVisibility={handlers.onToggleSegmentVisibility}
onToggleSegmentLock={handlers.onToggleSegmentLock}
onToggleSegmentationRepresentationVisibility={
handlers.onToggleSegmentationRepresentationVisibility
}
onSegmentationDownload={handlers.onSegmentationDownload}
storeSegmentation={handlers.storeSegmentation}
onSegmentationDownloadRTSS={handlers.onSegmentationDownloadRTSS}
setStyle={handlers.setStyle}
toggleRenderInactiveSegmentations={handlers.toggleRenderInactiveSegmentations}
onSegmentationRemoveFromViewport={handlers.onSegmentationRemoveFromViewport}
setFillAlpha={handlers.setFillAlpha}
setOutlineWidth={handlers.setOutlineWidth}
setRenderFill={handlers.setRenderFill}
setRenderOutline={handlers.setRenderOutline}
setFillAlphaInactive={handlers.setFillAlphaInactive}
renderInactiveSegmentations={handlers.getRenderInactiveSegmentations()}
>
{children}
<SegmentationTable.Config />
<SegmentationTable.AddSegmentationRow />
// Common props for SegmentationTable
const tableProps = {
disabled,
data: segmentationsWithRepresentations,
mode: segmentationTableMode,
title: 'Segmentations',
exportOptions,
disableEditing,
onSegmentationAdd,
showAddSegment,
renderInactiveSegmentations: handlers.getRenderInactiveSegmentations(),
...handlers,
};
{segmentationTableMode === 'collapsed' ? (
<SegmentationTable.Collapsed>
<SegmentationTable.SelectorHeader>
const renderSegments = () => {
return (
<SegmentationTable.Segments>
<SegmentationTable.SegmentStatistics.Header>
<CustomSegmentStatisticsHeader />
</SegmentationTable.SegmentStatistics.Header>
<SegmentationTable.SegmentStatistics.Body />
</SegmentationTable.Segments>
);
};
// Render content based on mode
const renderModeContent = () => {
if (tableProps.mode === 'collapsed') {
return (
<SegmentationTable.Collapsed>
<SegmentationTable.Collapsed.Header>
<SegmentationTable.Collapsed.DropdownMenu>
<CustomDropdownMenuContent />
</SegmentationTable.SelectorHeader>
</SegmentationTable.Collapsed.DropdownMenu>
<SegmentationTable.Collapsed.Selector />
<SegmentationTable.Collapsed.Info />
</SegmentationTable.Collapsed.Header>
<SegmentationTable.Collapsed.Content>
<SegmentationTable.AddSegmentRow />
<SegmentationTable.Segments />
</SegmentationTable.Collapsed>
) : (
<SegmentationTable.Expanded>
<SegmentationTable.Header>
{renderSegments()}
</SegmentationTable.Collapsed.Content>
</SegmentationTable.Collapsed>
);
}
return (
<>
<SegmentationTable.Expanded>
<SegmentationTable.Expanded.Header>
<SegmentationTable.Expanded.DropdownMenu>
<CustomDropdownMenuContent />
</SegmentationTable.Header>
{/* <SegmentationTable.AddSegmentRow /> */}
<SegmentationTable.Segments />
</SegmentationTable.Expanded>
)}
</SegmentationTable>
</>
</SegmentationTable.Expanded.DropdownMenu>
<SegmentationTable.Expanded.Label />
<SegmentationTable.Expanded.Info />
</SegmentationTable.Expanded.Header>
<SegmentationTable.Expanded.Content>
<SegmentationTable.AddSegmentRow />
{renderSegments()}
</SegmentationTable.Expanded.Content>
</SegmentationTable.Expanded>
</>
);
};
return (
<SegmentationTable {...tableProps}>
{children}
<SegmentationTable.Config />
<SegmentationTable.AddSegmentationRow />
{renderModeContent()}
</SegmentationTable>
);
}
@@ -92,6 +92,7 @@ class SegmentationService extends PubSubService {
};
private _segmentationIdToColorLUTIndexMap: Map<string, number>;
private _segmentationGroupStatsMap: Map<string, any>;
readonly servicesManager: AppTypes.ServicesManager;
highlightIntervalId = null;
readonly EVENTS = EVENTS;
@@ -102,6 +103,8 @@ class SegmentationService extends PubSubService {
this._segmentationIdToColorLUTIndexMap = new Map();
this.servicesManager = servicesManager;
this._segmentationGroupStatsMap = new Map();
}
public onModeEnter(): void {
@@ -1058,6 +1061,33 @@ class SegmentationService extends PubSubService {
public getRenderInactiveSegmentations(viewportId: string): boolean {
return cstSegmentation.config.style.getRenderInactiveSegmentations(viewportId);
}
/**
* Sets statistics for a group of segmentations
* @param segmentationIds - Array of segmentation IDs that form the group
* @param stats - Statistics object containing metrics for the segmentation group
*
* @remarks
* This method stores statistical data for a group of related segmentations.
* The stats are stored using a composite key created from the sorted and joined
*/
public setSegmentationGroupStats(segmentationIds: string[], stats: any) {
const groupId = this.getGroupId(segmentationIds);
this._segmentationGroupStatsMap.set(groupId, stats);
}
/**
* Gets statistics for a group of segmentations
* @param segmentationIds - Array of segmentation IDs that form the group
* @returns The stored statistics object for the segmentation group if found, undefined otherwise
*/
public getSegmentationGroupStats(segmentationIds: string[]) {
const groupId = this.getGroupId(segmentationIds);
return this._segmentationGroupStatsMap.get(groupId);
}
private getGroupId(segmentationIds: string[]) {
return segmentationIds.sort().join(',');
}
/**
* Toggles the visibility of a segmentation in the state, and broadcasts the event.
@@ -0,0 +1,139 @@
export function generateSegmentationCSVReport(
segmentationData,
info: {
reference: {
SeriesNumber: string;
SeriesInstanceUID: string;
StudyInstanceUID: string;
SeriesDate: string;
SeriesTime: string;
SeriesDescription: string;
};
}
) {
// Initialize the rows for our CSV
const csvRows = [];
// Add segmentation-level information
csvRows.push(['Segmentation ID', segmentationData.segmentationId || '']);
csvRows.push(['Segmentation Label', segmentationData.label || '']);
csvRows.push([]);
const additionalInfo = info.reference;
// Add reference information
const referenceKeys = [
['Series Number', additionalInfo.SeriesNumber],
['Series Instance UID', additionalInfo.SeriesInstanceUID],
['Study Instance UID', additionalInfo.StudyInstanceUID],
['Series Date', additionalInfo.SeriesDate],
['Series Time', additionalInfo.SeriesTime],
['Series Description', additionalInfo.SeriesDescription],
];
referenceKeys.forEach(([key, value]) => {
if (value) {
csvRows.push([`reference ${key}`, value]);
}
});
// Add a blank row for separation
csvRows.push([]);
csvRows.push(['Segments Statistics']);
// Add segment information in columns
if (segmentationData.segments) {
// First row: Segment headers
const segmentHeaderRow = ['Label'];
for (const segmentId in segmentationData.segments) {
const segment = segmentationData.segments[segmentId];
segmentHeaderRow.push(`${segment.label || ''}`);
}
csvRows.push(segmentHeaderRow);
// Add segment properties
csvRows.push([
'Segment Index',
...Object.values(segmentationData.segments).map(s => s.segmentIndex || ''),
]);
csvRows.push([
'Locked',
...Object.values(segmentationData.segments).map(s => (s.locked ? 'Yes' : 'No')),
]);
csvRows.push([
'Active',
...Object.values(segmentationData.segments).map(s => (s.active ? 'Yes' : 'No')),
]);
// Add segment statistics
// First, collect all unique statistics across all segments
const allStats = new Set();
for (const segment of Object.values(segmentationData.segments)) {
if (segment.cachedStats && segment.cachedStats.namedStats) {
for (const statKey in segment.cachedStats.namedStats) {
const stat = segment.cachedStats.namedStats[statKey];
const statLabel = stat.label || stat.name;
const statUnit = stat.unit ? ` (${stat.unit})` : '';
allStats.add(`${statLabel}${statUnit}`);
}
}
}
// Then create a row for each statistic
for (const statName of allStats) {
const statRow = [statName];
for (const segment of Object.values(segmentationData.segments)) {
let statValue = '';
if (segment.cachedStats && segment.cachedStats.namedStats) {
for (const statKey in segment.cachedStats.namedStats) {
const stat = segment.cachedStats.namedStats[statKey];
const currentStatName = `${stat.label || stat.name}${stat.unit ? ` (${stat.unit})` : ''}`;
if (currentStatName === statName) {
statValue = stat.value !== undefined ? stat.value : '';
break;
}
}
}
statRow.push(statValue);
}
csvRows.push(statRow);
}
}
// Convert to CSV string
let csvString = '';
for (const row of csvRows) {
const formattedRow = row.map(cell => {
// Handle values that need to be quoted (contain commas, quotes, or newlines)
const cellValue = cell !== undefined && cell !== null ? cell.toString() : '';
if (cellValue.includes(',') || cellValue.includes('"') || cellValue.includes('\n')) {
// Escape quotes and wrap in quotes
return '"' + cellValue.replace(/"/g, '""') + '"';
}
return cellValue;
});
csvString += formattedRow.join(',') + '\n';
}
// Create a download link and trigger the download
const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute(
'download',
`${segmentationData.label || 'Segmentation'}_Report_${new Date().toISOString().split('T')[0]}.csv`
);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
@@ -0,0 +1,185 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getDisplayUnit } from './utils';
import { getIsLocked } from './utils/getIsLocked';
import { getIsVisible } from './utils/getIsVisible';
const SegmentBidirectional = {
toAnnotation: measurement => {},
toMeasurement: (
csToolsEventDetail,
displaySetService,
cornerstoneViewportService,
getValueTypeFromToolType,
customizationService
) => {
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
debugger;
const isLocked = getIsLocked(annotationUID);
const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.debug('SegmentBidirectional tool: Missing metadata or data');
return null;
}
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes(
referencedImageId,
displaySetService,
annotation
);
let displaySet;
if (SOPInstanceUID) {
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0];
}
const { points, textBox } = data.handles;
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
textBox,
isLocked,
isVisible,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
referencedImageId,
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
if (!targets.length) {
return [];
}
const annotations = [];
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes(
referencedImageId,
displaySetService,
annotation
);
const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0];
const { SeriesNumber } = displaySet;
const { length, width, unit } = targetStats;
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
unit,
length,
width,
});
});
return annotations;
}
function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) {
const columns = [];
const values = [];
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:SegmentBidirectional');
mappedAnnotations.forEach(annotation => {
const { length, width, unit } = annotation;
columns.push(`Length`, `Width`, 'Unit');
values.push(length, width, unit);
});
if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}
if (points) {
columns.push('points');
values.push(points.map(p => p.join(' ')).join(';'));
}
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations, displaySet) {
const displayText = {
primary: [],
secondary: [],
};
if (!mappedAnnotations || !mappedAnnotations.length) {
return displayText;
}
// Area is the same for all series
const { length, width, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
const roundedLength = utils.roundNumber(length, 2);
const roundedWidth = utils.roundNumber(width, 2);
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
displayText.primary.push(`L: ${roundedLength} ${getDisplayUnit(unit)}`);
displayText.primary.push(`W: ${roundedWidth} ${getDisplayUnit(unit)}`);
displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
return displayText;
}
export default SegmentBidirectional;
@@ -13,6 +13,7 @@ const supportedTools = [
'LivewireContour',
'UltrasoundDirectionalTool',
'SCOORD3DPoint',
'SegmentBidirectional',
];
export default supportedTools;
@@ -12,6 +12,7 @@ import SplineROI from './SplineROI';
import LivewireContour from './LivewireContour';
import Probe from './Probe';
import UltrasoundDirectional from './UltrasoundDirectional';
import SegmentBidirectional from './SegmentBidirectional';
const measurementServiceMappingsFactory = (
measurementService: MeasurementService,
@@ -48,6 +49,7 @@ const measurementServiceMappingsFactory = (
LivewireContour: POLYLINE,
Probe: POINT,
UltrasoundDirectional: POLYLINE,
SegmentBidirectional: BIDIRECTIONAL,
};
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
@@ -94,6 +96,27 @@ const measurementServiceMappingsFactory = (
},
],
},
SegmentBidirectional: {
toAnnotation: SegmentBidirectional.toAnnotation,
toMeasurement: csToolsAnnotation =>
SegmentBidirectional.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType,
customizationService
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
{
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
],
},
EllipticalROI: {
toAnnotation: EllipticalROI.toAnnotation,
toMeasurement: csToolsAnnotation =>
@@ -0,0 +1,185 @@
import * as cornerstoneTools from '@cornerstonejs/tools';
interface BidirectionalAxis {
length: number;
// Add other axis properties as needed
}
interface BidirectionalData {
majorAxis: BidirectionalAxis;
minorAxis: BidirectionalAxis;
}
/**
* Updates the statistics for a segmentation by calculating stats for each segment
* and storing them in the segment's cachedStats property
*
* @param segmentation - The segmentation object containing segments to update stats for
* @param segmentationId - The ID of the segmentation
* @returns The updated segmentation object with new stats, or null if no updates were made
*/
export async function updateSegmentationStats({
segmentation,
segmentationId,
readableText,
}: {
segmentation: any;
segmentationId: string;
readableText: any;
}): Promise<any | null> {
if (!segmentation) {
console.debug('No segmentation found for id:', segmentationId);
return null;
}
const segmentIndices = Object.keys(segmentation.segments)
.map(index => parseInt(index))
.filter(index => index > 0); // Filter out segment 0 which is typically background
if (segmentIndices.length === 0) {
console.debug('No segments found in segmentation:', segmentationId);
return null;
}
const stats = await cornerstoneTools.utilities.segmentation.getStatistics({
segmentationId,
segmentIndices,
mode: 'individual',
});
if (!stats) {
return null;
}
const updatedSegmentation = { ...segmentation };
let hasUpdates = false;
// Loop through each segment's stats
Object.entries(stats).forEach(([segmentIndex, segmentStats]) => {
const index = parseInt(segmentIndex);
if (!updatedSegmentation.segments[index].cachedStats) {
updatedSegmentation.segments[index].cachedStats = {};
hasUpdates = true;
}
// Get existing namedStats or initialize if not present
const namedStats = updatedSegmentation.segments[index].cachedStats.namedStats || {};
if (segmentStats.array) {
segmentStats.array.forEach(stat => {
// only gather stats that are in the readableText
if (!readableText[stat.name]) {
return;
}
if (stat && stat.name) {
namedStats[stat.name] = {
name: stat.name,
label: readableText[stat.name],
value: stat.value,
unit: stat.unit,
order: Object.keys(readableText).indexOf(stat.name),
};
}
});
if (readableText.volume) {
// Add volume if it exists but isn't in the array
if (segmentStats.volume && !namedStats.volume) {
namedStats.volume = {
name: 'volume',
label: 'Volume',
value: segmentStats.volume.value,
unit: segmentStats.volume.unit,
order: Object.keys(readableText).indexOf('volume'),
};
}
}
// Update the segment's cachedStats with namedStats
updatedSegmentation.segments[index].cachedStats.namedStats = namedStats;
hasUpdates = true;
}
});
return hasUpdates ? updatedSegmentation : null;
}
/**
* Updates a segment's statistics with bidirectional measurement data
*
* @param segmentationId - The ID of the segmentation
* @param segmentIndex - The index of the segment to update
* @param bidirectionalData - The bidirectional measurement data to add
* @param segmentationService - The segmentation service to use for updating the segment
* @returns Whether the update was successful
*/
export function updateSegmentBidirectionalStats({
segmentationId,
segmentIndex,
bidirectionalData,
segmentationService,
annotation,
}: {
segmentationId: string;
segmentIndex: number;
bidirectionalData: BidirectionalData;
segmentationService: AppTypes.SegmentationService;
annotation: any;
}) {
if (!segmentationId || segmentIndex === undefined || !bidirectionalData) {
console.debug('Missing required data for bidirectional stats update');
return null;
}
const segmentation = segmentationService.getSegmentation(segmentationId);
if (!segmentation || !segmentation.segments[segmentIndex]) {
console.debug('Segment not found:', segmentIndex, 'in segmentation:', segmentationId);
return null;
}
const updatedSegmentation = { ...segmentation };
const segment = updatedSegmentation.segments[segmentIndex];
if (!segment.cachedStats) {
segment.cachedStats = { namedStats: {} };
}
if (!segment.cachedStats.namedStats) {
segment.cachedStats.namedStats = {};
}
const { majorAxis, minorAxis, maxMajor, maxMinor } = bidirectionalData;
if (!majorAxis || !minorAxis) {
console.debug('Missing major or minor axis data');
return null;
}
let hasUpdates = false;
const namedStats = segment.cachedStats.namedStats;
// Only calculate and update if we have valid measurements
if (maxMajor > 0 && maxMinor > 0) {
namedStats.bidirectional = {
name: 'bidirectional',
label: 'Bidirectional',
annotationUID: annotation.annotationUID,
value: {
maxMajor,
maxMinor,
majorAxis,
minorAxis,
},
unit: 'mm',
};
hasUpdates = true;
}
if (hasUpdates) {
return updatedSegmentation;
}
return null;
}