feat(segmentation): segment statistics, labelmap interpolation and segment bidirectional (#4865)
This commit is contained in:
1 parent
694f3e9611
commit
ec79b5523e
73 files changed
+3603
-1563
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
+1
@@ -13,6 +13,7 @@ const supportedTools = [
|
||||
'LivewireContour',
|
||||
'UltrasoundDirectionalTool',
|
||||
'SCOORD3DPoint',
|
||||
'SegmentBidirectional',
|
||||
];
|
||||
|
||||
export default supportedTools;
|
||||
+23
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user