fix(seg): Enhance segmentation tools and UI, refactor code, and update dependencies (#4915)

This commit is contained in:
Alireza authored and GitHub committed 2025-04-02 18:10:47 -04:00
1 parent 1dc00d3030
commit 8432d5f633
37 files changed
+1063 -1122

No files matched your search

+7 -7
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.8.2",
"@cornerstonejs/dicom-image-loader": "^3.8.4",
"@ohif/core": "3.10.0-beta.144",
"@ohif/ui": "3.10.0-beta.144",
"dcmjs": "*",
@@ -51,12 +51,12 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.8.2",
"@cornerstonejs/ai": "^3.8.2",
"@cornerstonejs/core": "^3.8.2",
"@cornerstonejs/labelmap-interpolation": "^3.8.2",
"@cornerstonejs/polymorphic-segmentation": "^3.8.2",
"@cornerstonejs/tools": "^3.8.2",
"@cornerstonejs/adapters": "^3.8.4",
"@cornerstonejs/ai": "^3.8.4",
"@cornerstonejs/core": "^3.8.4",
"@cornerstonejs/labelmap-interpolation": "^3.8.4",
"@cornerstonejs/polymorphic-segmentation": "^3.8.4",
"@cornerstonejs/tools": "^3.8.4",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",
+83 -1
View File
@@ -10,7 +10,6 @@ import {
ToolGroupManager,
Enums,
utilities as cstUtils,
ReferenceLinesTool,
annotation,
Types as ToolTypes,
} from '@cornerstonejs/tools';
@@ -43,6 +42,8 @@ const toggleSyncFunctions = {
voi: toggleVOISliceSync,
};
const { segmentation: segmentationUtils } = cstUtils;
const getLabelmapTools = ({ toolGroupService }) => {
const labelmapTools = [];
const toolGroupIds = toolGroupService.getToolGroupIds();
@@ -155,6 +156,8 @@ function commandsModule({
}
);
measurement.annotationUID = annotation.annotationUID;
// Update segmentation stats
const updatedSegmentation = updateSegmentBidirectionalStats({
segmentationId: targetId,
@@ -172,6 +175,14 @@ function commandsModule({
});
}
});
// get the active segmentIndex bidirectional annotation and jump to it
const activeBidirectional = bidirectionalData.find(
measurement => measurement.segmentIndex === targetIndex
);
commandsManager.run('jumpToMeasurement', {
uid: activeBidirectional.annotationUID,
});
},
interpolateLabelmap: () => {
const { segmentationId, segmentIndex } = _getActiveSegmentationInfo();
@@ -1630,6 +1641,72 @@ function commandsModule({
segmentAI.initViewport(viewport);
}
},
setBrushSize: ({ value, toolNames }) => {
const brushSize = Number(value);
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
if (toolNames?.length === 0) {
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize);
} else {
toolNames?.forEach(toolName => {
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
});
}
});
},
setThresholdRange: ({
value,
toolNames = [
'ThresholdCircularBrush',
'ThresholdSphereBrush',
'ThresholdCircularBrushDynamic',
'ThresholdSphereBrushDynamic',
],
}) => {
const toolGroupIds = toolGroupService.getToolGroupIds();
if (!toolGroupIds?.length) {
return;
}
for (const toolGroupId of toolGroupIds) {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
toolNames?.forEach(toolName => {
toolGroup.setToolConfiguration(toolName, {
threshold: {
range: value,
},
});
});
}
},
increaseBrushSize: () => {
const toolGroupIds = toolGroupService.getToolGroupIds();
if (!toolGroupIds?.length) {
return;
}
for (const toolGroupId of toolGroupIds) {
const brushSize = segmentationUtils.getBrushSizeForToolGroup(toolGroupId);
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize + 3);
}
},
decreaseBrushSize: () => {
const toolGroupIds = toolGroupService.getToolGroupIds();
if (!toolGroupIds?.length) {
return;
}
for (const toolGroupId of toolGroupIds) {
const brushSize = segmentationUtils.getBrushSizeForToolGroup(toolGroupId);
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize - 3);
}
},
addNewSegment: () => {
const { segmentationService } = servicesManager.services;
const { activeViewportId } = viewportGridService.getState();
const activeSegmentation = segmentationService.getActiveSegmentation(activeViewportId);
segmentationService.addSegment(activeSegmentation.segmentationId);
},
};
const definitions = {
@@ -1903,6 +1980,11 @@ function commandsModule({
toggleLabelmapAssist: actions.toggleLabelmapAssist,
interpolateScrollForMarkerLabelmap: actions.interpolateScrollForMarkerLabelmap,
clearMarkersForMarkerLabelmap: actions.clearMarkersForMarkerLabelmap,
setBrushSize: actions.setBrushSize,
setThresholdRange: actions.setThresholdRange,
increaseBrushSize: actions.increaseBrushSize,
decreaseBrushSize: actions.decreaseBrushSize,
addNewSegment: actions.addNewSegment,
};
return {
@@ -95,24 +95,53 @@ export const CustomDropdownMenuContent = () => {
<DropdownMenuSub>
<DropdownMenuSubTrigger className="pl-1">
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Export & Download')}</span>
<span className="pl-2">{t('Download & Export')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={() => actions.downloadCSVSegmentationReport(segmentationId)}>
{t('Download CSV Report')}
<DropdownMenuLabel className="flex items-center pl-0">
<Icons.Download className="h-5 w-5" />
<span className="pl-1">{t('Download')}</span>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.downloadCSVSegmentationReport(segmentationId);
}}
>
{t('CSV Report')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => actions.storeSegmentation(segmentationId)}
onClick={e => {
e.preventDefault();
actions.onSegmentationDownload(segmentationId);
}}
disabled={!allowExport}
>
{t('Export DICOM SEG')}
{t('DICOM SEG')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => actions.onSegmentationDownload(segmentationId)}
onClick={e => {
e.preventDefault();
actions.onSegmentationDownloadRTSS(segmentationId);
}}
disabled={!allowExport}
>
{t('Download DICOM SEG')}
{t('DICOM RTSS')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel className="flex items-center pl-0">
<Icons.Export className="h-5 w-5" />
<span className="pl-1 pt-1">{t('Export')}</span>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.storeSegmentation(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM SEG')}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
+13 -38
View File
@@ -39,7 +39,10 @@ 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';
import {
setupSegmentationDataModifiedHandler,
setupSegmentationModifiedHandler,
} from './utils/segmentationHandlers';
const { registerColormap } = csUtilities.colormap;
@@ -189,44 +192,16 @@ export default async function init({
});
});
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,
});
// Setup segmentation event handlers
setupSegmentationDataModifiedHandler({
segmentationService,
customizationService,
commandsManager,
});
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
);
setupSegmentationModifiedHandler({
segmentationService,
});
// When a custom image load is performed, update the relevant viewports
hangingProtocolService.subscribe(
@@ -602,10 +602,16 @@ class SegmentationService extends PubSubService {
const segments: { [segmentIndex: string]: cstTypes.Segment } = {};
let segmentsCachedStats = {};
// Create colorLUT array for RT structures
const colorLUT = [[0, 0, 0, 0]]; // First entry is transparent for index 0
// Process each segment similarly to the SEG function
for (const rtStructData of allRTStructData) {
const { data, id, color, segmentIndex, geometryId } = rtStructData;
// Add the color to the colorLUT array
colorLUT.push(color);
try {
const geometry = await geometryLoader.createAndCacheGeometry(geometryId, {
geometryData: {
@@ -647,6 +653,11 @@ class SegmentationService extends PubSubService {
}
}
// Create and register the colorLUT
const colorLUTIndex = getNextColorLUTIndex();
addColorLUT(colorLUT, colorLUTIndex);
this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex);
// Assign processed segments to segmentation config
segmentation.config.segments = segments;
@@ -53,7 +53,6 @@ const segmentationRepresentationModifiedCallback = async (
const targetFrameOfReferenceUID = viewport.getFrameOfReferenceUID();
if (!targetFrameOfReferenceUID) {
console.debug('No frame of reference UID found for the target viewport');
return;
}
@@ -846,13 +846,17 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
if (addOverlayFn) {
addOverlayFn();
}
viewport.render();
volumesProperties.forEach(({ properties, volumeId }) => {
viewport.setProperties(properties, volumeId);
setTimeout(() => {
// seems like a hack but we need the actor to be ready first before
// we set the properties
viewport.setProperties(properties, volumeId);
viewport.render();
}, 0);
});
viewport.render();
this.setPresentations(viewport.id, presentations, viewportInfo);
if (!presentations.positionPresentation) {
@@ -865,8 +869,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
}
viewport.render();
this._broadcastEvent(this.EVENTS.VIEWPORT_VOLUMES_CHANGED, {
viewportInfo,
});
@@ -2,7 +2,6 @@ const colormaps = [
{
ColorSpace: 'RGB',
Name: 'Grayscale',
name: 'Grayscale',
NanColor: [1, 0, 0],
RGBPoints: [0, 0, 0, 0, 1, 1, 1, 1],
description: 'Grayscale',
@@ -10,7 +9,6 @@ const colormaps = [
{
ColorSpace: 'RGB',
Name: 'X Ray',
name: 'X Ray',
NanColor: [1, 0, 0],
RGBPoints: [0, 1, 1, 1, 1, 0, 0, 0],
description: 'X Ray',
@@ -18,7 +16,6 @@ const colormaps = [
{
ColorSpace: 'RGB',
Name: 'hsv',
name: 'hsv',
RGBPoints: [
-1, 1, 0, 0, -0.666666, 1, 0, 1, -0.333333, 0, 0, 1, 0, 0, 1, 1, 0.33333, 0, 1, 0, 0.66666, 1,
1, 0, 1, 1, 0, 0,
@@ -28,7 +25,6 @@ const colormaps = [
{
ColorSpace: 'RGB',
Name: 'hot_iron',
name: 'hot_iron',
RGBPoints: [
0.0, 0.0039215686, 0.0039215686, 0.0156862745, 0.00392156862745098, 0.0039215686,
0.0039215686, 0.0156862745, 0.00784313725490196, 0.0039215686, 0.0039215686, 0.031372549,
@@ -224,7 +220,6 @@ const colormaps = [
{
ColorSpace: 'RGB',
Name: 'red_hot',
name: 'red_hot',
RGBPoints: [
0.0, 0.0, 0.0, 0.0, 0.00392156862745098, 0.0, 0.0, 0.0, 0.00784313725490196, 0.0, 0.0, 0.0,
0.011764705882352941, 0.0, 0.0, 0.0, 0.01568627450980392, 0.0039215686, 0.0039215686,
@@ -359,7 +354,6 @@ const colormaps = [
{
ColorSpace: 'RGB',
Name: 's_pet',
name: 's_pet',
RGBPoints: [
0.0, 0.0156862745, 0.0039215686, 0.0156862745, 0.00392156862745098, 0.0156862745,
0.0039215686, 0.0156862745, 0.00784313725490196, 0.0274509804, 0.0039215686, 0.031372549,
@@ -553,7 +547,6 @@ const colormaps = [
{
ColorSpace: 'RGB',
Name: 'perfusion',
name: 'perfusion',
RGBPoints: [
0.0, 0.0, 0.0, 0.0, 0.00392156862745098, 0.0078431373, 0.0235294118, 0.0235294118,
0.00784313725490196, 0.0078431373, 0.031372549, 0.0470588235, 0.011764705882352941,
@@ -733,7 +726,6 @@ const colormaps = [
{
ColorSpace: 'RGB',
Name: 'rainbow_2',
name: 'rainbow_2',
RGBPoints: [
0.0, 0.0, 0.0, 0.0, 0.00392156862745098, 0.0156862745, 0.0, 0.0117647059, 0.00784313725490196,
0.0352941176, 0.0, 0.0274509804, 0.011764705882352941, 0.0509803922, 0.0, 0.0392156863,
@@ -872,7 +864,6 @@ const colormaps = [
{
ColorSpace: 'RGB',
Name: 'suv',
name: 'suv',
RGBPoints: [
0.0, 1.0, 1.0, 1.0, 0.00392156862745098, 1.0, 1.0, 1.0, 0.00784313725490196, 1.0, 1.0, 1.0,
0.011764705882352941, 1.0, 1.0, 1.0, 0.01568627450980392, 1.0, 1.0, 1.0, 0.0196078431372549,
@@ -1019,7 +1010,6 @@ const colormaps = [
{
ColorSpace: 'RGB',
Name: 'ge_256',
name: 'ge_256',
RGBPoints: [
0.0, 0.0039215686, 0.0078431373, 0.0078431373, 0.00392156862745098, 0.0039215686,
0.0078431373, 0.0078431373, 0.00784313725490196, 0.0039215686, 0.0078431373, 0.0117647059,
@@ -1211,7 +1201,6 @@ const colormaps = [
{
ColorSpace: 'RGB',
Name: 'ge',
name: 'ge',
RGBPoints: [
0.0, 0.0078431373, 0.0078431373, 0.0078431373, 0.00392156862745098, 0.0078431373,
0.0078431373, 0.0078431373, 0.00784313725490196, 0.0078431373, 0.0078431373, 0.0078431373,
@@ -1404,7 +1393,6 @@ const colormaps = [
{
ColorSpace: 'RGB',
Name: 'siemens',
name: 'siemens',
RGBPoints: [
0.0, 0.0078431373, 0.0039215686, 0.1254901961, 0.00392156862745098, 0.0078431373,
0.0039215686, 0.1254901961, 0.00784313725490196, 0.0078431373, 0.0039215686, 0.1882352941,
@@ -1,9 +1,15 @@
import { handleSegmentChange } from './segmentUtils';
import { isReferenceViewable } from './isReferenceViewable';
import {
setupSegmentationDataModifiedHandler,
setupSegmentationModifiedHandler,
} from './segmentationHandlers';
const utils = {
handleSegmentChange,
isReferenceViewable,
setupSegmentationDataModifiedHandler,
setupSegmentationModifiedHandler,
};
export default utils;
@@ -84,7 +84,7 @@ const Bidirectional = {
function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId, referencedSeriesInstanceUID } = metadata;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
if (!targets.length) {
@@ -93,6 +93,7 @@ function getMappedAnnotations(annotation, displaySetService) {
}
const annotations = [];
const addedModalities = new Set();
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
@@ -108,6 +109,12 @@ function getMappedAnnotations(annotation, displaySetService) {
const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit, perimeter, radiusUnit } =
targetStats;
if (Modality && addedModalities.has(Modality)) {
return;
}
addedModalities.add(Modality);
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
@@ -93,6 +93,8 @@ function getMappedAnnotations(annotation, displaySetService) {
}
const annotations = [];
const addedModalities = new Set();
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
@@ -107,6 +109,16 @@ function getMappedAnnotations(annotation, displaySetService) {
const { SeriesNumber } = displaySet;
const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit } = targetStats;
// Skip if we've already added this modality
if (Modality && addedModalities.has(Modality)) {
return;
}
// Add modality to the set if it exists
if (Modality) {
addedModalities.add(Modality);
}
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
@@ -92,6 +92,8 @@ function getMappedAnnotations(annotation, displaySetService) {
}
const annotations = [];
const addedModalities = new Set();
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
@@ -106,6 +108,16 @@ function getMappedAnnotations(annotation, displaySetService) {
const { SeriesNumber } = displaySet;
const { mean, stdDev, max, area, Modality, modalityUnit, areaUnit } = targetStats;
// Skip if we've already added this modality
if (Modality && addedModalities.has(Modality)) {
return;
}
// Add modality to the set if it exists
if (Modality) {
addedModalities.add(Modality);
}
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
@@ -0,0 +1,93 @@
import * as cornerstoneTools from '@cornerstonejs/tools';
import { updateSegmentationStats } from './updateSegmentationStats';
/**
* Sets up the handler for segmentation data modification events
*/
export function setupSegmentationDataModifiedHandler({
segmentationService,
customizationService,
commandsManager,
}) {
segmentationService.subscribeDebounced(
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
async ({ segmentationId }) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const readableText = customizationService.getCustomization('panelSegmentation.readableText');
// Check for segments with bidirectional measurements and update them
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,
});
}
}
const updatedSegmentation = await updateSegmentationStats({
segmentation,
segmentationId,
readableText,
});
if (updatedSegmentation) {
segmentationService.addOrUpdateSegmentation({
segmentationId,
segments: updatedSegmentation.segments,
});
}
},
1000
);
}
/**
* Sets up the handler for segmentation modification events
*/
export function setupSegmentationModifiedHandler({ segmentationService }) {
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
async ({ segmentationId }) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const annotationState = cornerstoneTools.annotation.state.getAllAnnotations();
const bidirectionalAnnotations = annotationState.filter(
annotation =>
annotation.metadata.toolName === cornerstoneTools.SegmentBidirectionalTool.toolName
);
let toRemoveUIDs = [];
if (!segmentation) {
toRemoveUIDs = bidirectionalAnnotations.map(
annotation => annotation.metadata.segmentationId === segmentationId
);
return;
} else {
const segmentIndices = Object.keys(segmentation.segments)
.map(index => parseInt(index))
.filter(index => index > 0);
// check if there is a bidirectional data that exists but the segment
// does not exists anymore we need to remove the bidirectional data
const bidirectionalAnnotationsToRemove = bidirectionalAnnotations.filter(
annotation =>
annotation.metadata.segmentationId === segmentationId &&
!segmentIndices.includes(annotation.metadata.segmentIndex)
);
toRemoveUIDs = bidirectionalAnnotationsToRemove.map(annotation => annotation.annotationUID);
}
toRemoveUIDs.forEach(uid => {
cornerstoneTools.annotation.state.removeAnnotation(uid);
});
}
);
}