From fe408d8b6c7bfad822944f7108b07adefd3ac66c Mon Sep 17 00:00:00 2001 From: TFRadicalImaging <115191433+TFRadicalImaging@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:40:29 -0300 Subject: [PATCH 01/16] fix(seg): save the segment of an overlap labelmap layer under its own segment number A segment in a private overlap layer is stored under label value 1. The SEG export read the raw voxel values as segment numbers, so the segment was written as a second copy of segment 1. Map each voxel value through the layer's labelToSegmentIndex before the labelmap goes to the SEG adapter. --- .../src/commandsModule.ts | 48 ++++++++++- tests/LabelMapSegOverlapLayerSave.spec.ts | 85 +++++++++++++++++++ 2 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 tests/LabelMapSegOverlapLayerSave.spec.ts diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index a4cf5ad6444..454e98a5b7e 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -110,12 +110,40 @@ const commandsModule = ({ const labelmapData = segmentation.representationData.Labelmap; + // A labelmap layer stores each segment under a label value, and + // `labelToSegmentIndex` maps that value to the segment index. A private + // overlap layer stores its segment as label value 1, while the SEG adapter + // reads every voxel value as a segment number. Map the values to segment + // indices; a value without an entry keeps its value. The cached labelmap + // image is not changed: the data is copied only when the map changes a value. + const toSegmentIndexPixels = (pixelData, labelToSegmentIndex?: Record) => { + const remaps = Object.entries(labelToSegmentIndex ?? {}).some( + ([labelValue, segmentIndex]) => Number(labelValue) !== segmentIndex + ); + if (!remaps) { + return pixelData; + } + + const segmentIndexPixels = new pixelData.constructor(pixelData.length); + for (let i = 0; i < pixelData.length; i++) { + const labelValue = pixelData[i]; + segmentIndexPixels[i] = + labelValue === 0 ? 0 : (labelToSegmentIndex[labelValue] ?? labelValue); + } + return segmentIndexPixels; + }; + // Build a labelmap3D (one labelmaps2D entry per source slice) from a list of // derived labelmap image ids. When `referencedImageIds` is supplied (the // multi-layer/overlap path) each frame is indexed by its source slice so the // layers align to the same frames; otherwise frames are sequential (the legacy // single-layer behavior, kept byte-identical). - const buildLabelmap3D = (segImageIds: string[], metadata, referencedImageIds?: string[]) => { + const buildLabelmap3D = ( + segImageIds: string[], + metadata, + referencedImageIds?: string[], + labelToSegmentIndex?: Record + ) => { const segImages = segImageIds.map(imageId => cache.getImage(imageId)); const labelmaps2D = []; @@ -129,7 +157,7 @@ const commandsModule = ({ for (const segImage of segImages) { const segmentsOnLabelmap = new Set(); - const pixelData = segImage.getPixelData(); + const pixelData = toSegmentIndexPixels(segImage.getPixelData(), labelToSegmentIndex); const { rows, columns } = segImage; // Use a single pass through the pixel data @@ -240,7 +268,12 @@ const commandsModule = ({ layers[0].referencedImageIds ?? labelmapData.referencedImageIds ?? []; referencedImages = referencedImageIds.map(resolveReferencedImage); labelmaps3D = layers.map(layer => - buildLabelmap3D(layer.imageIds ?? [], metadata, referencedImageIds) + buildLabelmap3D( + layer.imageIds ?? [], + metadata, + referencedImageIds, + layer.labelToSegmentIndex + ) ); } else { const { imageIds } = labelmapData; @@ -248,7 +281,14 @@ const commandsModule = ({ referencedImages = segImages.map((image, sliceIndex) => resolveReferencedImage(image.referencedImageId, sliceIndex) ); - labelmaps3D = buildLabelmap3D(imageIds, metadata); + // The only layer can be a private overlap layer, for example after the + // user deletes the segments of the original layer. + labelmaps3D = buildLabelmap3D( + imageIds, + metadata, + undefined, + layers?.[0]?.labelToSegmentIndex + ); } // `...generateOptions` has the last word on the predecessor: the store diff --git a/tests/LabelMapSegOverlapLayerSave.spec.ts b/tests/LabelMapSegOverlapLayerSave.spec.ts new file mode 100644 index 00000000000..aa5beb51430 --- /dev/null +++ b/tests/LabelMapSegOverlapLayerSave.spec.ts @@ -0,0 +1,85 @@ +import fs from 'node:fs'; +import dcmjs from 'dcmjs'; + +import { expect, test, visitStudy } from './utils'; + +/** + * Painting a segment across a locked segment puts the painted segment in a + * separate labelmap layer, where it is stored under another label value. The + * DICOM SEG save must still write each segment under its own segment number, + * label and color. + */ +test.beforeEach(async ({ page }) => { + const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458'; + const mode = 'segmentation'; + await visitStudy(page, studyInstanceUID, mode, 2000); +}); + +test('should save each segment of an overlap layer under its own segment number', async ({ + page, + rightPanelPageObject, + viewportPageObject, +}) => { + const labelMapPanel = rightPanelPageObject.labelMapSegmentationPanel; + await labelMapPanel.select(); + await labelMapPanel.addSegmentationButton.click(); + await expect(labelMapPanel.panel.rows).toHaveCount(1); + + const viewport = await viewportPageObject.getById('default'); + await labelMapPanel.tools.brush.click(); + await labelMapPanel.tools.brush.setRadius(10); + + // Segment 1: a horizontal stroke. + await viewport.normalizedDragAt({ start: { x: 0.3, y: 0.5 }, end: { x: 0.7, y: 0.5 } }); + + // Segment 2: a vertical stroke across segment 1, which is locked. + await labelMapPanel.addSegmentButton.click(); + await expect(labelMapPanel.panel.rows).toHaveCount(2); + await labelMapPanel.panel.nthSegment(0).actions.toggleLock(); + await viewport.normalizedDragAt({ start: { x: 0.5, y: 0.3 }, end: { x: 0.5, y: 0.7 } }); + + // The save needs every source image of the series in the cache. + await expect + .poll( + () => + page.evaluate(() => { + const { cache, getEnabledElements } = (window as any).cornerstone; + const imageIds = getEnabledElements()[0].viewport.getImageIds(); + return imageIds.every(imageId => cache.getImage(imageId)); + }), + { timeout: 60_000 } + ) + .toBe(true); + + await labelMapPanel.panel.moreMenu.click(); + await page.getByRole('menuitem', { name: 'Save' }).click(); + await page.getByRole('menuitem', { name: 'DICOM SEG' }).click(); + + const downloadPromise = page.waitForEvent('download'); + await page.getByTestId('report-download-button').click(); + const downloadPath = await (await downloadPromise).path(); + if (!downloadPath) { + throw new Error('The save produced no downloaded file'); + } + + const bytes = fs.readFileSync(downloadPath); + const arrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); + const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( + dcmjs.data.DicomMessage.readFile(arrayBuffer).dict + ); + + const asArray = value => (Array.isArray(value) ? value : [value]); + const segments = asArray(dataset.SegmentSequence); + expect(segments.map(segment => segment.SegmentLabel)).toEqual(['Segment 1', 'Segment 2']); + expect(segments[0].RecommendedDisplayCIELabValue).not.toEqual( + segments[1].RecommendedDisplayCIELabValue + ); + + const framesPerSegment = {}; + for (const frame of asArray(dataset.PerFrameFunctionalGroupsSequence)) { + const segmentNumber = asArray(frame.SegmentIdentificationSequence)[0].ReferencedSegmentNumber; + framesPerSegment[segmentNumber] = (framesPerSegment[segmentNumber] ?? 0) + 1; + } + expect(framesPerSegment[1]).toBeGreaterThan(0); + expect(framesPerSegment[2]).toBeGreaterThan(0); +}); From 7696586fdde8b60c586a66735babc394f3cdffb5 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 14 Oct 2026 12:40:26 -0400 Subject: [PATCH 02/16] fix(seg): map overlap layer labels from segment bindings without full copies Build the label remap of each layer once from segmentBindings, copy a slice only when a value changes, widen to Uint16Array for segment indices above 255, and drop voxels whose label is bound to no segment. The single-layer path reads the pixels of the same layer as its remap. The e2e test now checks that an overlap layer exists before the save. Co-Authored-By: Claude Opus 5.5 --- .../src/commandsModule.ts | 65 +++++------ .../src/utils/labelmapLayerSegmentIndices.ts | 103 ++++++++++++++++++ tests/LabelMapSegOverlapLayerSave.spec.ts | 7 ++ 3 files changed, 135 insertions(+), 40 deletions(-) create mode 100644 extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 454e98a5b7e..fa1d03b843a 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -11,6 +11,11 @@ import { LABELMAP_SEG_SOP_CLASS_UID, BITMAP_SEG_SOP_CLASS_UID, } from './utils/segmentationConfig'; +import { + getLabelRemaps, + toSegmentIndexPixels, + type LabelRemap, +} from './utils/labelmapLayerSegmentIndices'; const getTargetViewport = ({ viewportId, viewportGridService }) => { const { viewports, activeViewportId } = viewportGridService.getState(); @@ -110,28 +115,8 @@ const commandsModule = ({ const labelmapData = segmentation.representationData.Labelmap; - // A labelmap layer stores each segment under a label value, and - // `labelToSegmentIndex` maps that value to the segment index. A private - // overlap layer stores its segment as label value 1, while the SEG adapter - // reads every voxel value as a segment number. Map the values to segment - // indices; a value without an entry keeps its value. The cached labelmap - // image is not changed: the data is copied only when the map changes a value. - const toSegmentIndexPixels = (pixelData, labelToSegmentIndex?: Record) => { - const remaps = Object.entries(labelToSegmentIndex ?? {}).some( - ([labelValue, segmentIndex]) => Number(labelValue) !== segmentIndex - ); - if (!remaps) { - return pixelData; - } - - const segmentIndexPixels = new pixelData.constructor(pixelData.length); - for (let i = 0; i < pixelData.length; i++) { - const labelValue = pixelData[i]; - segmentIndexPixels[i] = - labelValue === 0 ? 0 : (labelToSegmentIndex[labelValue] ?? labelValue); - } - return segmentIndexPixels; - }; + const labelRemaps = getLabelRemaps(labelmapData); + let unmappedVoxels = 0; // Build a labelmap3D (one labelmaps2D entry per source slice) from a list of // derived labelmap image ids. When `referencedImageIds` is supplied (the @@ -142,7 +127,7 @@ const commandsModule = ({ segImageIds: string[], metadata, referencedImageIds?: string[], - labelToSegmentIndex?: Record + labelRemap?: LabelRemap ) => { const segImages = segImageIds.map(imageId => cache.getImage(imageId)); const labelmaps2D = []; @@ -156,18 +141,10 @@ const commandsModule = ({ let z = 0; for (const segImage of segImages) { - const segmentsOnLabelmap = new Set(); - const pixelData = toSegmentIndexPixels(segImage.getPixelData(), labelToSegmentIndex); + const slice = toSegmentIndexPixels(segImage.getPixelData(), labelRemap); + unmappedVoxels += slice.unmappedVoxels; const { rows, columns } = segImage; - // Use a single pass through the pixel data - for (let i = 0; i < pixelData.length; i++) { - const segment = pixelData[i]; - if (segment !== 0) { - segmentsOnLabelmap.add(segment); - } - } - const frameIndex = referencedFrameIndexById ? (referencedFrameIndexById.get(segImage.referencedImageId) ?? -1) : z++; @@ -177,8 +154,8 @@ const commandsModule = ({ } labelmaps2D[frameIndex] = { - segmentsOnLabelmap: Array.from(segmentsOnLabelmap), - pixelData, + segmentsOnLabelmap: slice.segmentsOnLabelmap, + pixelData: slice.pixelData, rows, columns, }; @@ -272,22 +249,30 @@ const commandsModule = ({ layer.imageIds ?? [], metadata, referencedImageIds, - layer.labelToSegmentIndex + labelRemaps.get(layer.labelmapId) ) ); } else { - const { imageIds } = labelmapData; + // The only layer can be a private overlap layer. Read its own image ids, + // so that the pixels and the remap come from the same layer. + const layer = layers?.[0]; + const imageIds = layer?.imageIds ?? labelmapData.imageIds; const segImages = imageIds.map(imageId => cache.getImage(imageId)); referencedImages = segImages.map((image, sliceIndex) => resolveReferencedImage(image.referencedImageId, sliceIndex) ); - // The only layer can be a private overlap layer, for example after the - // user deletes the segments of the original layer. labelmaps3D = buildLabelmap3D( imageIds, metadata, undefined, - layers?.[0]?.labelToSegmentIndex + layer && labelRemaps.get(layer.labelmapId) + ); + } + + if (unmappedVoxels) { + console.warn( + `generateSegmentation: ${unmappedVoxels} labelmap voxels have a label value ` + + 'that is bound to no segment; the SEG does not include them.' ); } diff --git a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts new file mode 100644 index 00000000000..ab985def770 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts @@ -0,0 +1,103 @@ +type SegmentBinding = { labelmapId: string; labelValue: number }; + +/** + * A label remap of one labelmap layer: the map from a label value in the voxels + * to the segment index, and the typed array that holds every mapped index. + */ +export type LabelRemap = { + segmentIndexByLabel: Map; + SegmentIndexArray: Uint8ArrayConstructor | Uint16ArrayConstructor; +}; + +/** + * Gets the label remap of each labelmap layer whose label values are not the + * segment indices. + * + * cs3D stores a segment in a private overlap layer under a label value that is + * not its segment index (usually 1). The SEG adapter reads each voxel value as a + * segment number, so the export must map the label values first. + * + * The remaps come from `segmentBindings`, which is the source that cs3D uses to + * rebuild `labelToSegmentIndex` of each layer. A layer that has no remap keeps + * its voxel values. + * + * @returns The label remap for each `labelmapId` that needs one. + */ +export function getLabelRemaps(labelmapData): Map { + const segmentIndexByLabelByLayer = new Map>(); + const remappedLayerIds = new Set(); + + Object.entries((labelmapData?.segmentBindings ?? {}) as Record).forEach( + ([segmentIndexKey, { labelmapId, labelValue }]) => { + const segmentIndex = Number(segmentIndexKey); + let segmentIndexByLabel = segmentIndexByLabelByLayer.get(labelmapId); + if (!segmentIndexByLabel) { + segmentIndexByLabel = new Map(); + segmentIndexByLabelByLayer.set(labelmapId, segmentIndexByLabel); + } + segmentIndexByLabel.set(labelValue, segmentIndex); + if (labelValue !== segmentIndex) { + remappedLayerIds.add(labelmapId); + } + } + ); + + const remaps = new Map(); + remappedLayerIds.forEach(labelmapId => { + const segmentIndexByLabel = segmentIndexByLabelByLayer.get(labelmapId); + const maxSegmentIndex = Math.max(...segmentIndexByLabel.values()); + remaps.set(labelmapId, { + segmentIndexByLabel, + SegmentIndexArray: maxSegmentIndex > 255 ? Uint16Array : Uint8Array, + }); + }); + return remaps; +} + +/** + * Maps the label values of one labelmap slice to segment indices, and collects + * the segments on the slice, in one pass. + * + * The cached labelmap image does not change. The function copies the slice only + * when a voxel value changes, so an empty slice is never copied. A non-zero + * label value that has no entry in the remap belongs to no segment: the function + * writes 0 for that voxel and counts it in `unmappedVoxels`. + * + * @param pixelData - The voxel values of the labelmap slice. + * @param remap - The label remap of the layer, or undefined to keep the values. + */ +export function toSegmentIndexPixels(pixelData, remap?: LabelRemap) { + const segmentsOnLabelmap = new Set(); + let segmentIndexPixels = pixelData; + let unmappedVoxels = 0; + + for (let i = 0; i < pixelData.length; i++) { + const labelValue = pixelData[i]; + if (labelValue === 0) { + continue; + } + if (!remap) { + segmentsOnLabelmap.add(labelValue); + continue; + } + + const segmentIndex = remap.segmentIndexByLabel.get(labelValue) ?? 0; + if (segmentIndex === 0) { + unmappedVoxels++; + } else { + segmentsOnLabelmap.add(segmentIndex); + } + if (segmentIndex !== labelValue) { + if (segmentIndexPixels === pixelData) { + segmentIndexPixels = new remap.SegmentIndexArray(pixelData); + } + segmentIndexPixels[i] = segmentIndex; + } + } + + return { + pixelData: segmentIndexPixels, + segmentsOnLabelmap: Array.from(segmentsOnLabelmap), + unmappedVoxels, + }; +} diff --git a/tests/LabelMapSegOverlapLayerSave.spec.ts b/tests/LabelMapSegOverlapLayerSave.spec.ts index aa5beb51430..4c1dbf244d7 100644 --- a/tests/LabelMapSegOverlapLayerSave.spec.ts +++ b/tests/LabelMapSegOverlapLayerSave.spec.ts @@ -38,6 +38,13 @@ test('should save each segment of an overlap layer under its own segment number' await labelMapPanel.panel.nthSegment(0).actions.toggleLock(); await viewport.normalizedDragAt({ start: { x: 0.5, y: 0.3 }, end: { x: 0.5, y: 0.7 } }); + // The test covers the remap only when segment 2 is in a separate layer. + const layerCount = await page.evaluate(() => { + const [segmentation] = (window as any).cornerstoneTools.segmentation.state.getSegmentations(); + return Object.keys(segmentation.representationData.Labelmap.labelmaps ?? {}).length; + }); + expect(layerCount).toBeGreaterThan(1); + // The save needs every source image of the series in the cache. await expect .poll( From e1560c088b650e144c196a6b53a950367814562c Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 14 Oct 2026 13:33:56 -0400 Subject: [PATCH 03/16] fix(seg): keep unbound label values like the renderer, and tidy the remap An unbound label value now keeps its value, as the cs3D renderer and getSegmentIndexForLabelValue do, so the SEG holds what the viewport shows. The remap runs only on exported slices, the single-layer path applies a remap only to the image ids of that layer, and two stale comments are fixed. Declare dcmjs at the root for the e2e test. Co-Authored-By: Claude Opus 5.5 --- .../src/commandsModule.ts | 31 ++++++---------- .../src/utils/labelmapLayerSegmentIndices.ts | 36 +++++++++---------- package.json | 1 + pnpm-lock.yaml | 3 ++ 4 files changed, 31 insertions(+), 40 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index fa1d03b843a..3becc6b1a34 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -116,13 +116,12 @@ const commandsModule = ({ const labelmapData = segmentation.representationData.Labelmap; const labelRemaps = getLabelRemaps(labelmapData); - let unmappedVoxels = 0; // Build a labelmap3D (one labelmaps2D entry per source slice) from a list of - // derived labelmap image ids. When `referencedImageIds` is supplied (the - // multi-layer/overlap path) each frame is indexed by its source slice so the - // layers align to the same frames; otherwise frames are sequential (the legacy - // single-layer behavior, kept byte-identical). + // derived labelmap image ids, with the label values mapped to segment indices. + // When `referencedImageIds` is supplied (the multi-layer/overlap path) each + // frame is indexed by its source slice so the layers align to the same frames; + // otherwise frames are sequential. const buildLabelmap3D = ( segImageIds: string[], metadata, @@ -141,10 +140,6 @@ const commandsModule = ({ let z = 0; for (const segImage of segImages) { - const slice = toSegmentIndexPixels(segImage.getPixelData(), labelRemap); - unmappedVoxels += slice.unmappedVoxels; - const { rows, columns } = segImage; - const frameIndex = referencedFrameIndexById ? (referencedFrameIndexById.get(segImage.referencedImageId) ?? -1) : z++; @@ -153,6 +148,9 @@ const commandsModule = ({ continue; } + const slice = toSegmentIndexPixels(segImage.getPixelData(), labelRemap); + const { rows, columns } = segImage; + labelmaps2D[frameIndex] = { segmentsOnLabelmap: slice.segmentsOnLabelmap, pixelData: slice.pixelData, @@ -220,7 +218,7 @@ const commandsModule = ({ // source series, so cornerstone writes overlapping segments as separate frames // that reference the same source slice (the DICOM SEG overlap encoding). The // cs3D adapter's fillSegmentation accepts an array of labelmap3D for exactly - // this. Single-layer SEGs keep the original single-labelmap3D path unchanged. + // this. A single-layer SEG uses one labelmap3D with sequential frames. const layers = labelmapData.labelmaps ? Object.values(labelmapData.labelmaps) : undefined; // The referenced source images must be fully loaded (in cache) before we can @@ -253,8 +251,8 @@ const commandsModule = ({ ) ); } else { - // The only layer can be a private overlap layer. Read its own image ids, - // so that the pixels and the remap come from the same layer. + // The only layer can be a private overlap layer. Apply its remap only to + // its own image ids, so that the pixels and the remap match. const layer = layers?.[0]; const imageIds = layer?.imageIds ?? labelmapData.imageIds; const segImages = imageIds.map(imageId => cache.getImage(imageId)); @@ -265,14 +263,7 @@ const commandsModule = ({ imageIds, metadata, undefined, - layer && labelRemaps.get(layer.labelmapId) - ); - } - - if (unmappedVoxels) { - console.warn( - `generateSegmentation: ${unmappedVoxels} labelmap voxels have a label value ` + - 'that is bound to no segment; the SEG does not include them.' + layer?.imageIds ? labelRemaps.get(layer.labelmapId) : undefined ); } diff --git a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts index ab985def770..9e822ee2f44 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts @@ -2,11 +2,11 @@ type SegmentBinding = { labelmapId: string; labelValue: number }; /** * A label remap of one labelmap layer: the map from a label value in the voxels - * to the segment index, and the typed array that holds every mapped index. + * to the segment index, and the largest segment index in the map. */ export type LabelRemap = { segmentIndexByLabel: Map; - SegmentIndexArray: Uint8ArrayConstructor | Uint16ArrayConstructor; + maxSegmentIndex: number; }; /** @@ -45,10 +45,9 @@ export function getLabelRemaps(labelmapData): Map { const remaps = new Map(); remappedLayerIds.forEach(labelmapId => { const segmentIndexByLabel = segmentIndexByLabelByLayer.get(labelmapId); - const maxSegmentIndex = Math.max(...segmentIndexByLabel.values()); remaps.set(labelmapId, { segmentIndexByLabel, - SegmentIndexArray: maxSegmentIndex > 255 ? Uint16Array : Uint8Array, + maxSegmentIndex: Math.max(...segmentIndexByLabel.values()), }); }); return remaps; @@ -58,10 +57,13 @@ export function getLabelRemaps(labelmapData): Map { * Maps the label values of one labelmap slice to segment indices, and collects * the segments on the slice, in one pass. * + * A label value that has no entry in the remap keeps its value. The cs3D + * renderer and `getSegmentIndexForLabelValue` use the same fallback, so the SEG + * holds the segments that the viewport shows. + * * The cached labelmap image does not change. The function copies the slice only - * when a voxel value changes, so an empty slice is never copied. A non-zero - * label value that has no entry in the remap belongs to no segment: the function - * writes 0 for that voxel and counts it in `unmappedVoxels`. + * when a voxel value changes, so an empty slice is never copied. The copy is a + * `Uint16Array` when a segment index does not fit in the source array type. * * @param pixelData - The voxel values of the labelmap slice. * @param remap - The label remap of the layer, or undefined to keep the values. @@ -69,27 +71,22 @@ export function getLabelRemaps(labelmapData): Map { export function toSegmentIndexPixels(pixelData, remap?: LabelRemap) { const segmentsOnLabelmap = new Set(); let segmentIndexPixels = pixelData; - let unmappedVoxels = 0; for (let i = 0; i < pixelData.length; i++) { const labelValue = pixelData[i]; if (labelValue === 0) { continue; } - if (!remap) { - segmentsOnLabelmap.add(labelValue); - continue; - } - const segmentIndex = remap.segmentIndexByLabel.get(labelValue) ?? 0; - if (segmentIndex === 0) { - unmappedVoxels++; - } else { - segmentsOnLabelmap.add(segmentIndex); - } + const segmentIndex = remap?.segmentIndexByLabel.get(labelValue) ?? labelValue; + segmentsOnLabelmap.add(segmentIndex); if (segmentIndex !== labelValue) { if (segmentIndexPixels === pixelData) { - segmentIndexPixels = new remap.SegmentIndexArray(pixelData); + const SegmentIndexArray = + remap.maxSegmentIndex > 255 && pixelData.BYTES_PER_ELEMENT < 2 + ? Uint16Array + : pixelData.constructor; + segmentIndexPixels = new SegmentIndexArray(pixelData); } segmentIndexPixels[i] = segmentIndex; } @@ -98,6 +95,5 @@ export function toSegmentIndexPixels(pixelData, remap?: LabelRemap) { return { pixelData: segmentIndexPixels, segmentsOnLabelmap: Array.from(segmentsOnLabelmap), - unmappedVoxels, }; } diff --git a/package.json b/package.json index 353e1b8b0e2..c9bbbc3ad85 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "css-loader": "6.11.0", "cypress": "14.5.2", "cypress-file-upload": "5.0.8", + "dcmjs": "0.52.0", "eslint": "10.7.0", "eslint-plugin-react-hooks": "7.1.1", "file-loader": "6.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 428311ee643..117d921783c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -327,6 +327,9 @@ importers: cypress-file-upload: specifier: 5.0.8 version: 5.0.8(cypress@14.5.2) + dcmjs: + specifier: 0.52.0 + version: 0.52.0 eslint: specifier: 10.7.0 version: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) From 5406f67a9600ace9d98c7bf6937d2b5cda745082 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 14 Oct 2026 14:05:46 -0400 Subject: [PATCH 04/16] test(seg): add unit tests for the labelmap layer remap Add a Jest config and test scripts to the SEG extension, and test the remap, the fallback for unbound labels, the no-copy rule, and the Uint16Array widening. Co-Authored-By: Claude Opus 5.5 --- .../cornerstone-dicom-seg/jest.config.js | 9 +++ extensions/cornerstone-dicom-seg/package.json | 4 +- .../utils/labelmapLayerSegmentIndices.test.ts | 60 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 extensions/cornerstone-dicom-seg/jest.config.js create mode 100644 extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts diff --git a/extensions/cornerstone-dicom-seg/jest.config.js b/extensions/cornerstone-dicom-seg/jest.config.js new file mode 100644 index 00000000000..129dcb6c58b --- /dev/null +++ b/extensions/cornerstone-dicom-seg/jest.config.js @@ -0,0 +1,9 @@ +const base = require('../../jest.config.base.js'); + +module.exports = { + ...base, + moduleNameMapper: { + ...base.moduleNameMapper, + '@ohif/(.*)': '/../../platform/$1/src', + }, +}; diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json index 7c00b887cdd..86d4ba3e3a7 100644 --- a/extensions/cornerstone-dicom-seg/package.json +++ b/extensions/cornerstone-dicom-seg/package.json @@ -25,7 +25,9 @@ "dev:dicom-seg": "pnpm run dev", "build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js", "build:package-1": "pnpm run build", - "start": "pnpm run dev" + "start": "pnpm run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { "@ohif/core": "workspace:*", diff --git a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts new file mode 100644 index 00000000000..6693352d76f --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts @@ -0,0 +1,60 @@ +import { getLabelRemaps, toSegmentIndexPixels } from './labelmapLayerSegmentIndices'; + +// Segments 1 and 3 are in the primary layer. Segment 2 is in a private layer +// under label value 1. +const labelmapData = { + segmentBindings: { + 1: { labelmapId: 'primary', labelValue: 1 }, + 2: { labelmapId: 'private', labelValue: 1 }, + 3: { labelmapId: 'primary', labelValue: 3 }, + }, +}; + +describe('getLabelRemaps', () => { + it('gives a remap only to a layer whose label values are not the segment indices', () => { + const remaps = getLabelRemaps(labelmapData); + + expect([...remaps.keys()]).toEqual(['private']); + expect(remaps.get('private').segmentIndexByLabel.get(1)).toBe(2); + }); +}); + +describe('toSegmentIndexPixels', () => { + const privateRemap = getLabelRemaps(labelmapData).get('private'); + + it('maps label values to segment indices, and keeps the cached pixels', () => { + const pixelData = new Uint8Array([0, 1, 1, 0]); + + const slice = toSegmentIndexPixels(pixelData, privateRemap); + + expect(Array.from(slice.pixelData)).toEqual([0, 2, 2, 0]); + expect(slice.segmentsOnLabelmap).toEqual([2]); + expect(Array.from(pixelData)).toEqual([0, 1, 1, 0]); + }); + + it('keeps a label value that has no binding, as the cs3D renderer does', () => { + const slice = toSegmentIndexPixels(new Uint8Array([1, 5]), privateRemap); + + expect(Array.from(slice.pixelData)).toEqual([2, 5]); + expect(slice.segmentsOnLabelmap).toEqual([2, 5]); + }); + + it('does not copy a slice when no value changes', () => { + const empty = new Uint8Array(4); + const primary = new Uint8Array([0, 1, 3, 0]); + + expect(toSegmentIndexPixels(empty, privateRemap).pixelData).toBe(empty); + expect(toSegmentIndexPixels(primary).pixelData).toBe(primary); + }); + + it('uses a Uint16Array when a segment index does not fit in a byte', () => { + const remap = getLabelRemaps({ + segmentBindings: { 300: { labelmapId: 'private', labelValue: 1 } }, + }).get('private'); + + const slice = toSegmentIndexPixels(new Uint8Array([0, 1]), remap); + + expect(slice.pixelData).toBeInstanceOf(Uint16Array); + expect(Array.from(slice.pixelData)).toEqual([0, 300]); + }); +}); From 766b01774a8663da821258939d90d359b026dae3 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 14 Oct 2026 14:07:03 -0400 Subject: [PATCH 05/16] docs(seg): note the follow-ups for cornerstone3D#2988 in comments The overlap test depends on the current lock behavior, and the label remap can move to the SEG adapter after OHIF moves to that release. Co-Authored-By: Claude Opus 5.5 --- .../src/utils/labelmapLayerSegmentIndices.ts | 4 ++++ tests/LabelMapSegOverlapLayerSave.spec.ts | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts index 9e822ee2f44..427c621ced5 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts @@ -21,6 +21,10 @@ export type LabelRemap = { * rebuild `labelToSegmentIndex` of each layer. A layer that has no remap keeps * its voxel values. * + * cornerstonejs/cornerstone3D#2988 lets the SEG adapter take + * `labelmap3D.labelToSegmentIndex`. After OHIF moves to that release, the export + * can set that map on each labelmap3D, and this file can be removed. + * * @returns The label remap for each `labelmapId` that needs one. */ export function getLabelRemaps(labelmapData): Map { diff --git a/tests/LabelMapSegOverlapLayerSave.spec.ts b/tests/LabelMapSegOverlapLayerSave.spec.ts index 4c1dbf244d7..a99b0d2dfaa 100644 --- a/tests/LabelMapSegOverlapLayerSave.spec.ts +++ b/tests/LabelMapSegOverlapLayerSave.spec.ts @@ -8,6 +8,10 @@ import { expect, test, visitStudy } from './utils'; * separate labelmap layer, where it is stored under another label value. The * DICOM SEG save must still write each segment under its own segment number, * label and color. + * + * With cornerstonejs/cornerstone3D#2988 a lock stops the brush and creates no + * layer. After OHIF moves to that release, the test must create the overlap with + * the explicit option (for example `segmentation.overwriteMode: 'none'`). */ test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458'; @@ -38,7 +42,8 @@ test('should save each segment of an overlap layer under its own segment number' await labelMapPanel.panel.nthSegment(0).actions.toggleLock(); await viewport.normalizedDragAt({ start: { x: 0.5, y: 0.3 }, end: { x: 0.5, y: 0.7 } }); - // The test covers the remap only when segment 2 is in a separate layer. + // The test covers the remap only when segment 2 is in a separate layer. This + // check fails when a lock stops the brush (see the comment at the top). const layerCount = await page.evaluate(() => { const [segmentation] = (window as any).cornerstoneTools.segmentation.state.getSegmentations(); return Object.keys(segmentation.representationData.Labelmap.labelmaps ?? {}).length; From 2ab0e90ad8cca992c4bf3e1631a80cc95a64e7bd Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 14 Oct 2026 14:17:38 -0400 Subject: [PATCH 06/16] fix(seg): enable the SEG export when only an overlap layer has data hasExportableLabelMapData now checks the images of every labelmap layer, not only the legacy primary image ids. The e2e test also deletes segment 1 and saves the lone private layer of segment 2, and it waits on the 'default' viewport. Co-Authored-By: Claude Opus 5.5 --- .../src/utils/segmentationExportUtils.ts | 6 +- tests/LabelMapSegOverlapLayerSave.spec.ts | 72 ++++++++++++------- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/extensions/cornerstone/src/utils/segmentationExportUtils.ts b/extensions/cornerstone/src/utils/segmentationExportUtils.ts index dddd4e6a5c8..5d9153cadc3 100644 --- a/extensions/cornerstone/src/utils/segmentationExportUtils.ts +++ b/extensions/cornerstone/src/utils/segmentationExportUtils.ts @@ -18,7 +18,11 @@ export const hasExportableLabelMapData = ( return false; } - const imageIds = labelmap?.imageIds; + // A segment can be in an overlap layer only, so check the images of every layer. + const layers = Object.values(labelmap.labelmaps ?? {}); + const imageIds = layers.length + ? layers.flatMap(layer => layer.imageIds ?? []) + : labelmap.imageIds; if (!imageIds?.length) { return false; } diff --git a/tests/LabelMapSegOverlapLayerSave.spec.ts b/tests/LabelMapSegOverlapLayerSave.spec.ts index a99b0d2dfaa..122e86be9a7 100644 --- a/tests/LabelMapSegOverlapLayerSave.spec.ts +++ b/tests/LabelMapSegOverlapLayerSave.spec.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import dcmjs from 'dcmjs'; +import type { Page } from '@playwright/test'; import { expect, test, visitStudy } from './utils'; @@ -19,6 +20,34 @@ test.beforeEach(async ({ page }) => { await visitStudy(page, studyInstanceUID, mode, 2000); }); +const asArray = value => (Array.isArray(value) ? value : [value]); + +const getLayerCount = (page: Page) => + page.evaluate(() => { + const [segmentation] = (window as any).cornerstoneTools.segmentation.state.getSegmentations(); + return Object.keys(segmentation.representationData.Labelmap.labelmaps ?? {}).length; + }); + +/** Saves the segmentation as DICOM SEG with the Download button, and reads the file. */ +const saveAndReadSeg = async (page: Page, labelMapPanel) => { + await labelMapPanel.panel.moreMenu.click(); + await page.getByRole('menuitem', { name: 'Save' }).click(); + await page.getByRole('menuitem', { name: 'DICOM SEG' }).click(); + + const downloadPromise = page.waitForEvent('download'); + await page.getByTestId('report-download-button').click(); + const downloadPath = await (await downloadPromise).path(); + if (!downloadPath) { + throw new Error('The save produced no downloaded file'); + } + + const bytes = fs.readFileSync(downloadPath); + const arrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); + return dcmjs.data.DicomMetaDictionary.naturalizeDataset( + dcmjs.data.DicomMessage.readFile(arrayBuffer).dict + ); +}; + test('should save each segment of an overlap layer under its own segment number', async ({ page, rightPanelPageObject, @@ -44,43 +73,22 @@ test('should save each segment of an overlap layer under its own segment number' // The test covers the remap only when segment 2 is in a separate layer. This // check fails when a lock stops the brush (see the comment at the top). - const layerCount = await page.evaluate(() => { - const [segmentation] = (window as any).cornerstoneTools.segmentation.state.getSegmentations(); - return Object.keys(segmentation.representationData.Labelmap.labelmaps ?? {}).length; - }); - expect(layerCount).toBeGreaterThan(1); + expect(await getLayerCount(page)).toBeGreaterThan(1); // The save needs every source image of the series in the cache. await expect .poll( () => page.evaluate(() => { - const { cache, getEnabledElements } = (window as any).cornerstone; - const imageIds = getEnabledElements()[0].viewport.getImageIds(); + const { cache, getEnabledElementByViewportId } = (window as any).cornerstone; + const imageIds = getEnabledElementByViewportId('default').viewport.getImageIds(); return imageIds.every(imageId => cache.getImage(imageId)); }), { timeout: 60_000 } ) .toBe(true); - await labelMapPanel.panel.moreMenu.click(); - await page.getByRole('menuitem', { name: 'Save' }).click(); - await page.getByRole('menuitem', { name: 'DICOM SEG' }).click(); - - const downloadPromise = page.waitForEvent('download'); - await page.getByTestId('report-download-button').click(); - const downloadPath = await (await downloadPromise).path(); - if (!downloadPath) { - throw new Error('The save produced no downloaded file'); - } - - const bytes = fs.readFileSync(downloadPath); - const arrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); - const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( - dcmjs.data.DicomMessage.readFile(arrayBuffer).dict - ); - - const asArray = value => (Array.isArray(value) ? value : [value]); + const dataset = await saveAndReadSeg(page, labelMapPanel); const segments = asArray(dataset.SegmentSequence); expect(segments.map(segment => segment.SegmentLabel)).toEqual(['Segment 1', 'Segment 2']); expect(segments[0].RecommendedDisplayCIELabValue).not.toEqual( @@ -94,4 +102,18 @@ test('should save each segment of an overlap layer under its own segment number' } expect(framesPerSegment[1]).toBeGreaterThan(0); expect(framesPerSegment[2]).toBeGreaterThan(0); + + // Delete segment 1. The private layer of segment 2 is then the only layer. + await labelMapPanel.panel.nthSegment(0).actions.toggleLock(); + await labelMapPanel.panel.nthSegment(0).actions.delete(); + await expect(labelMapPanel.panel.rows).toHaveCount(1); + expect(await getLayerCount(page)).toBe(1); + + const onlySegment2 = await saveAndReadSeg(page, labelMapPanel); + const onlySegments = asArray(onlySegment2.SegmentSequence); + expect(onlySegments.map(segment => segment.SegmentLabel)).toEqual(['Segment 2']); + expect(onlySegments[0].RecommendedDisplayCIELabValue).toEqual( + segments[1].RecommendedDisplayCIELabValue + ); + expect(Number(onlySegment2.NumberOfFrames)).toBeGreaterThan(0); }); From 3f8b44d97f0d6461b8aaa7d609a6db5c5f5b6506 Mon Sep 17 00:00:00 2001 From: Thomas Forster Date: Thu, 15 Oct 2026 16:17:43 -0300 Subject: [PATCH 07/16] fix(seg): export with the label map of each layer, and drop label values that have no segment Read labelToSegmentIndex from each labelmap layer instead of rebuilding it from segmentBindings. A label value that has no segment is written as 0 with one warning per value, so a LABELMAP SEG never refers to a segment that is not in SegmentSequence. The copy is a Uint8Array up to segment index 255, else a Uint16Array. A single-layer export applies the label map of its layer also when the layer has no image ids. --- .../src/commandsModule.ts | 35 ++--- .../utils/labelmapLayerSegmentIndices.test.ts | 83 ++++++------ .../src/utils/labelmapLayerSegmentIndices.ts | 125 +++++++++--------- 3 files changed, 124 insertions(+), 119 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 3becc6b1a34..1844552baca 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -12,9 +12,9 @@ import { BITMAP_SEG_SOP_CLASS_UID, } from './utils/segmentationConfig'; import { - getLabelRemaps, + getSingleLayerExportSource, toSegmentIndexPixels, - type LabelRemap, + type LabelToSegmentIndex, } from './utils/labelmapLayerSegmentIndices'; const getTargetViewport = ({ viewportId, viewportGridService }) => { @@ -115,8 +115,6 @@ const commandsModule = ({ const labelmapData = segmentation.representationData.Labelmap; - const labelRemaps = getLabelRemaps(labelmapData); - // Build a labelmap3D (one labelmaps2D entry per source slice) from a list of // derived labelmap image ids, with the label values mapped to segment indices. // When `referencedImageIds` is supplied (the multi-layer/overlap path) each @@ -126,7 +124,7 @@ const commandsModule = ({ segImageIds: string[], metadata, referencedImageIds?: string[], - labelRemap?: LabelRemap + labelToSegmentIndex?: LabelToSegmentIndex ) => { const segImages = segImageIds.map(imageId => cache.getImage(imageId)); const labelmaps2D = []; @@ -138,6 +136,7 @@ const commandsModule = ({ : undefined; let z = 0; + const unboundLabels = new Set(); for (const segImage of segImages) { const frameIndex = referencedFrameIndexById @@ -148,7 +147,11 @@ const commandsModule = ({ continue; } - const slice = toSegmentIndexPixels(segImage.getPixelData(), labelRemap); + const slice = toSegmentIndexPixels( + segImage.getPixelData(), + labelToSegmentIndex, + labelValue => unboundLabels.add(labelValue) + ); const { rows, columns } = segImage; labelmaps2D[frameIndex] = { @@ -159,6 +162,12 @@ const commandsModule = ({ }; } + unboundLabels.forEach(labelValue => + console.warn( + `Label value ${labelValue} of segmentation ${segmentationId} has no segment. The SEG export does not include its voxels.` + ) + ); + const allSegmentsOnLabelmap = labelmaps2D .filter(Boolean) .map(labelmap => labelmap.segmentsOnLabelmap); @@ -247,24 +256,16 @@ const commandsModule = ({ layer.imageIds ?? [], metadata, referencedImageIds, - labelRemaps.get(layer.labelmapId) + layer.labelToSegmentIndex ) ); } else { - // The only layer can be a private overlap layer. Apply its remap only to - // its own image ids, so that the pixels and the remap match. - const layer = layers?.[0]; - const imageIds = layer?.imageIds ?? labelmapData.imageIds; + const { imageIds, labelToSegmentIndex } = getSingleLayerExportSource(labelmapData); const segImages = imageIds.map(imageId => cache.getImage(imageId)); referencedImages = segImages.map((image, sliceIndex) => resolveReferencedImage(image.referencedImageId, sliceIndex) ); - labelmaps3D = buildLabelmap3D( - imageIds, - metadata, - undefined, - layer?.imageIds ? labelRemaps.get(layer.labelmapId) : undefined - ); + labelmaps3D = buildLabelmap3D(imageIds, metadata, undefined, labelToSegmentIndex); } // `...generateOptions` has the last word on the predecessor: the store diff --git a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts index 6693352d76f..8adf556b27c 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts @@ -1,60 +1,67 @@ -import { getLabelRemaps, toSegmentIndexPixels } from './labelmapLayerSegmentIndices'; - -// Segments 1 and 3 are in the primary layer. Segment 2 is in a private layer -// under label value 1. -const labelmapData = { - segmentBindings: { - 1: { labelmapId: 'primary', labelValue: 1 }, - 2: { labelmapId: 'private', labelValue: 1 }, - 3: { labelmapId: 'primary', labelValue: 3 }, - }, -}; - -describe('getLabelRemaps', () => { - it('gives a remap only to a layer whose label values are not the segment indices', () => { - const remaps = getLabelRemaps(labelmapData); - - expect([...remaps.keys()]).toEqual(['private']); - expect(remaps.get('private').segmentIndexByLabel.get(1)).toBe(2); - }); -}); +import { getSingleLayerExportSource, toSegmentIndexPixels } from './labelmapLayerSegmentIndices'; -describe('toSegmentIndexPixels', () => { - const privateRemap = getLabelRemaps(labelmapData).get('private'); +// Segment 2 is in a private layer under label value 1. +const privateLabelToSegmentIndex = { 1: 2 }; +describe('toSegmentIndexPixels', () => { it('maps label values to segment indices, and keeps the cached pixels', () => { const pixelData = new Uint8Array([0, 1, 1, 0]); - const slice = toSegmentIndexPixels(pixelData, privateRemap); + const slice = toSegmentIndexPixels(pixelData, privateLabelToSegmentIndex); expect(Array.from(slice.pixelData)).toEqual([0, 2, 2, 0]); expect(slice.segmentsOnLabelmap).toEqual([2]); expect(Array.from(pixelData)).toEqual([0, 1, 1, 0]); }); - it('keeps a label value that has no binding, as the cs3D renderer does', () => { - const slice = toSegmentIndexPixels(new Uint8Array([1, 5]), privateRemap); + it('keeps the values when the layer has no label map', () => { + const slice = toSegmentIndexPixels(new Uint8Array([0, 1, 3])); - expect(Array.from(slice.pixelData)).toEqual([2, 5]); - expect(slice.segmentsOnLabelmap).toEqual([2, 5]); + expect(Array.from(slice.pixelData)).toEqual([0, 1, 3]); + expect(slice.segmentsOnLabelmap).toEqual([1, 3]); }); - it('does not copy a slice when no value changes', () => { - const empty = new Uint8Array(4); - const primary = new Uint8Array([0, 1, 3, 0]); + it('does not export a label value that has no segment, and reports it once', () => { + const onUnboundLabel = jest.fn(); - expect(toSegmentIndexPixels(empty, privateRemap).pixelData).toBe(empty); - expect(toSegmentIndexPixels(primary).pixelData).toBe(primary); + const slice = toSegmentIndexPixels( + new Uint8Array([1, 5, 5]), + privateLabelToSegmentIndex, + onUnboundLabel + ); + + expect(Array.from(slice.pixelData)).toEqual([2, 0, 0]); + expect(slice.segmentsOnLabelmap).toEqual([2]); + expect(onUnboundLabel.mock.calls).toEqual([[5]]); }); - it('uses a Uint16Array when a segment index does not fit in a byte', () => { - const remap = getLabelRemaps({ - segmentBindings: { 300: { labelmapId: 'private', labelValue: 1 } }, - }).get('private'); + it('writes segment indices up to 255 from an Int8Array without overflow', () => { + const slice = toSegmentIndexPixels(new Int8Array([0, 1]), { 1: 200 }); - const slice = toSegmentIndexPixels(new Uint8Array([0, 1]), remap); + expect(Array.from(slice.pixelData)).toEqual([0, 200]); + }); + + it('writes a segment index above 255', () => { + const slice = toSegmentIndexPixels(new Uint8Array([0, 1]), { 1: 300 }); - expect(slice.pixelData).toBeInstanceOf(Uint16Array); expect(Array.from(slice.pixelData)).toEqual([0, 300]); }); }); + +describe('getSingleLayerExportSource', () => { + it('applies the label map of the only layer when the layer has no image ids', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + const source = getSingleLayerExportSource({ + imageIds: ['labelmap:1'], + labelmaps: { + private: { labelmapId: 'private', volumeId: 'v', labelToSegmentIndex: { 1: 2 } }, + }, + }); + + expect(source.imageIds).toEqual(['labelmap:1']); + expect(source.labelToSegmentIndex).toEqual({ 1: 2 }); + expect(warn).toHaveBeenCalledTimes(1); + warn.mockRestore(); + }); +}); diff --git a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts index 427c621ced5..ee336b95647 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts @@ -1,79 +1,68 @@ -type SegmentBinding = { labelmapId: string; labelValue: number }; +/** The map from a label value in the voxels of a labelmap layer to the segment index. */ +export type LabelToSegmentIndex = { [labelValue: number]: number }; -/** - * A label remap of one labelmap layer: the map from a label value in the voxels - * to the segment index, and the largest segment index in the map. - */ -export type LabelRemap = { - segmentIndexByLabel: Map; - maxSegmentIndex: number; +type LabelmapLayer = { + labelmapId?: string; + imageIds?: string[]; + labelToSegmentIndex?: LabelToSegmentIndex; }; /** - * Gets the label remap of each labelmap layer whose label values are not the - * segment indices. - * - * cs3D stores a segment in a private overlap layer under a label value that is - * not its segment index (usually 1). The SEG adapter reads each voxel value as a - * segment number, so the export must map the label values first. + * Gets the image ids and the label map that the export of a single-layer + * segmentation uses. * - * The remaps come from `segmentBindings`, which is the source that cs3D uses to - * rebuild `labelToSegmentIndex` of each layer. A layer that has no remap keeps - * its voxel values. - * - * cornerstonejs/cornerstone3D#2988 lets the SEG adapter take - * `labelmap3D.labelToSegmentIndex`. After OHIF moves to that release, the export - * can set that map on each labelmap3D, and this file can be removed. - * - * @returns The label remap for each `labelmapId` that needs one. + * The only layer can be a private overlap layer, so its label map applies also + * when the layer has no image ids of its own and the legacy `imageIds` are used. */ -export function getLabelRemaps(labelmapData): Map { - const segmentIndexByLabelByLayer = new Map>(); - const remappedLayerIds = new Set(); - - Object.entries((labelmapData?.segmentBindings ?? {}) as Record).forEach( - ([segmentIndexKey, { labelmapId, labelValue }]) => { - const segmentIndex = Number(segmentIndexKey); - let segmentIndexByLabel = segmentIndexByLabelByLayer.get(labelmapId); - if (!segmentIndexByLabel) { - segmentIndexByLabel = new Map(); - segmentIndexByLabelByLayer.set(labelmapId, segmentIndexByLabel); - } - segmentIndexByLabel.set(labelValue, segmentIndex); - if (labelValue !== segmentIndex) { - remappedLayerIds.add(labelmapId); - } - } - ); - - const remaps = new Map(); - remappedLayerIds.forEach(labelmapId => { - const segmentIndexByLabel = segmentIndexByLabelByLayer.get(labelmapId); - remaps.set(labelmapId, { - segmentIndexByLabel, - maxSegmentIndex: Math.max(...segmentIndexByLabel.values()), - }); - }); - return remaps; +export function getSingleLayerExportSource(labelmapData): { + imageIds: string[]; + labelToSegmentIndex?: LabelToSegmentIndex; +} { + const layer: LabelmapLayer | undefined = Object.values(labelmapData.labelmaps ?? {})[0]; + if (layer && !layer.imageIds) { + console.warn( + `Labelmap layer ${layer.labelmapId} has no image ids. The SEG export uses the segmentation image ids with the label map of that layer.` + ); + } + return { + imageIds: layer?.imageIds ?? labelmapData.imageIds, + labelToSegmentIndex: layer?.labelToSegmentIndex, + }; } /** * Maps the label values of one labelmap slice to segment indices, and collects * the segments on the slice, in one pass. * - * A label value that has no entry in the remap keeps its value. The cs3D - * renderer and `getSegmentIndexForLabelValue` use the same fallback, so the SEG - * holds the segments that the viewport shows. + * cs3D stores a segment in a private overlap layer under a label value that is + * not its segment index (usually 1). Each layer keeps the map in + * `labelToSegmentIndex`. The SEG adapter reads each voxel value as a segment + * number, so the export must map the label values first. + * + * A label value that has no entry in the map belongs to no segment, so the SEG + * has no segment metadata for it. Such voxels are written as 0 (not exported), + * and `onUnboundLabel` is called for each such label value. Without a map, the + * label values are kept. * * The cached labelmap image does not change. The function copies the slice only - * when a voxel value changes, so an empty slice is never copied. The copy is a - * `Uint16Array` when a segment index does not fit in the source array type. + * when a voxel value changes. The copy is a `Uint8Array` when all segment + * indices are 255 or less, else a `Uint16Array`. + * + * cornerstonejs/cornerstone3D#2988 lets the SEG adapter take + * `labelmap3D.labelToSegmentIndex`. After OHIF moves to that release, the export + * can set that map on each labelmap3D, and this function can be removed. * * @param pixelData - The voxel values of the labelmap slice. - * @param remap - The label remap of the layer, or undefined to keep the values. + * @param labelToSegmentIndex - The label map of the layer, or undefined to keep the values. + * @param onUnboundLabel - Called with each label value that has no entry in the map. */ -export function toSegmentIndexPixels(pixelData, remap?: LabelRemap) { +export function toSegmentIndexPixels( + pixelData, + labelToSegmentIndex?: LabelToSegmentIndex, + onUnboundLabel?: (labelValue: number) => void +) { const segmentsOnLabelmap = new Set(); + const unboundLabels = new Set(); let segmentIndexPixels = pixelData; for (let i = 0; i < pixelData.length; i++) { @@ -82,20 +71,28 @@ export function toSegmentIndexPixels(pixelData, remap?: LabelRemap) { continue; } - const segmentIndex = remap?.segmentIndexByLabel.get(labelValue) ?? labelValue; - segmentsOnLabelmap.add(segmentIndex); + let segmentIndex = labelValue; + if (labelToSegmentIndex) { + segmentIndex = labelToSegmentIndex[labelValue] ?? 0; + if (segmentIndex === 0) { + unboundLabels.add(labelValue); + } + } + if (segmentIndex !== 0) { + segmentsOnLabelmap.add(segmentIndex); + } if (segmentIndex !== labelValue) { if (segmentIndexPixels === pixelData) { - const SegmentIndexArray = - remap.maxSegmentIndex > 255 && pixelData.BYTES_PER_ELEMENT < 2 - ? Uint16Array - : pixelData.constructor; + const maxSegmentIndex = Math.max(0, ...Object.values(labelToSegmentIndex)); + const SegmentIndexArray = maxSegmentIndex > 255 ? Uint16Array : Uint8Array; segmentIndexPixels = new SegmentIndexArray(pixelData); } segmentIndexPixels[i] = segmentIndex; } } + unboundLabels.forEach(labelValue => onUnboundLabel?.(labelValue)); + return { pixelData: segmentIndexPixels, segmentsOnLabelmap: Array.from(segmentsOnLabelmap), From 279ba71ee70df29feb4b4af15ab40ab4f2374433 Mon Sep 17 00:00:00 2001 From: Thomas Forster Date: Thu, 15 Oct 2026 16:17:44 -0300 Subject: [PATCH 08/16] perf(segmentation): scan the labelmap voxels for the export option only after the segmentation changes --- .../src/utils/segmentationExportUtils.test.ts | 58 +++++++++++++++ .../src/utils/segmentationExportUtils.ts | 72 ++++++++++++++----- 2 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 extensions/cornerstone/src/utils/segmentationExportUtils.test.ts diff --git a/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts b/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts new file mode 100644 index 00000000000..26b9529f5bc --- /dev/null +++ b/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts @@ -0,0 +1,58 @@ +import { cache, eventTarget, metaData } from '@cornerstonejs/core'; +import { Enums as cstEnums, segmentation as cstSegmentation } from '@cornerstonejs/tools'; +import { hasExportableLabelMapData } from './segmentationExportUtils'; + +jest.mock('@cornerstonejs/core', () => { + const listeners = {}; + return { + cache: { getImage: jest.fn() }, + metaData: { get: jest.fn() }, + eventTarget: { + addEventListener: (type, listener) => { + (listeners[type] ??= []).push(listener); + }, + dispatchEvent: evt => (listeners[evt.type] ?? []).forEach(listener => listener(evt)), + }, + }; +}); + +jest.mock('@cornerstonejs/tools', () => ({ + Enums: { + Events: { + SEGMENTATION_DATA_MODIFIED: 'SEGMENTATION_DATA_MODIFIED', + SEGMENTATION_MODIFIED: 'SEGMENTATION_MODIFIED', + }, + }, + segmentation: { state: { getSegmentation: jest.fn() } }, +})); + +describe('hasExportableLabelMapData', () => { + const displaySetService = { getDisplaySetForSOPInstanceUID: () => ({}) }; + const labelmap = { imageIds: ['labelmap:1'], referencedImageIds: ['source:1'] }; + + beforeEach(() => { + (metaData.get as jest.Mock).mockReturnValue({ SOPInstanceUID: '1', SeriesInstanceUID: '2' }); + (cstSegmentation.state.getSegmentation as jest.Mock).mockReturnValue({ + representationData: { Labelmap: labelmap }, + }); + }); + + it('scans the voxels again only after the segmentation data changes', () => { + const getImage = cache.getImage as jest.Mock; + getImage.mockReturnValue({ getPixelData: () => new Uint8Array([0, 0]) }); + + expect(hasExportableLabelMapData(labelmap, displaySetService)).toBe(false); + expect(hasExportableLabelMapData(labelmap, displaySetService)).toBe(false); + expect(getImage).toHaveBeenCalledTimes(1); + + getImage.mockReturnValue({ getPixelData: () => new Uint8Array([0, 1]) }); + eventTarget.dispatchEvent( + Object.assign(new Event(cstEnums.Events.SEGMENTATION_DATA_MODIFIED), { + detail: { segmentationId: 'seg' }, + }) + ); + + expect(hasExportableLabelMapData(labelmap, displaySetService)).toBe(true); + expect(getImage).toHaveBeenCalledTimes(2); + }); +}); diff --git a/extensions/cornerstone/src/utils/segmentationExportUtils.ts b/extensions/cornerstone/src/utils/segmentationExportUtils.ts index 5d9153cadc3..bf6afa6c558 100644 --- a/extensions/cornerstone/src/utils/segmentationExportUtils.ts +++ b/extensions/cornerstone/src/utils/segmentationExportUtils.ts @@ -1,33 +1,41 @@ -import { cache, metaData } from '@cornerstonejs/core'; -import { Types as cstTypes } from '@cornerstonejs/tools'; +import { cache, eventTarget, metaData } from '@cornerstonejs/core'; +import { + Enums as cstEnums, + segmentation as cstSegmentation, + Types as cstTypes, +} from '@cornerstonejs/tools'; /** - * Checks if a labelmap segmentation has exportable data - * A label map is exportable if it has any pixel data and - * it is referenced by a display set that is reconstructable. - * - * @param labelmap - The labelmap representation data - * @param displaySetService - The display set service instance - * @returns boolean - Whether the labelmap has exportable data + * The result of the voxel scan of each labelmap, kept until the segmentation + * changes. The panel asks on each render, and a scan reads every voxel. */ -export const hasExportableLabelMapData = ( - labelmap: cstTypes.LabelmapSegmentationData | undefined, - displaySetService: any -): boolean => { - if (!labelmap) { - return false; +const hasPixelDataByLabelmap = new WeakMap(); +let isListeningForChanges = false; + +const listenForChanges = () => { + if (isListeningForChanges) { + return; } + isListeningForChanges = true; + const forget = (evt: CustomEvent<{ segmentationId: string }>) => { + const labelmap = cstSegmentation.state.getSegmentation(evt.detail?.segmentationId) + ?.representationData?.Labelmap; + if (labelmap) { + hasPixelDataByLabelmap.delete(labelmap); + } + }; + eventTarget.addEventListener(cstEnums.Events.SEGMENTATION_DATA_MODIFIED, forget as EventListener); + eventTarget.addEventListener(cstEnums.Events.SEGMENTATION_MODIFIED, forget as EventListener); +}; +const hasLabelmapPixelData = (labelmap: cstTypes.LabelmapSegmentationData): boolean => { // A segment can be in an overlap layer only, so check the images of every layer. const layers = Object.values(labelmap.labelmaps ?? {}); const imageIds = layers.length ? layers.flatMap(layer => layer.imageIds ?? []) : labelmap.imageIds; - if (!imageIds?.length) { - return false; - } - const hasPixelData = imageIds.some(imageId => { + return !!imageIds?.some(imageId => { const pixelData = cache.getImage(imageId)?.getPixelData(); if (!pixelData) { return false; @@ -38,7 +46,33 @@ export const hasExportableLabelMapData = ( return true; } } + return false; }); +}; + +/** + * Checks if a labelmap segmentation has exportable data + * A label map is exportable if it has any pixel data and + * it is referenced by a display set that is reconstructable. + * + * @param labelmap - The labelmap representation data + * @param displaySetService - The display set service instance + * @returns boolean - Whether the labelmap has exportable data + */ +export const hasExportableLabelMapData = ( + labelmap: cstTypes.LabelmapSegmentationData | undefined, + displaySetService: any +): boolean => { + if (!labelmap) { + return false; + } + + listenForChanges(); + let hasPixelData = hasPixelDataByLabelmap.get(labelmap); + if (hasPixelData === undefined) { + hasPixelData = hasLabelmapPixelData(labelmap); + hasPixelDataByLabelmap.set(labelmap, hasPixelData); + } if (!hasPixelData) { return false; From 7cac7fec2166d9233f30bc62588d319a87e0f446 Mon Sep 17 00:00:00 2001 From: Thomas Forster Date: Thu, 15 Oct 2026 16:17:44 -0300 Subject: [PATCH 09/16] test(seg): anchor the @ohif module mappers of the jest config --- extensions/cornerstone-dicom-seg/jest.config.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/cornerstone-dicom-seg/jest.config.js b/extensions/cornerstone-dicom-seg/jest.config.js index 129dcb6c58b..278e132ef76 100644 --- a/extensions/cornerstone-dicom-seg/jest.config.js +++ b/extensions/cornerstone-dicom-seg/jest.config.js @@ -4,6 +4,9 @@ module.exports = { ...base, moduleNameMapper: { ...base.moduleNameMapper, - '@ohif/(.*)': '/../../platform/$1/src', + // Deep imports already name `src`, so they must be matched before the + // catch-all below appends a second one. + '^@ohif/([^/]+)/src/(.*)$': '/../../platform/$1/src/$2', + '^@ohif/([^/]+)$': '/../../platform/$1/src', }, }; From b94669e81fd3fcc95b98cdaf9000cab61d1a9c98 Mon Sep 17 00:00:00 2001 From: Thomas Forster Date: Thu, 15 Oct 2026 16:17:44 -0300 Subject: [PATCH 10/16] test(seg): move the segment to an overlap layer with the cornerstone3D API, and remove the root dcmjs pin The spec uses segmentation.helpers.moveSegmentToPrivateLabelmap when cornerstone3D exports it, and paints across a locked segment only with a cornerstone3D release without it. dcmjs resolves to the hoisted copy that the workspace packages pin. --- package.json | 1 - pnpm-lock.yaml | 3 -- tests/LabelMapSegOverlapLayerSave.spec.ts | 51 +++++++++++++++++------ 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index defda76739b..e6ae7eceea4 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,6 @@ "css-loader": "6.11.0", "cypress": "14.5.2", "cypress-file-upload": "5.0.8", - "dcmjs": "0.52.0", "eslint": "10.7.0", "eslint-plugin-react-hooks": "7.1.1", "file-loader": "6.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 117d921783c..428311ee643 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -327,9 +327,6 @@ importers: cypress-file-upload: specifier: 5.0.8 version: 5.0.8(cypress@14.5.2) - dcmjs: - specifier: 0.52.0 - version: 0.52.0 eslint: specifier: 10.7.0 version: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) diff --git a/tests/LabelMapSegOverlapLayerSave.spec.ts b/tests/LabelMapSegOverlapLayerSave.spec.ts index 122e86be9a7..b80ccbf8bb6 100644 --- a/tests/LabelMapSegOverlapLayerSave.spec.ts +++ b/tests/LabelMapSegOverlapLayerSave.spec.ts @@ -1,18 +1,23 @@ import fs from 'node:fs'; +// dcmjs is not a root dependency: it resolves to the hoisted copy that the +// workspace packages pin. import dcmjs from 'dcmjs'; import type { Page } from '@playwright/test'; import { expect, test, visitStudy } from './utils'; /** - * Painting a segment across a locked segment puts the painted segment in a - * separate labelmap layer, where it is stored under another label value. The - * DICOM SEG save must still write each segment under its own segment number, - * label and color. + * A segment in a separate labelmap layer is stored under another label value + * (usually 1). The DICOM SEG save must still write each segment under its own + * segment number, label and color. * - * With cornerstonejs/cornerstone3D#2988 a lock stops the brush and creates no - * layer. After OHIF moves to that release, the test must create the overlap with - * the explicit option (for example `segmentation.overwriteMode: 'none'`). + * The test puts segment 2 in a separate layer with + * `segmentation.helpers.moveSegmentToPrivateLabelmap` + * (cornerstonejs/cornerstone3D#2988). The cornerstone3D release that OHIF uses + * now does not export that function, and its tools configuration (with + * `segmentation.overwriteMode`) is not available to the page. With that release + * the test paints segment 2 across a locked segment 1, which moves segment 2 to + * a separate layer. Remove that fallback after OHIF moves to a release with #2988. */ test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458'; @@ -22,6 +27,19 @@ test.beforeEach(async ({ page }) => { const asArray = value => (Array.isArray(value) ? value : [value]); +const hasMoveSegmentApi = (page: Page) => + page.evaluate( + () => !!(window as any).cornerstoneTools.segmentation.helpers?.moveSegmentToPrivateLabelmap + ); + +/** Moves a segment to a separate labelmap layer with the cornerstone3D API. */ +const moveSegmentToNewLayer = (page: Page, segmentIndex: number) => + page.evaluate(segmentIndex => { + const { segmentation } = (window as any).cornerstoneTools; + const [{ segmentationId }] = segmentation.state.getSegmentations(); + segmentation.helpers.moveSegmentToPrivateLabelmap(segmentationId, segmentIndex); + }, segmentIndex); + const getLayerCount = (page: Page) => page.evaluate(() => { const [segmentation] = (window as any).cornerstoneTools.segmentation.state.getSegmentations(); @@ -65,14 +83,19 @@ test('should save each segment of an overlap layer under its own segment number' // Segment 1: a horizontal stroke. await viewport.normalizedDragAt({ start: { x: 0.3, y: 0.5 }, end: { x: 0.7, y: 0.5 } }); - // Segment 2: a vertical stroke across segment 1, which is locked. + // Segment 2: a vertical stroke across segment 1, in a separate layer. await labelMapPanel.addSegmentButton.click(); await expect(labelMapPanel.panel.rows).toHaveCount(2); - await labelMapPanel.panel.nthSegment(0).actions.toggleLock(); + const canMoveSegment = await hasMoveSegmentApi(page); + const isSegment1Locked = !canMoveSegment; + if (isSegment1Locked) { + // Fallback for a cornerstone3D release without #2988 (see the comment at the top). + await labelMapPanel.panel.nthSegment(0).actions.toggleLock(); + } await viewport.normalizedDragAt({ start: { x: 0.5, y: 0.3 }, end: { x: 0.5, y: 0.7 } }); - - // The test covers the remap only when segment 2 is in a separate layer. This - // check fails when a lock stops the brush (see the comment at the top). + if (canMoveSegment) { + await moveSegmentToNewLayer(page, 2); + } expect(await getLayerCount(page)).toBeGreaterThan(1); // The save needs every source image of the series in the cache. @@ -104,7 +127,9 @@ test('should save each segment of an overlap layer under its own segment number' expect(framesPerSegment[2]).toBeGreaterThan(0); // Delete segment 1. The private layer of segment 2 is then the only layer. - await labelMapPanel.panel.nthSegment(0).actions.toggleLock(); + if (isSegment1Locked) { + await labelMapPanel.panel.nthSegment(0).actions.toggleLock(); + } await labelMapPanel.panel.nthSegment(0).actions.delete(); await expect(labelMapPanel.panel.rows).toHaveCount(1); expect(await getLayerCount(page)).toBe(1); From ef069aaa20b94d5cd85c8ec81f7536cb678fdfe7 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 15 Oct 2026 15:41:44 -0400 Subject: [PATCH 11/16] chore(deps): update cornerstone3D to 5.12.0 Co-Authored-By: Claude Opus 5.5 --- .../cornerstone-dicom-pmap/package.json | 4 +- extensions/cornerstone-dicom-seg/package.json | 4 +- extensions/cornerstone-dicom-sr/package.json | 6 +- .../cornerstone-dynamic-volume/package.json | 4 +- extensions/cornerstone/package.json | 16 +- extensions/measurement-tracking/package.json | 4 +- extensions/usAnnotation/package.json | 4 +- modes/usAnnotation/package.json | 4 +- platform/app/package.json | 2 +- platform/core/package.json | 6 +- pnpm-lock.yaml | 274 +++++++++--------- 11 files changed, 171 insertions(+), 157 deletions(-) diff --git a/extensions/cornerstone-dicom-pmap/package.json b/extensions/cornerstone-dicom-pmap/package.json index 36af6a5afd2..839da0eb931 100644 --- a/extensions/cornerstone-dicom-pmap/package.json +++ b/extensions/cornerstone-dicom-pmap/package.json @@ -40,8 +40,8 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/adapters": "5.10.3", - "@cornerstonejs/core": "5.10.3", + "@cornerstonejs/adapters": "5.12.0", + "@cornerstonejs/core": "5.12.0", "@kitware/vtk.js": "36.4.1" }, "devDependencies": { diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json index c0083d3dce4..960ab05f4d7 100644 --- a/extensions/cornerstone-dicom-seg/package.json +++ b/extensions/cornerstone-dicom-seg/package.json @@ -42,8 +42,8 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/adapters": "5.10.3", - "@cornerstonejs/core": "5.10.3", + "@cornerstonejs/adapters": "5.12.0", + "@cornerstonejs/core": "5.12.0", "@kitware/vtk.js": "36.4.1" }, "devDependencies": { diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index 5cdfab0f26b..caf492d4ae2 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -38,9 +38,9 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/adapters": "5.10.3", - "@cornerstonejs/core": "5.10.3", - "@cornerstonejs/tools": "5.10.3", + "@cornerstonejs/adapters": "5.12.0", + "@cornerstonejs/core": "5.12.0", + "@cornerstonejs/tools": "5.12.0", "classnames": "2.5.1" }, "devDependencies": { diff --git a/extensions/cornerstone-dynamic-volume/package.json b/extensions/cornerstone-dynamic-volume/package.json index 350539db6d1..aed0dba7940 100644 --- a/extensions/cornerstone-dynamic-volume/package.json +++ b/extensions/cornerstone-dynamic-volume/package.json @@ -43,8 +43,8 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/core": "5.10.3", - "@cornerstonejs/tools": "5.10.3", + "@cornerstonejs/core": "5.12.0", + "@cornerstonejs/tools": "5.12.0", "classnames": "2.5.1" }, "devDependencies": { diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 3946e73bc9e..cc6e34aa5f4 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -38,7 +38,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.4", "@cornerstonejs/codec-openjpeg": "1.3.2", "@cornerstonejs/codec-openjph": "2.4.9", - "@cornerstonejs/dicom-image-loader": "5.10.3", + "@cornerstonejs/dicom-image-loader": "5.12.0", "@ohif/core": "workspace:*", "@ohif/extension-default": "workspace:*", "dcmjs": "0.52.0", @@ -49,13 +49,13 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/adapters": "5.10.3", - "@cornerstonejs/ai": "5.10.3", - "@cornerstonejs/core": "5.10.3", - "@cornerstonejs/labelmap-interpolation": "5.10.3", - "@cornerstonejs/metadata": "5.10.3", - "@cornerstonejs/polymorphic-segmentation": "5.10.3", - "@cornerstonejs/tools": "5.10.3", + "@cornerstonejs/adapters": "5.12.0", + "@cornerstonejs/ai": "5.12.0", + "@cornerstonejs/core": "5.12.0", + "@cornerstonejs/labelmap-interpolation": "5.12.0", + "@cornerstonejs/metadata": "5.12.0", + "@cornerstonejs/polymorphic-segmentation": "5.12.0", + "@cornerstonejs/tools": "5.12.0", "@icr/polyseg-wasm": "0.4.0", "@itk-wasm/morphological-contour-interpolation": "1.1.0", "@kitware/vtk.js": "36.4.1", diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 9dfe3540237..cb0108cc346 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -27,8 +27,8 @@ "start": "pnpm run dev" }, "peerDependencies": { - "@cornerstonejs/core": "5.10.3", - "@cornerstonejs/tools": "5.10.3", + "@cornerstonejs/core": "5.12.0", + "@cornerstonejs/tools": "5.12.0", "@ohif/core": "workspace:*", "@ohif/extension-cornerstone-dicom-sr": "workspace:*", "@ohif/extension-default": "workspace:*", diff --git a/extensions/usAnnotation/package.json b/extensions/usAnnotation/package.json index 815d375f19b..8867c052599 100644 --- a/extensions/usAnnotation/package.json +++ b/extensions/usAnnotation/package.json @@ -35,8 +35,8 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/core": "5.10.3", - "@cornerstonejs/tools": "5.10.3", + "@cornerstonejs/core": "5.12.0", + "@cornerstonejs/tools": "5.12.0", "@ohif/core": "workspace:*", "@ohif/extension-cornerstone": "workspace:*", "@ohif/extension-default": "workspace:*", diff --git a/modes/usAnnotation/package.json b/modes/usAnnotation/package.json index 2ea44b71924..2cd7a9e4326 100644 --- a/modes/usAnnotation/package.json +++ b/modes/usAnnotation/package.json @@ -29,8 +29,8 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/core": "5.10.3", - "@cornerstonejs/tools": "5.10.3", + "@cornerstonejs/core": "5.12.0", + "@cornerstonejs/tools": "5.12.0", "@ohif/core": "workspace:*", "@ohif/extension-cornerstone-dicom-sr": "workspace:*", "@ohif/extension-ultrasound-pleura-bline": "workspace:*", diff --git a/platform/app/package.json b/platform/app/package.json index b72101ea0ba..4612055f05f 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -53,7 +53,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.4", "@cornerstonejs/codec-openjpeg": "1.3.2", "@cornerstonejs/codec-openjph": "2.4.9", - "@cornerstonejs/dicom-image-loader": "5.10.3", + "@cornerstonejs/dicom-image-loader": "5.12.0", "@emotion/serialize": "1.3.3", "@ohif/core": "workspace:*", "@ohif/i18n": "workspace:*", diff --git a/platform/core/package.json b/platform/core/package.json index e2e675c4339..29b2f1b4694 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -35,9 +35,9 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.4", "@cornerstonejs/codec-openjpeg": "1.3.2", "@cornerstonejs/codec-openjph": "2.4.9", - "@cornerstonejs/core": "5.10.3", - "@cornerstonejs/dicom-image-loader": "5.10.3", - "@cornerstonejs/metadata": "5.10.3", + "@cornerstonejs/core": "5.12.0", + "@cornerstonejs/dicom-image-loader": "5.12.0", + "@cornerstonejs/metadata": "5.12.0", "cornerstone-math": "0.1.10", "dicom-parser": "1.8.21" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 428311ee643..a96810e7ac5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -392,11 +392,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/adapters': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/tools@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/tools@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) '@cornerstonejs/ai': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/tools@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/tools@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) '@cornerstonejs/codec-charls': specifier: 1.2.5 version: 1.2.5 @@ -410,23 +410,23 @@ importers: specifier: 2.4.9 version: 2.4.9 '@cornerstonejs/core': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) '@cornerstonejs/dicom-image-loader': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.10.3)(dicom-parser@1.8.21) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.12.0)(dicom-parser@1.8.21) '@cornerstonejs/labelmap-interpolation': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/tools@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/tools@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) '@cornerstonejs/metadata': - specifier: 5.10.3 - version: 5.10.3 + specifier: 5.12.0 + version: 5.12.0 '@cornerstonejs/polymorphic-segmentation': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/tools@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0)) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/tools@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0)) '@cornerstonejs/tools': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@icr/polyseg-wasm': specifier: 0.4.0 version: 0.4.0 @@ -492,11 +492,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/adapters': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/tools@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/tools@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) '@cornerstonejs/core': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) '@kitware/vtk.js': specifier: 36.4.1 version: 36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) @@ -575,11 +575,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/adapters': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/tools@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/tools@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) '@cornerstonejs/core': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) '@kitware/vtk.js': specifier: 36.4.1 version: 36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) @@ -621,14 +621,14 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/adapters': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/tools@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/tools@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) '@cornerstonejs/core': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) '@cornerstonejs/tools': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@ohif/core': specifier: workspace:* version: link:../../platform/core @@ -661,11 +661,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/core': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) '@cornerstonejs/tools': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@ohif/core': specifier: workspace:* version: link:../../platform/core @@ -870,11 +870,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/core': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) '@cornerstonejs/tools': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@ohif/core': specifier: workspace:* version: link:../../platform/core @@ -975,11 +975,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/core': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) '@cornerstonejs/tools': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@ohif/core': specifier: workspace:* version: link:../../platform/core @@ -1416,11 +1416,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/core': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) '@cornerstonejs/tools': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@ohif/core': specifier: workspace:* version: link:../../platform/core @@ -1504,8 +1504,8 @@ importers: specifier: 2.4.9 version: 2.4.9 '@cornerstonejs/dicom-image-loader': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.10.3)(dicom-parser@1.8.21) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.12.0)(dicom-parser@1.8.21) '@emotion/serialize': specifier: 1.3.3 version: 1.3.3 @@ -1712,14 +1712,14 @@ importers: specifier: 2.4.9 version: 2.4.9 '@cornerstonejs/core': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) '@cornerstonejs/dicom-image-loader': - specifier: 5.10.3 - version: 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.10.3)(dicom-parser@1.8.21) + specifier: 5.12.0 + version: 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.12.0)(dicom-parser@1.8.21) '@cornerstonejs/metadata': - specifier: 5.10.3 - version: 5.10.3 + specifier: 5.12.0 + version: 5.12.0 cornerstone-math: specifier: 0.1.10 version: 0.1.10 @@ -3081,18 +3081,18 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@cornerstonejs/adapters@5.10.3': - resolution: {integrity: sha512-tsZiv45qNQCPOzNkLlo+RRLlZVsm5VV1s1jFrJddDOUvWCGx38WGc9Ou1axF+w3KkcMkfnnKivmhZ9YmtRmQRw==} + '@cornerstonejs/adapters@5.12.0': + resolution: {integrity: sha512-X+cwHxj8LYcaI1AA8mrPDTZzdgh6GZ6qDdrgkBEP4rQWrQ2n3uLWkql/oi6Rk+CqtDPMFAerio5qgD348wffaQ==} peerDependencies: - '@cornerstonejs/core': 5.10.3 - '@cornerstonejs/metadata': 5.10.3 - '@cornerstonejs/tools': 5.10.3 + '@cornerstonejs/core': 5.12.0 + '@cornerstonejs/metadata': 5.12.0 + '@cornerstonejs/tools': 5.12.0 - '@cornerstonejs/ai@5.10.3': - resolution: {integrity: sha512-P/OWKq/eE+YK5ftEg1JIPQFfeY3NVXa1mqsFaex1x0pUQCVueKpFS4BXpD88hkOdR+crdG5JaLvkN5QBvMV3MQ==} + '@cornerstonejs/ai@5.12.0': + resolution: {integrity: sha512-gLZUS347hWbETsbveXAFnQvA3knru4R15yF+Gqa+epuDSqqUht7BS9Mejn99I3RyTeMurY3MCIwmBjZmPaduMg==} peerDependencies: - '@cornerstonejs/core': 5.10.3 - '@cornerstonejs/tools': 5.10.3 + '@cornerstonejs/core': 5.12.0 + '@cornerstonejs/tools': 5.12.0 '@cornerstonejs/calculate-suv@1.0.3': resolution: {integrity: sha512-2SwVJKzC1DzyxdxJtCht9dhTND2GFjLwhhkDyyC7vJq5tIgbhxgPk1CSwovO1pxmoybAXzjOxnaubllxLgoT+w==} @@ -3106,52 +3106,71 @@ packages: resolution: {integrity: sha512-BoENE2cXfzQ2RMxPfMMJxyv7E78iErTCB8b8lod/qv3dsYfCmSOUQKiRsuOZJM24JrjCFnyzbkxhAnF8gAKQnQ==} engines: {node: '>=0.14'} + '@cornerstonejs/codec-charls@1.2.7': + resolution: {integrity: sha512-kNfQ7J8LIrfQUbt56fPJ0SEvevLlAAbsLHjqPS4DUDWVSuAHb5Fn0cMIV/U0SYkJRJqKvzyLJYF1hl7/xQGqLQ==} + engines: {node: '>=0.14'} + '@cornerstonejs/codec-libjpeg-turbo-8bit@1.2.4': resolution: {integrity: sha512-k5e2/EjE9gV7dkg2x3CzId6mp6P7tTya/AmnLzbN3fZTW6skTMrOTJfqowgGQBzX7styLdcsnjlqwEVqeqaaHQ==} engines: {node: '>=0.14'} + '@cornerstonejs/codec-libjpeg-turbo-8bit@1.2.7': + resolution: {integrity: sha512-TINxDtm+RQuMvWNyNR6z8lCkqDKguZBMrPJ3+oAvLc1XQ3tQsIGIIUWzveedpyr8gp7DYsQqoPLhCl2mYTSv3w==} + engines: {node: '>=0.14'} + + '@cornerstonejs/codec-libjxl@1.1.1': + resolution: {integrity: sha512-Gp5AXNDuqX9CggJ6VJzjEuPowL2IsKGkvUPo3I7ZmCaY1vNDTwOdJUFrHACl3RIMDm+EL9EUw0Lhsmbs5b0fvA==} + engines: {node: '>=24'} + '@cornerstonejs/codec-openjpeg@1.3.2': resolution: {integrity: sha512-IsvhGRZ/3LzoNQFyAsKKzPw/S769b9JrKhpufU8uEEalOBn+D1OLetzZ48pvpbwxR4J7ZFrnK0FBIYtl8Hdt9Q==} engines: {node: '>=0.14'} + '@cornerstonejs/codec-openjph@2.4.11': + resolution: {integrity: sha512-NzwCYevGSj2JOuP7BztxrOd5WnJ86gOGLv3sq36jZxxHUXoDo1YhzizHiYz+T7GQqkzbfaQcwSyxsRogzGFIPQ==} + engines: {node: '>=0.14'} + '@cornerstonejs/codec-openjph@2.4.9': resolution: {integrity: sha512-iUGvF8r4SMHFrHGjwhNsTlHDPm+fIcGw3fUYm4vzhsf2J6VR9DqHtynXwUVOCBMSG9YTyWkcsD4db++0kMffMw==} engines: {node: '>=0.14'} - '@cornerstonejs/core@5.10.3': - resolution: {integrity: sha512-Pu+EZwqpAHlDukek5GQv4EJNAOq2OzCHX2Fyw/9pAR4wlJ8AqyJZN5ggvl8eBdLhBK/IUGnF/WOUPZap9dY8YQ==} + '@cornerstonejs/core@5.12.0': + resolution: {integrity: sha512-tnjJYCcrmUdnjk2suFprcR4mkrfJZ3NOAHlml0vo23EqePHgDWNiVCbAkootJ9rGaaKBiCz99VWLUL1+sBbefw==} peerDependencies: - '@cornerstonejs/metadata': 5.10.3 - '@cornerstonejs/utils': 5.10.3 + '@cornerstonejs/metadata': 5.12.0 + '@cornerstonejs/utils': 5.12.0 - '@cornerstonejs/dicom-image-loader@5.10.3': - resolution: {integrity: sha512-X54YXP+OXRdzTP0kK3x2IZKk2c3fE2jcIk50jZzbr06crVURbw493yPI0VAwi4BI2XP8Suw3gFVxG6qvdOsivw==} + '@cornerstonejs/dicom-image-loader@5.12.0': + resolution: {integrity: sha512-UfBJoNSM+L4SzTZfhxOHRQUIuL0LO8SApeysrYsZdwwQWBomLzqiQ1BlLIwsIfpfb7h9gHOSYt2RKHqidVRmDQ==} peerDependencies: - '@cornerstonejs/core': 5.10.3 - '@cornerstonejs/metadata': 5.10.3 + '@cornerstonejs/core': 5.12.0 + '@cornerstonejs/metadata': 5.12.0 dicom-parser: 1.8.21 - '@cornerstonejs/labelmap-interpolation@5.10.3': - resolution: {integrity: sha512-fY1VHAXDIHEhWdI866d7D/qaZjZJucoD6oVvjyh0tYoMocImMtJqZ3ldaLJq9IkK2r37JBadNaRQji21VpCIgQ==} + '@cornerstonejs/jpeg-lossless-decoder-js@2.2.1': + resolution: {integrity: sha512-cDCv7dm3qw9kbxCydvj0UGbNQcsPmTwcyd9SQXkBt34CDgSBeb8Xvj0Gz9HlJ+7DIKOFMTazF1oDJSu0pcCKmg==} + + '@cornerstonejs/labelmap-interpolation@5.12.0': + resolution: {integrity: sha512-y3gjCxrG1OYay/mLQ6sv/29dKqlnny2EW55M03pS+tw4Q9yMcS6/jlHOopgSA0Y4/pzq+KWKZtikLygz8m1Sxw==} peerDependencies: - '@cornerstonejs/core': 5.10.3 - '@cornerstonejs/tools': 5.10.3 + '@cornerstonejs/core': 5.12.0 + '@cornerstonejs/tools': 5.12.0 '@kitware/vtk.js': 36.4.1 - '@cornerstonejs/metadata@5.10.3': - resolution: {integrity: sha512-+wgDUt5QDc0YldesFcp6weNma80Vg9Mjg8xIjR4659GZsSM1TFguGtyMd4tQPZaJrzM9vggjjFn4KHD9FQZF4g==} + '@cornerstonejs/metadata@5.12.0': + resolution: {integrity: sha512-0wucYfpVy5IFWxvVJ+yUpFI0Scjw3wMhAtM5jKy/XT+TPrNpEzlBVFSaHO2VR2xj8ZUjGnE/UFMQAkVWf/WATg==} - '@cornerstonejs/polymorphic-segmentation@5.10.3': - resolution: {integrity: sha512-+9TZsXmUE57u/fsc5AlJpHcSn9jP94QmzhuIVd3t6afQ+q9Fk04TCAb9xpTSc6CS0PECRJAi7SOBMWjZrydkpA==} + '@cornerstonejs/polymorphic-segmentation@5.12.0': + resolution: {integrity: sha512-ybJfIPGk1t7wAcSpyKYSsT6GCkpqDFltucC4liK8F25vTUPy0ZlRVnMS84wABIZj1jOFgl12grrc6m/gx3234Q==} peerDependencies: - '@cornerstonejs/core': 5.10.3 - '@cornerstonejs/tools': 5.10.3 + '@cornerstonejs/core': 5.12.0 + '@cornerstonejs/tools': 5.12.0 '@kitware/vtk.js': 36.4.1 - '@cornerstonejs/tools@5.10.3': - resolution: {integrity: sha512-ON1ZtAWMmrmkKldNTcwBBfPI9dItqaGQt9xcFw5mSUSpxow4z0I3Jb2tObV+RPLQkK5ultzNX2Lql+2nAV6dWA==} + '@cornerstonejs/tools@5.12.0': + resolution: {integrity: sha512-ZdQrd15m3pIahGjU6+gzukUl1Vc0PxXcxU52SvtPTN4i2z1pcDU0SbHQAAGmMieaEdUmZMl5w4mQax3/8Tmc/w==} peerDependencies: - '@cornerstonejs/core': 5.10.3 + '@cornerstonejs/core': 5.12.0 '@kitware/vtk.js': 36.4.1 '@types/d3-array': 3.2.1 '@types/d3-interpolate': 3.0.4 @@ -3159,8 +3178,8 @@ packages: d3-interpolate: 3.0.1 gl-matrix: 3.4.3 - '@cornerstonejs/utils@5.10.3': - resolution: {integrity: sha512-45rIXR3ynJrlmrykjG0ijn0qRA4MrXxv7es++e28dRZo0eZMjkKQLoYB1Ng41PFd5zJr5QpowDoquGGQ5h4DcA==} + '@cornerstonejs/utils@5.12.0': + resolution: {integrity: sha512-vJXYHLbBHV9uM/9391KQAJTYcUbgENFpQt2IvegVJglrBWdkthDZ1HPPq01V9hcFLmBbv4zH86Fmu6XxjmcS7g==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -5196,12 +5215,6 @@ packages: rollup: optional: true - '@rollup/rollup-linux-x64-gnu@4.13.0': - resolution: {integrity: sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==} - cpu: [x64] - os: [linux] - libc: [glibc] - '@rsbuild/core@2.1.6': resolution: {integrity: sha512-w2WxblstOgHnDElkqJZVO/jM/EqPaEhg7zqQhON3Xu3Mj9FlVOJx+SgimOtghFzxLt8x7atX4oMUtMTNSZGf0Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -9684,9 +9697,6 @@ packages: joi@17.13.7: resolution: {integrity: sha512-MF80Dm5Y2veNy8QWVx9Bj3ui4mo7+VPSPsR1M+oaHXV0Gx6zGX9a2F+OZG3Blby9tOlzU9Rs5FUimlEhbKtfnQ==} - jpeg-lossless-decoder-js@2.1.2: - resolution: {integrity: sha512-fYf/plymnuKwVw+s8gJ8O/BPw8y8OLaN11MBvEcT05kuy3m45o7BEC4J0JbxMNUYyO+MdK/I/jq0q1gkVYZm2Q==} - js-sha3@0.9.3: resolution: {integrity: sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==} @@ -15158,22 +15168,22 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@cornerstonejs/adapters@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/tools@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))': + '@cornerstonejs/adapters@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/tools@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))': dependencies: '@babel/runtime-corejs2': 7.26.10 - '@cornerstonejs/core': 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) - '@cornerstonejs/metadata': 5.10.3 - '@cornerstonejs/tools': 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + '@cornerstonejs/core': 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + '@cornerstonejs/metadata': 5.12.0 + '@cornerstonejs/tools': 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) buffer: 6.0.3 dcmjs: 0.52.0 gl-matrix: 3.4.3 ndarray: 1.0.19 - '@cornerstonejs/ai@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/tools@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))': + '@cornerstonejs/ai@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/tools@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))': dependencies: '@babel/runtime-corejs2': 7.26.10 - '@cornerstonejs/core': 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) - '@cornerstonejs/tools': 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + '@cornerstonejs/core': 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + '@cornerstonejs/tools': 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) buffer: 6.0.3 dcmjs: 0.52.0 gl-matrix: 3.4.3 @@ -15188,16 +15198,24 @@ snapshots: '@cornerstonejs/codec-charls@1.2.5': {} + '@cornerstonejs/codec-charls@1.2.7': {} + '@cornerstonejs/codec-libjpeg-turbo-8bit@1.2.4': {} + '@cornerstonejs/codec-libjpeg-turbo-8bit@1.2.7': {} + + '@cornerstonejs/codec-libjxl@1.1.1': {} + '@cornerstonejs/codec-openjpeg@1.3.2': {} + '@cornerstonejs/codec-openjph@2.4.11': {} + '@cornerstonejs/codec-openjph@2.4.9': {} - '@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0)': + '@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0)': dependencies: - '@cornerstonejs/metadata': 5.10.3 - '@cornerstonejs/utils': 5.10.3 + '@cornerstonejs/metadata': 5.12.0 + '@cornerstonejs/utils': 5.12.0 '@kitware/vtk.js': 36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) comlink: 4.4.2 gl-matrix: 3.4.3 @@ -15206,24 +15224,27 @@ snapshots: - autoprefixer - wslink - '@cornerstonejs/dicom-image-loader@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.10.3)(dicom-parser@1.8.21)': + '@cornerstonejs/dicom-image-loader@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/metadata@5.12.0)(dicom-parser@1.8.21)': dependencies: - '@cornerstonejs/codec-charls': 1.2.5 - '@cornerstonejs/codec-libjpeg-turbo-8bit': 1.2.4 + '@cornerstonejs/codec-charls': 1.2.7 + '@cornerstonejs/codec-libjpeg-turbo-8bit': 1.2.7 + '@cornerstonejs/codec-libjxl': 1.1.1 '@cornerstonejs/codec-openjpeg': 1.3.2 - '@cornerstonejs/codec-openjph': 2.4.9 - '@cornerstonejs/core': 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) - '@cornerstonejs/metadata': 5.10.3 + '@cornerstonejs/codec-openjph': 2.4.11 + '@cornerstonejs/core': 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + '@cornerstonejs/jpeg-lossless-decoder-js': 2.2.1 + '@cornerstonejs/metadata': 5.12.0 comlink: 4.4.2 dicom-parser: 1.8.21 - jpeg-lossless-decoder-js: 2.1.2 pako: 2.1.0 uuid: 9.0.1 - '@cornerstonejs/labelmap-interpolation@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/tools@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)': + '@cornerstonejs/jpeg-lossless-decoder-js@2.2.1': {} + + '@cornerstonejs/labelmap-interpolation@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/tools@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@cornerstonejs/core': 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) - '@cornerstonejs/tools': 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + '@cornerstonejs/core': 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + '@cornerstonejs/tools': 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@itk-wasm/morphological-contour-interpolation': 1.1.0(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) '@kitware/vtk.js': 36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) itk-wasm: 1.0.0-b.165(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) @@ -15231,23 +15252,23 @@ snapshots: - debug - supports-color - '@cornerstonejs/metadata@5.10.3': + '@cornerstonejs/metadata@5.12.0': dependencies: '@cornerstonejs/calculate-suv': 1.0.3 - '@cornerstonejs/utils': 5.10.3 + '@cornerstonejs/utils': 5.12.0 dcmjs: 0.52.0 gl-matrix: 3.4.3 - '@cornerstonejs/polymorphic-segmentation@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/tools@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))': + '@cornerstonejs/polymorphic-segmentation@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@cornerstonejs/tools@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))': dependencies: - '@cornerstonejs/core': 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) - '@cornerstonejs/tools': 5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + '@cornerstonejs/core': 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + '@cornerstonejs/tools': 5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@icr/polyseg-wasm': 0.4.0 '@kitware/vtk.js': 36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) - '@cornerstonejs/tools@5.10.3(@cornerstonejs/core@5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)': + '@cornerstonejs/tools@5.12.0(@cornerstonejs/core@5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@kitware/vtk.js@36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)': dependencies: - '@cornerstonejs/core': 5.10.3(@cornerstonejs/metadata@5.10.3)(@cornerstonejs/utils@5.10.3)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) + '@cornerstonejs/core': 5.12.0(@cornerstonejs/metadata@5.12.0)(@cornerstonejs/utils@5.12.0)(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) '@kitware/vtk.js': 36.4.1(autoprefixer@10.4.27(postcss@8.5.26))(wslink@2.5.0) '@types/d3-array': 3.2.1 '@types/d3-interpolate': 3.0.4 @@ -15259,7 +15280,7 @@ snapshots: gl-matrix: 3.4.3 lodash.get: 4.4.2 - '@cornerstonejs/utils@5.10.3': + '@cornerstonejs/utils@5.12.0': dependencies: dcmjs: 0.52.0 @@ -18264,9 +18285,6 @@ snapshots: optionalDependencies: rollup: 2.80.0 - '@rollup/rollup-linux-x64-gnu@4.13.0': - optional: true - '@rsbuild/core@2.1.6(core-js@3.45.1)': dependencies: '@rspack/core': 2.1.10(@swc/helpers@0.5.23) @@ -23499,10 +23517,6 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 - jpeg-lossless-decoder-js@2.1.2: - optionalDependencies: - '@rollup/rollup-linux-x64-gnu': 4.13.0 - js-sha3@0.9.3: {} js-tokens@4.0.0: {} From 040e3f2d3fd95a83f0ec9770ad8d86304c72d7ad Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 15 Oct 2026 15:50:38 -0400 Subject: [PATCH 12/16] fix(seg): export the label map of each layer with the SEG adapter, and map an unbound label value to no segment Co-Authored-By: Claude Opus 5.5 --- .../src/commandsModule.ts | 23 +++-- .../utils/labelmapLayerSegmentIndices.test.ts | 55 ++++------- .../src/utils/labelmapLayerSegmentIndices.ts | 99 +++++++++---------- 3 files changed, 81 insertions(+), 96 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 1844552baca..34a2b115743 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -12,8 +12,9 @@ import { BITMAP_SEG_SOP_CLASS_UID, } from './utils/segmentationConfig'; import { + getExportedLabelValues, + getExportLabelToSegmentIndex, getSingleLayerExportSource, - toSegmentIndexPixels, type LabelToSegmentIndex, } from './utils/labelmapLayerSegmentIndices'; @@ -116,8 +117,8 @@ const commandsModule = ({ const labelmapData = segmentation.representationData.Labelmap; // Build a labelmap3D (one labelmaps2D entry per source slice) from a list of - // derived labelmap image ids, with the label values mapped to segment indices. - // When `referencedImageIds` is supplied (the multi-layer/overlap path) each + // derived labelmap image ids. The voxels keep their label values, and the + // adapter maps them with `labelToSegmentIndex`. When `referencedImageIds` is supplied (the multi-layer/overlap path) each // frame is indexed by its source slice so the layers align to the same frames; // otherwise frames are sequential. const buildLabelmap3D = ( @@ -147,16 +148,21 @@ const commandsModule = ({ continue; } - const slice = toSegmentIndexPixels( - segImage.getPixelData(), + const pixelData = segImage.getPixelData(); + const segmentsOnLabelmap = getExportedLabelValues( + pixelData, labelToSegmentIndex, - labelValue => unboundLabels.add(labelValue) + unboundLabels ); + // A slice with only unbound label values would give an empty LABELMAP frame. + if (!segmentsOnLabelmap.length) { + continue; + } const { rows, columns } = segImage; labelmaps2D[frameIndex] = { - segmentsOnLabelmap: slice.segmentsOnLabelmap, - pixelData: slice.pixelData, + segmentsOnLabelmap, + pixelData, rows, columns, }; @@ -176,6 +182,7 @@ const commandsModule = ({ segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())), metadata, labelmaps2D, + labelToSegmentIndex: getExportLabelToSegmentIndex(labelToSegmentIndex, unboundLabels), }; }; diff --git a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts index 8adf556b27c..5d7b31597f4 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts @@ -1,50 +1,37 @@ -import { getSingleLayerExportSource, toSegmentIndexPixels } from './labelmapLayerSegmentIndices'; +import { + getExportedLabelValues, + getExportLabelToSegmentIndex, + getSingleLayerExportSource, +} from './labelmapLayerSegmentIndices'; // Segment 2 is in a private layer under label value 1. const privateLabelToSegmentIndex = { 1: 2 }; -describe('toSegmentIndexPixels', () => { - it('maps label values to segment indices, and keeps the cached pixels', () => { - const pixelData = new Uint8Array([0, 1, 1, 0]); - - const slice = toSegmentIndexPixels(pixelData, privateLabelToSegmentIndex); - - expect(Array.from(slice.pixelData)).toEqual([0, 2, 2, 0]); - expect(slice.segmentsOnLabelmap).toEqual([2]); - expect(Array.from(pixelData)).toEqual([0, 1, 1, 0]); - }); - - it('keeps the values when the layer has no label map', () => { - const slice = toSegmentIndexPixels(new Uint8Array([0, 1, 3])); - - expect(Array.from(slice.pixelData)).toEqual([0, 1, 3]); - expect(slice.segmentsOnLabelmap).toEqual([1, 3]); +describe('getExportedLabelValues', () => { + it('keeps every label value when the layer has no label map', () => { + expect(getExportedLabelValues(new Uint8Array([0, 1, 3, 3]))).toEqual([1, 3]); }); - it('does not export a label value that has no segment, and reports it once', () => { - const onUnboundLabel = jest.fn(); + it('does not export a label value that has no segment, and reports it', () => { + const unboundLabels = new Set(); - const slice = toSegmentIndexPixels( + const labelValues = getExportedLabelValues( new Uint8Array([1, 5, 5]), privateLabelToSegmentIndex, - onUnboundLabel + unboundLabels ); - expect(Array.from(slice.pixelData)).toEqual([2, 0, 0]); - expect(slice.segmentsOnLabelmap).toEqual([2]); - expect(onUnboundLabel.mock.calls).toEqual([[5]]); + expect(labelValues).toEqual([1]); + expect(Array.from(unboundLabels)).toEqual([5]); }); +}); - it('writes segment indices up to 255 from an Int8Array without overflow', () => { - const slice = toSegmentIndexPixels(new Int8Array([0, 1]), { 1: 200 }); - - expect(Array.from(slice.pixelData)).toEqual([0, 200]); - }); - - it('writes a segment index above 255', () => { - const slice = toSegmentIndexPixels(new Uint8Array([0, 1]), { 1: 300 }); - - expect(Array.from(slice.pixelData)).toEqual([0, 300]); +describe('getExportLabelToSegmentIndex', () => { + it('maps a label value that has no segment to 0, so the adapter does not export it', () => { + expect(getExportLabelToSegmentIndex(privateLabelToSegmentIndex, new Set([5]))).toEqual({ + 1: 2, + 5: 0, + }); }); }); diff --git a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts index ee336b95647..cff5a702782 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts @@ -31,70 +31,61 @@ export function getSingleLayerExportSource(labelmapData): { } /** - * Maps the label values of one labelmap slice to segment indices, and collects - * the segments on the slice, in one pass. + * Gets the label values of one labelmap slice that the SEG export includes, in + * one pass. The result is the `segmentsOnLabelmap` of a labelmap2D, which the + * SEG adapter reads as label values. * * cs3D stores a segment in a private overlap layer under a label value that is - * not its segment index (usually 1). Each layer keeps the map in - * `labelToSegmentIndex`. The SEG adapter reads each voxel value as a segment - * number, so the export must map the label values first. - * - * A label value that has no entry in the map belongs to no segment, so the SEG - * has no segment metadata for it. Such voxels are written as 0 (not exported), - * and `onUnboundLabel` is called for each such label value. Without a map, the - * label values are kept. - * - * The cached labelmap image does not change. The function copies the slice only - * when a voxel value changes. The copy is a `Uint8Array` when all segment - * indices are 255 or less, else a `Uint16Array`. - * - * cornerstonejs/cornerstone3D#2988 lets the SEG adapter take - * `labelmap3D.labelToSegmentIndex`. After OHIF moves to that release, the export - * can set that map on each labelmap3D, and this function can be removed. + * not its segment index (usually 1), and keeps the map in `labelToSegmentIndex` + * of each layer. A label value that has no entry in that map belongs to no + * segment: it is not returned, and it is added to `unboundLabels`. Without a + * map, every label value is its own segment index. * * @param pixelData - The voxel values of the labelmap slice. - * @param labelToSegmentIndex - The label map of the layer, or undefined to keep the values. - * @param onUnboundLabel - Called with each label value that has no entry in the map. + * @param labelToSegmentIndex - The label map of the layer, or undefined. + * @param unboundLabels - Gets each label value that has no entry in the map. */ -export function toSegmentIndexPixels( +export function getExportedLabelValues( pixelData, labelToSegmentIndex?: LabelToSegmentIndex, - onUnboundLabel?: (labelValue: number) => void -) { - const segmentsOnLabelmap = new Set(); - const unboundLabels = new Set(); - let segmentIndexPixels = pixelData; - + unboundLabels?: Set +): number[] { + const labelValues = new Set(); for (let i = 0; i < pixelData.length; i++) { const labelValue = pixelData[i]; - if (labelValue === 0) { - continue; - } - - let segmentIndex = labelValue; - if (labelToSegmentIndex) { - segmentIndex = labelToSegmentIndex[labelValue] ?? 0; - if (segmentIndex === 0) { - unboundLabels.add(labelValue); - } - } - if (segmentIndex !== 0) { - segmentsOnLabelmap.add(segmentIndex); - } - if (segmentIndex !== labelValue) { - if (segmentIndexPixels === pixelData) { - const maxSegmentIndex = Math.max(0, ...Object.values(labelToSegmentIndex)); - const SegmentIndexArray = maxSegmentIndex > 255 ? Uint16Array : Uint8Array; - segmentIndexPixels = new SegmentIndexArray(pixelData); - } - segmentIndexPixels[i] = segmentIndex; + if (labelValue !== 0) { + labelValues.add(labelValue); } } + if (!labelToSegmentIndex) { + return Array.from(labelValues); + } + return Array.from(labelValues).filter(labelValue => { + if (labelToSegmentIndex[labelValue]) { + return true; + } + unboundLabels?.add(labelValue); + return false; + }); +} - unboundLabels.forEach(labelValue => onUnboundLabel?.(labelValue)); - - return { - pixelData: segmentIndexPixels, - segmentsOnLabelmap: Array.from(segmentsOnLabelmap), - }; +/** + * Gets the `labelToSegmentIndex` of a labelmap3D for the SEG adapter. + * + * The adapter reads a label value that is not in the map as its own segment + * index. The export must not include a label value that has no segment, so the + * map gives segment index 0 (no segment) to each such label value. + */ +export function getExportLabelToSegmentIndex( + labelToSegmentIndex: LabelToSegmentIndex | undefined, + unboundLabels: Set +): LabelToSegmentIndex | undefined { + if (!labelToSegmentIndex) { + return undefined; + } + const exportMap = { ...labelToSegmentIndex }; + unboundLabels.forEach(labelValue => { + exportMap[labelValue] = 0; + }); + return exportMap; } From d3418b8ca31e27fea21f41337a8c335435d0ee1e Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 15 Oct 2026 15:50:39 -0400 Subject: [PATCH 13/16] fix(segmentation): do not keep the voxel scan result while a labelmap image is not in the cache Co-Authored-By: Claude Opus 5.5 --- .../src/utils/segmentationExportUtils.test.ts | 11 +++++++++++ .../src/utils/segmentationExportUtils.ts | 19 ++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts b/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts index 26b9529f5bc..3fba491c494 100644 --- a/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts +++ b/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts @@ -55,4 +55,15 @@ describe('hasExportableLabelMapData', () => { expect(hasExportableLabelMapData(labelmap, displaySetService)).toBe(true); expect(getImage).toHaveBeenCalledTimes(2); }); + + it('scans the voxels again while a labelmap image is not in the cache', () => { + const loadingLabelmap = { imageIds: ['labelmap:2'], referencedImageIds: ['source:1'] }; + const getImage = cache.getImage as jest.Mock; + getImage.mockReturnValue(undefined); + + expect(hasExportableLabelMapData(loadingLabelmap, displaySetService)).toBe(false); + + getImage.mockReturnValue({ getPixelData: () => new Uint8Array([0, 1]) }); + expect(hasExportableLabelMapData(loadingLabelmap, displaySetService)).toBe(true); + }); }); diff --git a/extensions/cornerstone/src/utils/segmentationExportUtils.ts b/extensions/cornerstone/src/utils/segmentationExportUtils.ts index bf6afa6c558..2f64f895f19 100644 --- a/extensions/cornerstone/src/utils/segmentationExportUtils.ts +++ b/extensions/cornerstone/src/utils/segmentationExportUtils.ts @@ -28,16 +28,25 @@ const listenForChanges = () => { eventTarget.addEventListener(cstEnums.Events.SEGMENTATION_MODIFIED, forget as EventListener); }; -const hasLabelmapPixelData = (labelmap: cstTypes.LabelmapSegmentationData): boolean => { +/** + * Scans the voxels of every layer of a labelmap. + * + * @returns true when a voxel is set, false when no voxel is set, and undefined + * when no voxel is set but an image is not in the cache yet. An undefined + * result must not be kept: no segmentation event follows when the image loads. + */ +const hasLabelmapPixelData = (labelmap: cstTypes.LabelmapSegmentationData): boolean | undefined => { // A segment can be in an overlap layer only, so check the images of every layer. const layers = Object.values(labelmap.labelmaps ?? {}); const imageIds = layers.length ? layers.flatMap(layer => layer.imageIds ?? []) : labelmap.imageIds; - return !!imageIds?.some(imageId => { + let isImageMissing = false; + const hasPixelData = !!imageIds?.some(imageId => { const pixelData = cache.getImage(imageId)?.getPixelData(); if (!pixelData) { + isImageMissing = true; return false; } @@ -48,6 +57,8 @@ const hasLabelmapPixelData = (labelmap: cstTypes.LabelmapSegmentationData): bool } return false; }); + + return hasPixelData || (isImageMissing ? undefined : false); }; /** @@ -71,7 +82,9 @@ export const hasExportableLabelMapData = ( let hasPixelData = hasPixelDataByLabelmap.get(labelmap); if (hasPixelData === undefined) { hasPixelData = hasLabelmapPixelData(labelmap); - hasPixelDataByLabelmap.set(labelmap, hasPixelData); + if (hasPixelData !== undefined) { + hasPixelDataByLabelmap.set(labelmap, hasPixelData); + } } if (!hasPixelData) { From 7f8896b17be44f5534aa2ed89cfa1f40518e3a55 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 15 Oct 2026 15:52:15 -0400 Subject: [PATCH 14/16] test(seg): move the segment to an overlap layer with the cornerstone3D API only Co-Authored-By: Claude Opus 5.5 --- tests/LabelMapSegOverlapLayerSave.spec.ts | 25 ++--------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/tests/LabelMapSegOverlapLayerSave.spec.ts b/tests/LabelMapSegOverlapLayerSave.spec.ts index b80ccbf8bb6..9ae0bbe6fa2 100644 --- a/tests/LabelMapSegOverlapLayerSave.spec.ts +++ b/tests/LabelMapSegOverlapLayerSave.spec.ts @@ -12,12 +12,7 @@ import { expect, test, visitStudy } from './utils'; * segment number, label and color. * * The test puts segment 2 in a separate layer with - * `segmentation.helpers.moveSegmentToPrivateLabelmap` - * (cornerstonejs/cornerstone3D#2988). The cornerstone3D release that OHIF uses - * now does not export that function, and its tools configuration (with - * `segmentation.overwriteMode`) is not available to the page. With that release - * the test paints segment 2 across a locked segment 1, which moves segment 2 to - * a separate layer. Remove that fallback after OHIF moves to a release with #2988. + * `segmentation.helpers.moveSegmentToPrivateLabelmap`. */ test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458'; @@ -27,11 +22,6 @@ test.beforeEach(async ({ page }) => { const asArray = value => (Array.isArray(value) ? value : [value]); -const hasMoveSegmentApi = (page: Page) => - page.evaluate( - () => !!(window as any).cornerstoneTools.segmentation.helpers?.moveSegmentToPrivateLabelmap - ); - /** Moves a segment to a separate labelmap layer with the cornerstone3D API. */ const moveSegmentToNewLayer = (page: Page, segmentIndex: number) => page.evaluate(segmentIndex => { @@ -86,16 +76,8 @@ test('should save each segment of an overlap layer under its own segment number' // Segment 2: a vertical stroke across segment 1, in a separate layer. await labelMapPanel.addSegmentButton.click(); await expect(labelMapPanel.panel.rows).toHaveCount(2); - const canMoveSegment = await hasMoveSegmentApi(page); - const isSegment1Locked = !canMoveSegment; - if (isSegment1Locked) { - // Fallback for a cornerstone3D release without #2988 (see the comment at the top). - await labelMapPanel.panel.nthSegment(0).actions.toggleLock(); - } await viewport.normalizedDragAt({ start: { x: 0.5, y: 0.3 }, end: { x: 0.5, y: 0.7 } }); - if (canMoveSegment) { - await moveSegmentToNewLayer(page, 2); - } + await moveSegmentToNewLayer(page, 2); expect(await getLayerCount(page)).toBeGreaterThan(1); // The save needs every source image of the series in the cache. @@ -127,9 +109,6 @@ test('should save each segment of an overlap layer under its own segment number' expect(framesPerSegment[2]).toBeGreaterThan(0); // Delete segment 1. The private layer of segment 2 is then the only layer. - if (isSegment1Locked) { - await labelMapPanel.panel.nthSegment(0).actions.toggleLock(); - } await labelMapPanel.panel.nthSegment(0).actions.delete(); await expect(labelMapPanel.panel.rows).toHaveCount(1); expect(await getLayerCount(page)).toBe(1); From a53fbc3bed2de300f5fd595ffe866871c94d59c6 Mon Sep 17 00:00:00 2001 From: Thomas Forster Date: Thu, 15 Oct 2026 18:14:23 -0300 Subject: [PATCH 15/16] fix(segmentation): scan the top-level image ids when the only labelmap layer has none --- .../src/utils/segmentationExportUtils.test.ts | 14 ++++++++++++++ .../src/utils/segmentationExportUtils.ts | 10 +++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts b/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts index 3fba491c494..89500440bbb 100644 --- a/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts +++ b/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts @@ -66,4 +66,18 @@ describe('hasExportableLabelMapData', () => { getImage.mockReturnValue({ getPixelData: () => new Uint8Array([0, 1]) }); expect(hasExportableLabelMapData(loadingLabelmap, displaySetService)).toBe(true); }); + + it('scans the top-level image ids when the only layer has none', () => { + const singleLayerLabelmap = { + labelmaps: { layer: { labelmapId: 'layer' } }, + imageIds: ['labelmap:3'], + referencedImageIds: ['source:1'], + }; + const getImage = cache.getImage as jest.Mock; + getImage.mockImplementation(imageId => + imageId === 'labelmap:3' ? { getPixelData: () => new Uint8Array([0, 2]) } : undefined + ); + + expect(hasExportableLabelMapData(singleLayerLabelmap, displaySetService)).toBe(true); + }); }); diff --git a/extensions/cornerstone/src/utils/segmentationExportUtils.ts b/extensions/cornerstone/src/utils/segmentationExportUtils.ts index 2f64f895f19..e0f4f8c63e8 100644 --- a/extensions/cornerstone/src/utils/segmentationExportUtils.ts +++ b/extensions/cornerstone/src/utils/segmentationExportUtils.ts @@ -37,10 +37,14 @@ const listenForChanges = () => { */ const hasLabelmapPixelData = (labelmap: cstTypes.LabelmapSegmentationData): boolean | undefined => { // A segment can be in an overlap layer only, so check the images of every layer. + // A single layer without image ids exports the top-level image ids, so scan those. const layers = Object.values(labelmap.labelmaps ?? {}); - const imageIds = layers.length - ? layers.flatMap(layer => layer.imageIds ?? []) - : labelmap.imageIds; + const imageIds = + layers.length > 1 + ? layers.flatMap(layer => layer.imageIds ?? []) + : layers[0]?.imageIds?.length + ? layers[0].imageIds + : labelmap.imageIds; let isImageMissing = false; const hasPixelData = !!imageIds?.some(imageId => { From 8db99c88db361fcc448a72ffb8da9fc7f457afa7 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 15 Oct 2026 17:34:44 -0400 Subject: [PATCH 16/16] refactor(segmentation): share the single-layer export source between the SEG export and the Save menu check Co-Authored-By: Claude Opus 5.5 --- .../src/commandsModule.ts | 16 +++++-- .../utils/labelmapLayerSegmentIndices.test.ts | 19 -------- .../src/utils/labelmapLayerSegmentIndices.ts | 29 ----------- extensions/cornerstone/src/utils/index.ts | 2 + .../src/utils/segmentationExportUtils.test.ts | 21 +++++++- .../src/utils/segmentationExportUtils.ts | 48 +++++++++++++++---- 6 files changed, 71 insertions(+), 64 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 34a2b115743..b147904ebf6 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -4,6 +4,7 @@ import { cache, metaData } from '@cornerstonejs/core'; import { segmentation as cornerstoneToolsSegmentation } from '@cornerstonejs/tools'; import { adaptersRT, adaptersSEG } from '@cornerstonejs/adapters'; import { createReportDialogPrompt, useUIStateStore } from '@ohif/extension-default'; +import { utils as cornerstoneUtils } from '@ohif/extension-cornerstone'; import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES'; import { @@ -14,7 +15,6 @@ import { import { getExportedLabelValues, getExportLabelToSegmentIndex, - getSingleLayerExportSource, type LabelToSegmentIndex, } from './utils/labelmapLayerSegmentIndices'; @@ -118,9 +118,9 @@ const commandsModule = ({ // Build a labelmap3D (one labelmaps2D entry per source slice) from a list of // derived labelmap image ids. The voxels keep their label values, and the - // adapter maps them with `labelToSegmentIndex`. When `referencedImageIds` is supplied (the multi-layer/overlap path) each - // frame is indexed by its source slice so the layers align to the same frames; - // otherwise frames are sequential. + // adapter maps them with `labelToSegmentIndex`. When `referencedImageIds` is + // supplied (the multi-layer/overlap path) each frame is indexed by its source + // slice so the layers align to the same frames; otherwise frames are sequential. const buildLabelmap3D = ( segImageIds: string[], metadata, @@ -267,7 +267,13 @@ const commandsModule = ({ ) ); } else { - const { imageIds, labelToSegmentIndex } = getSingleLayerExportSource(labelmapData); + const { imageIds, labelToSegmentIndex, layerWithoutImageIds } = + cornerstoneUtils.getSingleLayerExportSource(labelmapData); + if (layerWithoutImageIds) { + console.warn( + `Labelmap layer ${layerWithoutImageIds.labelmapId} has no image ids. The SEG export uses the segmentation image ids with the label map of that layer.` + ); + } const segImages = imageIds.map(imageId => cache.getImage(imageId)); referencedImages = segImages.map((image, sliceIndex) => resolveReferencedImage(image.referencedImageId, sliceIndex) diff --git a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts index 5d7b31597f4..2ca274a7f70 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.test.ts @@ -1,7 +1,6 @@ import { getExportedLabelValues, getExportLabelToSegmentIndex, - getSingleLayerExportSource, } from './labelmapLayerSegmentIndices'; // Segment 2 is in a private layer under label value 1. @@ -34,21 +33,3 @@ describe('getExportLabelToSegmentIndex', () => { }); }); }); - -describe('getSingleLayerExportSource', () => { - it('applies the label map of the only layer when the layer has no image ids', () => { - const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); - - const source = getSingleLayerExportSource({ - imageIds: ['labelmap:1'], - labelmaps: { - private: { labelmapId: 'private', volumeId: 'v', labelToSegmentIndex: { 1: 2 } }, - }, - }); - - expect(source.imageIds).toEqual(['labelmap:1']); - expect(source.labelToSegmentIndex).toEqual({ 1: 2 }); - expect(warn).toHaveBeenCalledTimes(1); - warn.mockRestore(); - }); -}); diff --git a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts index cff5a702782..4b50e91514e 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/labelmapLayerSegmentIndices.ts @@ -1,35 +1,6 @@ /** The map from a label value in the voxels of a labelmap layer to the segment index. */ export type LabelToSegmentIndex = { [labelValue: number]: number }; -type LabelmapLayer = { - labelmapId?: string; - imageIds?: string[]; - labelToSegmentIndex?: LabelToSegmentIndex; -}; - -/** - * Gets the image ids and the label map that the export of a single-layer - * segmentation uses. - * - * The only layer can be a private overlap layer, so its label map applies also - * when the layer has no image ids of its own and the legacy `imageIds` are used. - */ -export function getSingleLayerExportSource(labelmapData): { - imageIds: string[]; - labelToSegmentIndex?: LabelToSegmentIndex; -} { - const layer: LabelmapLayer | undefined = Object.values(labelmapData.labelmaps ?? {})[0]; - if (layer && !layer.imageIds) { - console.warn( - `Labelmap layer ${layer.labelmapId} has no image ids. The SEG export uses the segmentation image ids with the label map of that layer.` - ); - } - return { - imageIds: layer?.imageIds ?? labelmapData.imageIds, - labelToSegmentIndex: layer?.labelToSegmentIndex, - }; -} - /** * Gets the label values of one labelmap slice that the SEG export includes, in * one pass. The result is the `segmentsOnLabelmap` of a labelmap2D, which the diff --git a/extensions/cornerstone/src/utils/index.ts b/extensions/cornerstone/src/utils/index.ts index 467b6cb683c..428d836179f 100644 --- a/extensions/cornerstone/src/utils/index.ts +++ b/extensions/cornerstone/src/utils/index.ts @@ -11,6 +11,7 @@ import promptHydrationDialog, { } from './promptHydrationDialog'; import { getCenterExtent } from './getCenterExtent'; import { createSegmentationForViewport } from './createSegmentationForViewport'; +import { getSingleLayerExportSource } from './segmentationExportUtils'; const utils = { handleSegmentChange, @@ -20,6 +21,7 @@ const utils = { promptHydrationDialog, getCenterExtent, createSegmentationForViewport, + getSingleLayerExportSource, }; export type { HydrationDialogProps, HydrationCallback, HydrationSRResult }; diff --git a/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts b/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts index 89500440bbb..64193f79099 100644 --- a/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts +++ b/extensions/cornerstone/src/utils/segmentationExportUtils.test.ts @@ -1,6 +1,6 @@ import { cache, eventTarget, metaData } from '@cornerstonejs/core'; import { Enums as cstEnums, segmentation as cstSegmentation } from '@cornerstonejs/tools'; -import { hasExportableLabelMapData } from './segmentationExportUtils'; +import { getSingleLayerExportSource, hasExportableLabelMapData } from './segmentationExportUtils'; jest.mock('@cornerstonejs/core', () => { const listeners = {}; @@ -69,7 +69,7 @@ describe('hasExportableLabelMapData', () => { it('scans the top-level image ids when the only layer has none', () => { const singleLayerLabelmap = { - labelmaps: { layer: { labelmapId: 'layer' } }, + labelmaps: { layer: { labelmapId: 'layer', imageIds: [] } }, imageIds: ['labelmap:3'], referencedImageIds: ['source:1'], }; @@ -81,3 +81,20 @@ describe('hasExportableLabelMapData', () => { expect(hasExportableLabelMapData(singleLayerLabelmap, displaySetService)).toBe(true); }); }); + +describe('getSingleLayerExportSource', () => { + it('applies the label map of the only layer when the layer has no image ids', () => { + const layer = { labelmapId: 'private', labelToSegmentIndex: { 1: 2 } }; + + const source = getSingleLayerExportSource({ + imageIds: ['labelmap:1'], + labelmaps: { private: layer }, + } as never); + + expect(source).toEqual({ + imageIds: ['labelmap:1'], + labelToSegmentIndex: { 1: 2 }, + layerWithoutImageIds: layer, + }); + }); +}); diff --git a/extensions/cornerstone/src/utils/segmentationExportUtils.ts b/extensions/cornerstone/src/utils/segmentationExportUtils.ts index e0f4f8c63e8..05558a98abb 100644 --- a/extensions/cornerstone/src/utils/segmentationExportUtils.ts +++ b/extensions/cornerstone/src/utils/segmentationExportUtils.ts @@ -28,6 +28,43 @@ const listenForChanges = () => { eventTarget.addEventListener(cstEnums.Events.SEGMENTATION_MODIFIED, forget as EventListener); }; +type LabelmapLayer = NonNullable[string]; + +/** + * Gets the image ids and the label map that the SEG export of a single-layer + * labelmap uses. + * + * The only layer can be a private overlap layer, so its label map applies also + * when the layer has no image ids of its own (missing or empty) and the + * top-level `imageIds` are used. + * + * @returns `layerWithoutImageIds` is the layer when its image ids are not used. + */ +export function getSingleLayerExportSource(labelmap: cstTypes.LabelmapSegmentationData): { + imageIds: string[]; + labelToSegmentIndex?: LabelmapLayer['labelToSegmentIndex']; + layerWithoutImageIds?: LabelmapLayer; +} { + const layer: LabelmapLayer | undefined = Object.values(labelmap.labelmaps ?? {})[0]; + if (layer?.imageIds?.length) { + return { imageIds: layer.imageIds, labelToSegmentIndex: layer.labelToSegmentIndex }; + } + return { + imageIds: (labelmap as { imageIds?: string[] }).imageIds ?? [], + labelToSegmentIndex: layer?.labelToSegmentIndex, + layerWithoutImageIds: layer, + }; +} + +/** Gets the labelmap image ids of every layer that the SEG export uses. */ +const getExportImageIds = (labelmap: cstTypes.LabelmapSegmentationData): string[] => { + const layers = Object.values(labelmap.labelmaps ?? {}); + if (layers.length > 1) { + return layers.flatMap(layer => layer.imageIds ?? []); + } + return getSingleLayerExportSource(labelmap).imageIds; +}; + /** * Scans the voxels of every layer of a labelmap. * @@ -37,17 +74,10 @@ const listenForChanges = () => { */ const hasLabelmapPixelData = (labelmap: cstTypes.LabelmapSegmentationData): boolean | undefined => { // A segment can be in an overlap layer only, so check the images of every layer. - // A single layer without image ids exports the top-level image ids, so scan those. - const layers = Object.values(labelmap.labelmaps ?? {}); - const imageIds = - layers.length > 1 - ? layers.flatMap(layer => layer.imageIds ?? []) - : layers[0]?.imageIds?.length - ? layers[0].imageIds - : labelmap.imageIds; + const imageIds = getExportImageIds(labelmap); let isImageMissing = false; - const hasPixelData = !!imageIds?.some(imageId => { + const hasPixelData = imageIds.some(imageId => { const pixelData = cache.getImage(imageId)?.getPixelData(); if (!pixelData) { isImageMissing = true;