From 31134192806c9fa5d095ff280c7b56cc08554490 Mon Sep 17 00:00:00 2001 From: Pedro Kohler Date: Wed, 12 Feb 2025 08:32:39 -0300 Subject: [PATCH] feat: make stack viewport work --- .../examples/segmentationStack/index.ts | 20 +- .../examples/segmentationVolume/index.ts | 22 +-- .../examples/segmentationVolume/utils.ts | 69 ++++--- .../segmentation/imageChangeEventListener.ts | 187 +++++++++--------- .../onLabelmapSegmentationDataModified.ts | 34 ++-- .../segmentation/SegmentationStateManager.ts | 84 ++++++++ .../getCurrentLabelmapImageIdForViewport.ts | 11 ++ .../helpers/getSegmentationActor.ts | 51 ++++- .../segmentation/helpers/index.ts | 2 + .../Labelmap/addLabelmapToElement.ts | 24 +-- .../displayTools/Labelmap/labelmapDisplay.ts | 31 +-- .../getSegmentIndexAtWorldPoint.ts | 4 +- 12 files changed, 352 insertions(+), 187 deletions(-) diff --git a/packages/adapters/examples/segmentationStack/index.ts b/packages/adapters/examples/segmentationStack/index.ts index 8e3dc279d2..297150d9b5 100644 --- a/packages/adapters/examples/segmentationStack/index.ts +++ b/packages/adapters/examples/segmentationStack/index.ts @@ -23,7 +23,6 @@ console.warn( "Click on index.ts to open source code for this example --------->" ); -const { Enums: csEnums, RenderingEngine, utilities: csUtilities } = cornerstone; const { segmentation: csToolsSegmentation } = cornerstoneTools; import { readDicom, @@ -32,14 +31,11 @@ import { loadSegmentation, exportSegmentation, restart, - getSegmentationIds, handleFileSelect, handleDragOver, - createSegmentation + createEmptySegmentation } from "../segmentationVolume/utils"; -const referenceImageIds: string[] = []; -const segImageIds: string[] = []; // ======== Set up page ======== // setTitleAndDescription( @@ -104,7 +100,7 @@ const state = { toolGroup: null, toolGroupId: "MY_TOOL_GROUP_ID", viewportIds: ["CT_AXIAL"], - segmentationIds: [], + segmentationId: "LOAD_SEG_ID:" + cornerstone.utilities.uuidv4(), referenceImageIds: [], segImageIds: [], skipOverlapping: false, @@ -135,12 +131,10 @@ function loadDicom() { } function createSegmentationRepresentation() { - for (const segmentationId of state.segmentationIds) { - csToolsSegmentation.addLabelmapRepresentationToViewport( - state.viewportIds[0], - [{ segmentationId }] - ); - } + csToolsSegmentation.addLabelmapRepresentationToViewport( + state.viewportIds[0], + [{ segmentationId: state.segmentationId }] + ); } // ============================= // @@ -196,7 +190,7 @@ addButtonToToolbar({ id: "CREATE_SEGMENTATION", title: "Create Empty SEG", onClick: async () => { - await createSegmentation(state); + await createEmptySegmentation(state); createSegmentationRepresentation(); }, container: group2 diff --git a/packages/adapters/examples/segmentationVolume/index.ts b/packages/adapters/examples/segmentationVolume/index.ts index 88a2a328de..b319a57c01 100644 --- a/packages/adapters/examples/segmentationVolume/index.ts +++ b/packages/adapters/examples/segmentationVolume/index.ts @@ -33,7 +33,7 @@ import { handleFileSelect, handleDragOver, restart, - createSegmentation + createEmptySegmentation } from "../segmentationVolume/utils"; setTitleAndDescription( @@ -116,7 +116,7 @@ const state = { toolGroupId: "MY_TOOL_GROUP_ID", viewportIds: ["CT_AXIAL", "CT_SAGITTAL", "CT_CORONAL"], volumeId: "", - segmentationIds: [], + segmentationId: "LOAD_SEG_ID:" + cornerstone.utilities.uuidv4(), referenceImageIds: [], skipOverlapping: false, segImageIds: [], @@ -161,15 +161,13 @@ async function loadDicom() { } function createSegmentationRepresentation() { - for (const segmentationId of state.segmentationIds) { - const segMap = { - [state.viewportIds[0]]: [{ segmentationId }], - [state.viewportIds[1]]: [{ segmentationId }], - [state.viewportIds[2]]: [{ segmentationId }] - }; - - csToolsSegmentation.addLabelmapRepresentationToViewportMap(segMap); - } + const segMap = { + [state.viewportIds[0]]: [{ segmentationId: state.segmentationId }], + [state.viewportIds[1]]: [{ segmentationId: state.segmentationId }], + [state.viewportIds[2]]: [{ segmentationId: state.segmentationId }] + }; + + csToolsSegmentation.addLabelmapRepresentationToViewportMap(segMap); } // ============================= // addButtonToToolbar({ @@ -223,7 +221,7 @@ addButtonToToolbar({ id: "CREATE_SEGMENTATION", title: "Create Empty SEG", onClick: async () => { - await createSegmentation(state); + await createEmptySegmentation(state); createSegmentationRepresentation(); }, container: group2 diff --git a/packages/adapters/examples/segmentationVolume/utils.ts b/packages/adapters/examples/segmentationVolume/utils.ts index 1775ab948c..ba8729031c 100644 --- a/packages/adapters/examples/segmentationVolume/utils.ts +++ b/packages/adapters/examples/segmentationVolume/utils.ts @@ -18,7 +18,7 @@ export async function readDicom(files: FileList, state) { } } -export async function createSegmentation(state) { +export async function createEmptySegmentation(state) { const { referenceImageIds, segmentationId } = state; const derivedSegmentationImages = @@ -44,6 +44,25 @@ export async function createSegmentation(state) { ]); } +export async function createSegmentation({ state, labelMapImages }) { + const { segmentationId } = state; + + const imageIds = labelMapImages?.flat().map(image => image.imageId); + + csToolsSegmentation.addSegmentations([ + { + segmentationId, + representation: { + type: cornerstoneTools.Enums.SegmentationRepresentations + .Labelmap, + data: { + imageIds + } + } + } + ]); +} + export async function readSegmentation(file: File, state) { const imageId = wadouri.fileManager.add(file); const image = await imageLoader.loadAndCacheImage(imageId); @@ -65,9 +84,9 @@ export async function readSegmentation(file: File, state) { } export async function loadSegmentation(arrayBuffer: ArrayBuffer, state) { - const { referenceImageIds, skipOverlapping, segmentationIds } = state; + const { referenceImageIds, skipOverlapping, segmentationId } = state; - const generateToolState = + const { labelMapImages } = await Cornerstone3D.Segmentation.createFromDICOMSegBuffer( referenceImageIds, arrayBuffer, @@ -77,29 +96,27 @@ export async function loadSegmentation(arrayBuffer: ArrayBuffer, state) { } ); - for (let i = 0; i < generateToolState.labelMapImages.length; i++) { - const segmentationId = "LOAD_SEG_ID:" + cornerstone.utilities.uuidv4(); - segmentationIds.push(segmentationId); - await createSegmentation({ ...state, segmentationId }); - - const segmentation = - csToolsSegmentation.state.getSegmentation(segmentationId); - - const { imageIds } = segmentation.representationData.Labelmap; - const derivedSegmentationImages = imageIds.map(imageId => - cache.getImage(imageId) - ); - - const labelmapImagesNonOverlapping = - generateToolState.labelMapImages[i]; - - for (let j = 0; j < derivedSegmentationImages.length; j++) { - const voxelManager = derivedSegmentationImages[j].voxelManager; - const scalarData = voxelManager.getScalarData(); - scalarData.set(labelmapImagesNonOverlapping[j].getPixelData()); - voxelManager.setScalarData(scalarData); - } - } + await createSegmentation({ state, labelMapImages }); + // for (let i = 0; i < labelMapImages.length; i++) { + // const segmentation = + // csToolsSegmentation.state.getSegmentation(segmentationId); + + // const { imageIds } = segmentation.representationData.Labelmap; + // console.log("🚀 ~ loadSegmentation ~ imageIds:", imageIds); + // const derivedSegmentationImages = imageIds.map(imageId => + // cache.getImage(imageId) + // ); + + // const labelmapImagesNonOverlapping = labelMapImages[i]; + + // for (let j = 0; j < derivedSegmentationImages.length; j++) { + // const voxelManager = derivedSegmentationImages[j].voxelManager; + // const scalarData = voxelManager.getScalarData(); + // const derivedImage = labelmapImagesNonOverlapping[j]; + // scalarData.set(labelmapImagesNonOverlapping[j].getPixelData()); + // voxelManager.setScalarData(scalarData); + // } + // } } export async function exportSegmentation(state) { diff --git a/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts b/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts index 20f0ac5709..2dea22da6e 100644 --- a/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts +++ b/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts @@ -11,9 +11,9 @@ import { } from '@cornerstonejs/core'; import { triggerSegmentationRender } from '../../stateManagement/segmentation/SegmentationRenderingEngine'; import { updateLabelmapSegmentationImageReferences } from '../../stateManagement/segmentation/updateLabelmapSegmentationImageReferences'; -import { getCurrentLabelmapImageIdForViewport } from '../../stateManagement/segmentation/getCurrentLabelmapImageIdForViewport'; +import { getCurrentLabelmapImageIdForViewportOverlapping } from '../../stateManagement/segmentation/getCurrentLabelmapImageIdForViewport'; import { SegmentationRepresentations } from '../../enums'; -import { getLabelmapActorEntry } from '../../stateManagement/segmentation/helpers/getSegmentationActor'; +import { getLabelmapActorEntries } from '../../stateManagement/segmentation/helpers/getSegmentationActor'; import { getSegmentationRepresentations } from '../../stateManagement/segmentation/getSegmentationRepresentation'; const enable = function (element: HTMLDivElement): void { @@ -97,8 +97,8 @@ function _imageChangeEventListener(evt) { }); const labelmapActors = labelmapRepresentations - .map((representation) => { - return getLabelmapActorEntry(viewportId, representation.segmentationId); + .flatMap((representation) => { + return getLabelmapActorEntries(viewportId, representation.segmentationId); }) .filter((actor) => actor !== undefined); @@ -114,12 +114,12 @@ function _imageChangeEventListener(evt) { // if cannot find a representation for this actor means it has stuck around // form previous renderings and should be removed const validActor = labelmapRepresentations.find((representation) => { - const derivedImageId = getCurrentLabelmapImageIdForViewport( + const derivedImageIds = getCurrentLabelmapImageIdForViewportOverlapping( viewportId, representation.segmentationId ); - return derivedImageId === actor.referencedId; + return derivedImageIds.includes(actor.referencedId); }); if (!validActor) { @@ -130,105 +130,108 @@ function _imageChangeEventListener(evt) { labelmapRepresentations.forEach((representation) => { const { segmentationId } = representation; const currentImageId = viewport.getCurrentImageId(); - const derivedImageId = getCurrentLabelmapImageIdForViewport( + const derivedImageIds = getCurrentLabelmapImageIdForViewportOverlapping( viewportId, segmentationId ); - if (!derivedImageId) { + if (!derivedImageIds) { return; } - const derivedImage = cache.getImage(derivedImageId); - if (!derivedImage) { - console.warn( - 'No derived image found in the cache for segmentation representation', - representation - ); - return; - } + derivedImageIds.forEach((derivedImageId) => { + const derivedImage = cache.getImage(derivedImageId); - // re-use the old labelmap actor for the new image labelmap for speed and memory - const segmentationActorInput = actors.find( - (actor) => actor.referencedId === derivedImageId - ); + if (!derivedImage) { + console.warn( + 'No derived image found in the cache for segmentation representation', + representation + ); + return; + } - if (!segmentationActorInput) { - // i guess we need to create here - const { dimensions, spacing, direction } = - viewport.getImageDataMetadata(derivedImage); - - const currentImage = - cache.getImage(currentImageId) || - ({ - imageId: currentImageId, - } as Types.IImage); - - const { origin: currentOrigin } = - viewport.getImageDataMetadata(currentImage); - - // IMPORTANT: We need to make sure that the origin of the segmentation - // is the same as the current image origin. This is because due to some - // floating point precision issues, when coming from volume to stack - // the origin of the segmentation can be slightly different from the - // current image origin. This can cause the segmentation to be rendered - // in the wrong location. - // Todo: This will not work for segmentations that are not in the same frame - // of reference or derived from the same image. This can happen when we have - // a segmentation that happens to exist in the same space as the image but is - // not derived from it. We need to find a way to handle this case, but don't think - // it makes sense to do it for the stack viewport, as the volume viewport is designed to handle this case. - const originToUse = currentOrigin; - const constructor = derivedImage.voxelManager.getConstructor(); - const newPixelData = derivedImage.voxelManager.getScalarData(); - - const scalarArray = vtkDataArray.newInstance({ - name: 'Pixels', - numberOfComponents: 1, - // @ts-expect-error - values: new constructor(newPixelData), - }); - - const imageData = vtkImageData.newInstance(); - - imageData.setDimensions(dimensions[0], dimensions[1], 1); - imageData.setSpacing(spacing); - imageData.setDirection(direction); - imageData.setOrigin(originToUse); - imageData.getPointData().setScalars(scalarArray); - imageData.modified(); - - viewport.addImages([ - { - imageId: derivedImageId, - representationUID: `${segmentationId}-${SegmentationRepresentations.Labelmap}`, - callback: ({ imageActor }) => { - imageActor.getMapper().setInputData(imageData); + // re-use the old labelmap actor for the new image labelmap for speed and memory + const segmentationActorInput = actors.find( + (actor) => actor.referencedId === derivedImageId + ); + + if (!segmentationActorInput) { + // i guess we need to create here + const { dimensions, spacing, direction } = + viewport.getImageDataMetadata(derivedImage); + + const currentImage = + cache.getImage(currentImageId) || + ({ + imageId: currentImageId, + } as Types.IImage); + + const { origin: currentOrigin } = + viewport.getImageDataMetadata(currentImage); + + // IMPORTANT: We need to make sure that the origin of the segmentation + // is the same as the current image origin. This is because due to some + // floating point precision issues, when coming from volume to stack + // the origin of the segmentation can be slightly different from the + // current image origin. This can cause the segmentation to be rendered + // in the wrong location. + // Todo: This will not work for segmentations that are not in the same frame + // of reference or derived from the same image. This can happen when we have + // a segmentation that happens to exist in the same space as the image but is + // not derived from it. We need to find a way to handle this case, but don't think + // it makes sense to do it for the stack viewport, as the volume viewport is designed to handle this case. + const originToUse = currentOrigin; + const constructor = derivedImage.voxelManager.getConstructor(); + const newPixelData = derivedImage.voxelManager.getScalarData(); + + const scalarArray = vtkDataArray.newInstance({ + name: 'Pixels', + numberOfComponents: 1, + // @ts-expect-error + values: new constructor(newPixelData), + }); + + const imageData = vtkImageData.newInstance(); + + imageData.setDimensions(dimensions[0], dimensions[1], 1); + imageData.setSpacing(spacing); + imageData.setDirection(direction); + imageData.setOrigin(originToUse); + imageData.getPointData().setScalars(scalarArray); + imageData.modified(); + + viewport.addImages([ + { + imageId: derivedImageId, + representationUID: `${segmentationId}-${SegmentationRepresentations.Labelmap}-${derivedImage.imageId}`, + callback: ({ imageActor }) => { + imageActor.getMapper().setInputData(imageData); + }, }, - }, - ]); + ]); - triggerSegmentationRender(viewportId); - return; - } else { - // if actor found - // update the image data - - const segmentationImageData = segmentationActorInput.actor - .getMapper() - .getInputData(); - - if (segmentationImageData.setDerivedImage) { - // Update the derived image data, whether vtk or other as appropriate - // to the actor(s) displaying the data. - segmentationImageData.setDerivedImage(derivedImage); + triggerSegmentationRender(viewportId); + return; } else { - utilities.updateVTKImageDataWithCornerstoneImage( - segmentationImageData, - derivedImage - ); + // if actor found + // update the image data + + const segmentationImageData = segmentationActorInput.actor + .getMapper() + .getInputData(); + + if (segmentationImageData.setDerivedImage) { + // Update the derived image data, whether vtk or other as appropriate + // to the actor(s) displaying the data. + segmentationImageData.setDerivedImage(derivedImage); + } else { + utilities.updateVTKImageDataWithCornerstoneImage( + segmentationImageData, + derivedImage + ); + } } - } + }); viewport.render(); diff --git a/packages/tools/src/eventListeners/segmentation/labelmap/onLabelmapSegmentationDataModified.ts b/packages/tools/src/eventListeners/segmentation/labelmap/onLabelmapSegmentationDataModified.ts index e3219d5644..2bd64856ce 100644 --- a/packages/tools/src/eventListeners/segmentation/labelmap/onLabelmapSegmentationDataModified.ts +++ b/packages/tools/src/eventListeners/segmentation/labelmap/onLabelmapSegmentationDataModified.ts @@ -10,7 +10,7 @@ import * as SegmentationState from '../../../stateManagement/segmentation/segmen import type { SegmentationDataModifiedEventType } from '../../../types/EventTypes'; import type { LabelmapSegmentationDataVolume } from '../../../types/LabelmapTypes'; import { SegmentationRepresentations } from '../../../enums'; -import { getLabelmapActorEntry } from '../../../stateManagement/segmentation/helpers/getSegmentationActor'; +import { getLabelmapActorEntries } from '../../../stateManagement/segmentation/helpers/getSegmentationActor'; /** A callback function that is called when the segmentation data is modified which * often is as a result of tool interactions e.g., scissors, eraser, etc. @@ -137,28 +137,30 @@ function performStackLabelmapUpdate({ viewportIds, segmentationId }) { return; } - const actorEntry = getLabelmapActorEntry(viewportId, segmentationId); + const actorEntries = getLabelmapActorEntries(viewportId, segmentationId); - if (!actorEntry) { + if (!actorEntries) { return; } - const segImageData = actorEntry.actor.getMapper().getInputData(); + actorEntries.forEach((actorEntry) => { + const segImageData = actorEntry.actor.getMapper().getInputData(); - const currentSegmentationImageId = - SegmentationState.getCurrentLabelmapImageIdForViewport( - viewportId, - segmentationId - ); + const currentSegmentationImageId = + SegmentationState.getCurrentLabelmapImageIdForViewport( + viewportId, + segmentationId + ); - const segmentationImage = cache.getImage(currentSegmentationImageId); - segImageData.modified(); + const segmentationImage = cache.getImage(currentSegmentationImageId); + segImageData.modified(); - // update the cache with the new image data - csUtils.updateVTKImageDataWithCornerstoneImage( - segImageData, - segmentationImage - ); + // update the cache with the new image data + csUtils.updateVTKImageDataWithCornerstoneImage( + segImageData, + segmentationImage + ); + }); }); }); } diff --git a/packages/tools/src/stateManagement/segmentation/SegmentationStateManager.ts b/packages/tools/src/stateManagement/segmentation/SegmentationStateManager.ts index ff10b3133d..974688ce64 100644 --- a/packages/tools/src/stateManagement/segmentation/SegmentationStateManager.ts +++ b/packages/tools/src/stateManagement/segmentation/SegmentationStateManager.ts @@ -57,6 +57,12 @@ export default class SegmentationStateManager { Map >(); + /** + * A map of segmentationId-imageId to the labelmapImageIds that supports segment overlapping + * meaning that it supports a list of labelmapImageIds for each segmentationId-imageId pair + */ + private _labelmapImageIdReferenceMap = new Map(); + /** * Creates an instance of SegmentationStateManager. * @param {string} [uid] - Optional unique identifier for the manager. @@ -69,6 +75,47 @@ export default class SegmentationStateManager { this.uid = uid; } + /** + * Generates a key for the _labelmapImageIdReferenceMap + * @param param0.segmentationId + * @param param0.imageId + */ + generateMapKey({ segmentationId, imageId }) { + return `${segmentationId}-${imageId}`; + } + + /** + * Updates the _labelmapImageIdReferenceMap according to the correct key and preserving old values + * @param param0.segmentationId + * @param param0.imageId + * @param param0.labelmapImageId + */ + _updateLabelmapImageIdReferenceMap({ + segmentationId, + imageId, + labelmapImageId, + }) { + const key = this.generateMapKey({ segmentationId, imageId }); + + console.log( + '🚀 ~ SegmentationStateManager ~ segmentationId, imageId:', + segmentationId, + imageId + ); + console.log( + '🚀 ~ SegmentationStateManager ~ labelmapImageId:', + labelmapImageId + ); + if (!this._labelmapImageIdReferenceMap.has(key)) { + this._labelmapImageIdReferenceMap.set(key, [labelmapImageId]); + return; + } + + const currentValues = this._labelmapImageIdReferenceMap.get(key); + const newValues = Array.from(new Set([...currentValues, labelmapImageId])); + this._labelmapImageIdReferenceMap.set(key, newValues); + } + /** * Returns a copy of the current state of the segmentation. */ @@ -464,6 +511,11 @@ export default class SegmentationStateManager { this._stackLabelmapImageIdReferenceMap .get(segmentationId) .set(currentImageId, labelmapImageId); + this._updateLabelmapImageIdReferenceMap({ + segmentationId, + imageId: currentImageId, + labelmapImageId, + }); } } @@ -553,6 +605,11 @@ export default class SegmentationStateManager { this._stackLabelmapImageIdReferenceMap .get(segmentationId) .set(imageId, labelmapImageId); + this._updateLabelmapImageIdReferenceMap({ + segmentationId, + imageId, + labelmapImageId, + }); } } }); @@ -589,6 +646,33 @@ export default class SegmentationStateManager { return labelmapImageIds; } + /** + * Retrieves the stack labelmap imageId associated with the current imageId + * that is rendered on the viewport. + * @param viewportId - The ID of the viewport. + * @param segmentationId - The UID of the segmentation representation. + * @returns A Map object containing the image ID reference map, or undefined if the enabled element is not found. + */ + getCurrentLabelmapImageIdForViewportOverlapping( + viewportId: string, + segmentationId: string + ): string[] | undefined { + const enabledElement = getEnabledElementByViewportId(viewportId); + + if (!enabledElement) { + return; + } + + const stackViewport = enabledElement.viewport as Types.IStackViewport; + const currentImageId = stackViewport.getCurrentImageId(); + + const key = this.generateMapKey({ + segmentationId, + imageId: currentImageId, + }); + return this._labelmapImageIdReferenceMap.get(key); + } + /** * Retrieves the stack labelmap imageId associated with the current imageId * that is rendered on the viewport. diff --git a/packages/tools/src/stateManagement/segmentation/getCurrentLabelmapImageIdForViewport.ts b/packages/tools/src/stateManagement/segmentation/getCurrentLabelmapImageIdForViewport.ts index 1e162817e4..454c1c4b59 100644 --- a/packages/tools/src/stateManagement/segmentation/getCurrentLabelmapImageIdForViewport.ts +++ b/packages/tools/src/stateManagement/segmentation/getCurrentLabelmapImageIdForViewport.ts @@ -17,3 +17,14 @@ export function getCurrentLabelmapImageIdForViewport( segmentationId ); } + +export function getCurrentLabelmapImageIdForViewportOverlapping( + viewportId: string, + segmentationId: string +) { + const segmentationStateManager = defaultSegmentationStateManager; + return segmentationStateManager.getCurrentLabelmapImageIdForViewportOverlapping( + viewportId, + segmentationId + ); +} diff --git a/packages/tools/src/stateManagement/segmentation/helpers/getSegmentationActor.ts b/packages/tools/src/stateManagement/segmentation/helpers/getSegmentationActor.ts index 0c088d69fe..85711071bb 100644 --- a/packages/tools/src/stateManagement/segmentation/helpers/getSegmentationActor.ts +++ b/packages/tools/src/stateManagement/segmentation/helpers/getSegmentationActor.ts @@ -32,6 +32,35 @@ function getActorEntry( return filteredActors.length > 0 ? filteredActors[0] : undefined; } +/** + * Retrieves the actor entry based on the given criteria. + * @param viewportId - The ID of the viewport. + * @param segmentationId - The ID of the segmentation. + * @param filterFn - A function to filter the actors. + * @returns The actor entry if found, undefined otherwise. + */ +function getActorEntries( + viewportId: string, + filterFn: (actor: Types.ActorEntry) => boolean +): Types.ActorEntry[] | undefined { + const enabledElement = getEnabledElementByViewportId(viewportId); + + if (!enabledElement) { + return; + } + + const { renderingEngine, viewport } = enabledElement; + + if (!renderingEngine || !viewport) { + return; + } + + const actors = viewport.getActors(); + const filteredActors = actors.filter(filterFn); + + return filteredActors.length > 0 ? filteredActors : undefined; +} + /** * Retrieves the UID of the labelmap actor for the given viewport and segmentation. * @param viewportId - The ID of the viewport. @@ -42,10 +71,27 @@ export function getLabelmapActorUID( viewportId: string, segmentationId: string ): string | undefined { - const actorEntry = getLabelmapActorEntry(viewportId, segmentationId); + const actorEntry = getLabelmapActorEntries(viewportId, segmentationId); return actorEntry?.uid; } +/** + * Retrieves the labelmap actor entries for the given viewport and segmentation. + * @param viewportId - The ID of the viewport. + * @param segmentationId - The ID of the segmentation. + * @returns The labelmap actor entry if found, undefined otherwise. + */ +export function getLabelmapActorEntries( + viewportId: string, + segmentationId: string +) { + return getActorEntries(viewportId, (actor) => + (actor.representationUID as string)?.startsWith( + `${segmentationId}-${SegmentationRepresentations.Labelmap}` + ) + ); +} + /** * Retrieves the labelmap actor entry for the given viewport and segmentation. * @param viewportId - The ID of the viewport. @@ -57,8 +103,7 @@ export function getLabelmapActorEntry( segmentationId: string ) { return getActorEntry(viewportId, segmentationId, (actor) => - // @ts-expect-error - actor.representationUID?.startsWith( + (actor.representationUID as string)?.startsWith( `${segmentationId}-${SegmentationRepresentations.Labelmap}` ) ); diff --git a/packages/tools/src/stateManagement/segmentation/helpers/index.ts b/packages/tools/src/stateManagement/segmentation/helpers/index.ts index d6880aeac8..b397f4b78b 100644 --- a/packages/tools/src/stateManagement/segmentation/helpers/index.ts +++ b/packages/tools/src/stateManagement/segmentation/helpers/index.ts @@ -1,5 +1,6 @@ import validateSegmentationInput from './validateSegmentationInput'; import { + getLabelmapActorEntries, getLabelmapActorEntry, getSurfaceActorEntry, getLabelmapActorUID, @@ -8,6 +9,7 @@ import { export { validateSegmentationInput, + getLabelmapActorEntries, getLabelmapActorEntry, getSurfaceActorEntry, getLabelmapActorUID, diff --git a/packages/tools/src/tools/displayTools/Labelmap/addLabelmapToElement.ts b/packages/tools/src/tools/displayTools/Labelmap/addLabelmapToElement.ts index a3487dcf72..5988fd700d 100644 --- a/packages/tools/src/tools/displayTools/Labelmap/addLabelmapToElement.ts +++ b/packages/tools/src/tools/displayTools/Labelmap/addLabelmapToElement.ts @@ -14,7 +14,7 @@ import type { LabelmapSegmentationDataStack, LabelmapSegmentationDataVolume, } from '../../../types/LabelmapTypes'; -import { getCurrentLabelmapImageIdForViewport } from '../../../stateManagement/segmentation/getCurrentLabelmapImageIdForViewport'; +import { getCurrentLabelmapImageIdForViewportOverlapping } from '../../../stateManagement/segmentation/getCurrentLabelmapImageIdForViewport'; import { getSegmentation } from '../../../stateManagement/segmentation/getSegmentation'; import { triggerSegmentationDataModified, @@ -52,7 +52,6 @@ async function addLabelmapToElement( const visibility = true; const immediateRender = false; const suppressEvents = true; - if (viewport instanceof BaseVolumeViewport) { const volumeLabelMapData = labelMapData as LabelmapSegmentationDataVolume; const volumeId = _ensureVolumeHasVolumeId( @@ -118,17 +117,18 @@ async function addLabelmapToElement( } else { // We can use the current imageId in the viewport to get the segmentation imageId // which later is used to create the actor and mapper. - const segmentationImageId = getCurrentLabelmapImageIdForViewport( - viewport.id, - segmentationId - ); + const segmentationImageIds = + getCurrentLabelmapImageIdForViewportOverlapping( + viewport.id, + segmentationId + ); - const stackInputs: Types.IStackInput[] = [ - { - imageId: segmentationImageId, - representationUID: `${segmentationId}-${SegmentationRepresentations.Labelmap}`, - }, - ]; + const stackInputs: Types.IStackInput[] = segmentationImageIds.map( + (imageId) => ({ + imageId, + representationUID: `${segmentationId}-${SegmentationRepresentations.Labelmap}-${imageId}`, + }) + ); // Add labelmap volumes to the viewports to be be rendered, but not force the render addImageSlicesToViewports(renderingEngine, stackInputs, [viewportId]); diff --git a/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts b/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts index 6e4e1ad96b..00e40b9623 100644 --- a/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts +++ b/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts @@ -18,7 +18,7 @@ import addLabelmapToElement from './addLabelmapToElement'; import removeLabelmapFromElement from './removeLabelmapFromElement'; import { getActiveSegmentation } from '../../../stateManagement/segmentation/activeSegmentation'; import { getColorLUT } from '../../../stateManagement/segmentation/getColorLUT'; -import { getCurrentLabelmapImageIdForViewport } from '../../../stateManagement/segmentation/getCurrentLabelmapImageIdForViewport'; +import { getCurrentLabelmapImageIdForViewportOverlapping } from '../../../stateManagement/segmentation/getCurrentLabelmapImageIdForViewport'; import { getSegmentation } from '../../../stateManagement/segmentation/getSegmentation'; import { canComputeRequestedRepresentation } from '../../../stateManagement/segmentation/polySeg/canComputeRequestedRepresentation'; import { computeAndAddLabelmapRepresentation } from '../../../stateManagement/segmentation/polySeg/Labelmap/computeAndAddLabelmapRepresentation'; @@ -29,7 +29,7 @@ import SegmentationRepresentations from '../../../enums/SegmentationRepresentati import { internalGetHiddenSegmentIndices } from '../../../stateManagement/segmentation/helpers/internalGetHiddenSegmentIndices'; import { getActiveSegmentIndex } from '../../../stateManagement/segmentation/getActiveSegmentIndex'; import type vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume'; -import { getLabelmapActorEntry } from '../../../stateManagement/segmentation/helpers/getSegmentationActor'; +import { getLabelmapActorEntries } from '../../../stateManagement/segmentation/helpers/getSegmentationActor'; // 255 itself is used as preview color, so basically // we have 254 colors to use for the segments if we are using the preview. @@ -98,7 +98,10 @@ async function render( let labelmapData = segmentation.representationData[SegmentationRepresentations.Labelmap]; - let labelmapActorEntry = getLabelmapActorEntry(viewport.id, segmentationId); + let labelmapActorEntries = getLabelmapActorEntries( + viewport.id, + segmentationId + ); if ( !labelmapData && @@ -133,7 +136,7 @@ async function render( } if (viewport instanceof VolumeViewport) { - if (!labelmapActorEntry) { + if (!labelmapActorEntries) { // only add the labelmap to ToolGroup viewports if it is not already added await _addLabelmapToViewport( viewport, @@ -143,21 +146,21 @@ async function render( ); } - labelmapActorEntry = getLabelmapActorEntry(viewport.id, segmentationId); + labelmapActorEntries = getLabelmapActorEntries(viewport.id, segmentationId); } else { // stack segmentation - const labelmapImageId = getCurrentLabelmapImageIdForViewport( + const labelmapImageIds = getCurrentLabelmapImageIdForViewportOverlapping( viewport.id, segmentationId ); // if the stack labelmap is not built for the current imageId that is // rendered at the viewport then return - if (!labelmapImageId) { + if (!labelmapImageIds?.length) { return; } - if (!labelmapActorEntry) { + if (!labelmapActorEntries) { // only add the labelmap to ToolGroup viewports if it is not already added await _addLabelmapToViewport( viewport, @@ -167,14 +170,20 @@ async function render( ); } - labelmapActorEntry = getLabelmapActorEntry(viewport.id, segmentationId); + labelmapActorEntries = getLabelmapActorEntries(viewport.id, segmentationId); } - if (!labelmapActorEntry) { + if (!labelmapActorEntries) { return; } - _setLabelmapColorAndOpacity(viewport.id, labelmapActorEntry, representation); + for (const labelmapActorEntry of labelmapActorEntries) { + _setLabelmapColorAndOpacity( + viewport.id, + labelmapActorEntry, + representation + ); + } } function _setLabelmapColorAndOpacity( diff --git a/packages/tools/src/utilities/segmentation/getSegmentIndexAtWorldPoint.ts b/packages/tools/src/utilities/segmentation/getSegmentIndexAtWorldPoint.ts index 65082b7371..1816bb81cc 100644 --- a/packages/tools/src/utilities/segmentation/getSegmentIndexAtWorldPoint.ts +++ b/packages/tools/src/utilities/segmentation/getSegmentIndexAtWorldPoint.ts @@ -9,7 +9,7 @@ import type { LabelmapSegmentationDataVolume } from '../../types/LabelmapTypes'; import type { ContourSegmentationAnnotation, Segmentation } from '../../types'; import { getAnnotation } from '../../stateManagement'; import { isPointInsidePolyline3D } from '../math/polyline'; -import { getLabelmapActorEntry } from '../../stateManagement/segmentation/helpers/getSegmentationActor'; +import { getLabelmapActorEntries } from '../../stateManagement/segmentation/helpers/getSegmentationActor'; type Options = { representationType?: SegmentationRepresentations; @@ -101,7 +101,7 @@ export function getSegmentIndexAtWorldForLabelmap( return; } - const segmentationActorEntry = getLabelmapActorEntry( + const segmentationActorEntry = getLabelmapActorEntries( viewport.id, segmentation.segmentationId );