diff --git a/packages/adapters/examples/segmentationStack/demo.ts b/packages/adapters/examples/segmentationStack/demo.ts index 8a08d7bca5..35e0927059 100644 --- a/packages/adapters/examples/segmentationStack/demo.ts +++ b/packages/adapters/examples/segmentationStack/demo.ts @@ -1,6 +1,28 @@ /* eslint-disable */ const dicomMap = new Map(); +dicomMap.set( + "1.3.6.1.4.1.14519.5.2.1.3671.4754.298665348758363466150039312520", + { + fetchDicom: { + StudyInstanceUID: + "1.3.6.1.4.1.14519.5.2.1.3671.4754.298665348758363466150039312520", + SeriesInstanceUID: + "1.3.6.1.4.1.14519.5.2.1.3671.4754.235188122843915982710753948536", + wadoRsRoot: "https://d14fa38qiwhyfd.cloudfront.net/dicomweb" + }, + fetchSegmentation: { + StudyInstanceUID: + "1.3.6.1.4.1.14519.5.2.1.3671.4754.298665348758363466150039312520", + SeriesInstanceUID: + "1.2.276.0.7230010.3.1.3.1426846371.15380.1513205183.303", + SOPInstanceUID: + "1.2.276.0.7230010.3.1.4.1426846371.15380.1513205183.304", + wadoRsRoot: "https://d14fa38qiwhyfd.cloudfront.net/dicomweb" + } + } +); + dicomMap.set( "1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458", { @@ -22,6 +44,7 @@ dicomMap.set( } } ); + dicomMap.set("1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046", { fetchDicom: { StudyInstanceUID: diff --git a/packages/adapters/examples/segmentationStack/index.ts b/packages/adapters/examples/segmentationStack/index.ts index 1c7b11c09a..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( @@ -194,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 f9ba0d3daa..f74c3a1eae 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"; import addDropDownToToolbar from "../../../../utils/demo/helpers/addDropdownToToolbar"; @@ -227,7 +227,7 @@ addButtonToToolbar({ onClick: async () => { const segmentationId = cornerstone.utilities.uuidv4(); state.segmentationId = segmentationId; - await createSegmentation(state); + await createEmptySegmentation(state); createSegmentationRepresentation(); updateSegmentationDropdown(); }, diff --git a/packages/adapters/examples/segmentationVolume/utils.ts b/packages/adapters/examples/segmentationVolume/utils.ts index c693e9ce4a..4d7d38739b 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); @@ -67,7 +86,7 @@ export async function readSegmentation(file: File, state) { export async function loadSegmentation(arrayBuffer: ArrayBuffer, state) { const { referenceImageIds, skipOverlapping, segmentationId } = state; - const generateToolState = + const { labelMapImages } = await Cornerstone3D.Segmentation.createFromDICOMSegBuffer( referenceImageIds, arrayBuffer, @@ -77,24 +96,7 @@ export async function loadSegmentation(arrayBuffer: ArrayBuffer, state) { } ); - await createSegmentation(state); - - const segmentation = - csToolsSegmentation.state.getSegmentation(segmentationId); - - const { imageIds } = segmentation.representationData.Labelmap; - const derivedSegmentationImages = imageIds.map(imageId => - cache.getImage(imageId) - ); - - const labelmapImagesNonOverlapping = generateToolState.labelMapImages[0]; - - for (let i = 0; i < derivedSegmentationImages.length; i++) { - const voxelManager = derivedSegmentationImages[i].voxelManager; - const scalarData = voxelManager.getScalarData(); - scalarData.set(labelmapImagesNonOverlapping[i].getPixelData()); - voxelManager.setScalarData(scalarData); - } + await createSegmentation({ state, labelMapImages }); } export async function exportSegmentation(state) { diff --git a/packages/adapters/jest.config.js b/packages/adapters/jest.config.js index bd1f3dad96..2173321fb3 100644 --- a/packages/adapters/jest.config.js +++ b/packages/adapters/jest.config.js @@ -7,5 +7,6 @@ module.exports = { displayName: "adapters", moduleNameMapper: { "^@cornerstonejs/(.*)$": path.resolve(__dirname, "../$1/src") - } + }, + testEnvironment: undefined }; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/compactMergeSegData.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/compactMergeSegData.ts new file mode 100644 index 0000000000..5e4393b76c --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/compactMergeSegData.ts @@ -0,0 +1,81 @@ +const checkHasOverlapping = ({ largerArray, currentTestedArray, newArray }) => + largerArray.some((_, currentImageIndex) => { + const originalImagePixelData = currentTestedArray[currentImageIndex]; + + const newImagePixelData = newArray[currentImageIndex]; + + if (!originalImagePixelData || !newImagePixelData) { + return false; + } + + return originalImagePixelData.some( + (originalPixel, currentPixelIndex) => { + const newPixel = newImagePixelData[currentPixelIndex]; + return originalPixel && newPixel; + } + ); + }); + +export const compactMergeSegmentDataWithoutInformationLoss = ({ + arrayOfSegmentData, + newSegmentData +}) => { + if (arrayOfSegmentData.length === 0) { + arrayOfSegmentData.push(newSegmentData); + return; + } + + for ( + let currentTestedIndex = 0; + currentTestedIndex < arrayOfSegmentData.length; + currentTestedIndex++ + ) { + const currentTestedArray = arrayOfSegmentData[currentTestedIndex]; + + const originalArrayIsLarger = + currentTestedArray.length > newSegmentData.length; + const largerArray = originalArrayIsLarger + ? currentTestedArray + : newSegmentData; + + const hasOverlapping = checkHasOverlapping({ + currentTestedArray, + largerArray, + newArray: newSegmentData + }); + + if (hasOverlapping) { + continue; + } + + largerArray.forEach((_, currentImageIndex) => { + const originalImagePixelData = + currentTestedArray[currentImageIndex]; + const newImagePixelData = newSegmentData[currentImageIndex]; + + if ( + (!originalImagePixelData && !newImagePixelData) || + !newImagePixelData + ) { + return; + } + + if (!originalImagePixelData) { + currentTestedArray[currentImageIndex] = newImagePixelData; + return; + } + + const mergedPixelData = originalImagePixelData.map( + (originalPixel, currentPixelIndex) => { + const newPixel = newImagePixelData[currentPixelIndex]; + return originalPixel || newPixel; + } + ); + + currentTestedArray[currentImageIndex] = mergedPixelData; + }); + return; + } + + arrayOfSegmentData.push(newSegmentData); +}; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.ts index c0c0d240dd..88ad73ec87 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.ts @@ -13,11 +13,53 @@ import { readFromUnpackedChunks, unpackPixelData } from "../../Cornerstone/Segmentation_4X"; +import { compactMergeSegmentDataWithoutInformationLoss } from "./compactMergeSegData"; const { DicomMessage, DicomMetaDictionary } = dcmjsData; const { Normalizer } = normalizers; const { decode } = utilities.compression; +const updateSegmentsOnFrame = ({ + segmentsOnFrame, + imageIdIndex, + segmentIndex +}) => { + if (!segmentsOnFrame[imageIdIndex]) { + segmentsOnFrame[imageIdIndex] = []; + } + + segmentsOnFrame[imageIdIndex].push(segmentIndex); +}; + +const updateSegmentsPixelIndices = ({ + segmentsPixelIndices, + segmentIndex, + imageIdIndex, + indexCache +}) => { + if (!segmentsPixelIndices.has(segmentIndex)) { + segmentsPixelIndices.set(segmentIndex, {}); + } + const segmentIndexObject = segmentsPixelIndices.get(segmentIndex); + segmentIndexObject[imageIdIndex] = indexCache; + segmentsPixelIndices.set(segmentIndex, segmentIndexObject); +}; + +const extractInfoFromPerFrameFunctionalGroups = ({ + PerFrameFunctionalGroups, + sequenceIndex, + sopUIDImageIdIndexMap, + multiframe +}) => { + const referencedSOPInstanceUid = + PerFrameFunctionalGroups.DerivationImageSequence[0] + .SourceImageSequence[0].ReferencedSOPInstanceUID; + const referencedImageId = sopUIDImageIdIndexMap[referencedSOPInstanceUid]; + const segmentIndex = getSegmentIndex(multiframe, sequenceIndex); + + return { referencedSOPInstanceUid, referencedImageId, segmentIndex }; +}; + async function createLabelmapsFromBufferInternal( referencedImageIds, arrayBuffer, @@ -140,9 +182,7 @@ async function createLabelmapsFromBufferInternal( switch (orientation) { case "Planar": if (overlapping) { - throw new Error( - "Overlapping segmentations are not yet supported." - ); + insertFunction = insertOverlappingPixelDataPlanar; } else { insertFunction = insertPixelDataPlanar; } @@ -163,8 +203,6 @@ async function createLabelmapsFromBufferInternal( 3) insertFunction will return the number of LabelMaps 4) generateToolState return is an array*/ - const segmentsOnFrameArray = []; - segmentsOnFrameArray[0] = []; const segmentsOnFrame = []; const imageIdMaps = { indices: {}, metadata: {} }; @@ -188,19 +226,21 @@ async function createLabelmapsFromBufferInternal( // segment in the labelmapBuffer const segmentsPixelIndices = new Map(); - const overlappingSegments = await insertFunction( - segmentsOnFrame, - labelMapImages, - pixelDataChunks, - multiframe, - referencedImageIds, - validOrientations, - metadataProvider, - tolerance, - segmentsPixelIndices, - sopUIDImageIdIndexMap, - imageIdMaps - ); + const { hasOverlappingSegments, arrayOfLabelMapImages } = + await insertFunction({ + segmentsOnFrame, + labelMapImages, + pixelDataChunks, + multiframe, + referencedImageIds, + validOrientations, + metadataProvider, + tolerance, + segmentsPixelIndices, + sopUIDImageIdIndexMap, + imageIdMaps, + TypedArrayConstructor + }); // calculate the centroid of each segment const centroidXYZ = new Map(); @@ -217,29 +257,27 @@ async function createLabelmapsFromBufferInternal( }); return { - // array of array since there might be overlapping segments - labelMapImages: [labelMapImages], + labelMapImages: arrayOfLabelMapImages, segMetadata, segmentsOnFrame, - segmentsOnFrameArray, centroids: centroidXYZ, - overlappingSegments + overlappingSegments: hasOverlappingSegments }; } -export function insertPixelDataPlanar( +export function insertPixelDataPlanar({ segmentsOnFrame, labelMapImages, - pixelData, + pixelDataChunks, multiframe, - imageIds, + referencedImageIds, validOrientations, metadataProvider, tolerance, segmentsPixelIndices, sopUIDImageIdIndexMap, imageIdMaps -) { +}) { const { SharedFunctionalGroupsSequence, PerFrameFunctionalGroupsSequence, @@ -271,7 +309,7 @@ export function insertPixelDataPlanar( .ImageOrientationPatient; const view = readFromUnpackedChunks( - pixelData, + pixelDataChunks, i * sliceLength, sliceLength ); @@ -307,7 +345,7 @@ export function insertPixelDataPlanar( const imageId = findReferenceSourceImageId( multiframe, i, - imageIds, + referencedImageIds, metadataProvider, tolerance, sopUIDImageIdIndexMap @@ -367,8 +405,332 @@ export function insertPixelDataPlanar( segmentIndexObject[imageIdIndex] = indexCache; segmentsPixelIndices.set(segmentIndex, segmentIndexObject); } - resolve(overlapping); + resolve({ + hasOverlappingSegments: overlapping, + arrayOfLabelMapImages: [labelMapImages] + }); + }); +} + +const getAlignedPixelData = ({ + sharedImageOrientationPatient, + PerFrameFunctionalGroups, + pixelDataChunks, + sequenceIndex, + sliceLength, + Rows, + Columns, + validOrientations, + tolerance +}) => { + const ImageOrientationPatientI = + sharedImageOrientationPatient || + PerFrameFunctionalGroups.PlaneOrientationSequence + .ImageOrientationPatient; + + const view = readFromUnpackedChunks( + pixelDataChunks, + sequenceIndex * sliceLength, + sliceLength + ); + + const pixelDataI2D = ndarray(view, [Rows, Columns]); + + const alignedPixelDataI = alignPixelDataWithSourceData( + pixelDataI2D, + ImageOrientationPatientI, + validOrientations, + tolerance + ); + + if (!alignedPixelDataI) { + throw new Error( + "Individual SEG frames are out of plane with respect to the first SEG frame. " + + "This is not yet supported. Aborting segmentation loading." + ); + } + return alignedPixelDataI; +}; + +const checkImageDimensions = ({ metadataProvider, imageId, Rows, Columns }) => { + const sourceImageMetadata = metadataProvider.get("instance", imageId); + if ( + Rows !== sourceImageMetadata.Rows || + Columns !== sourceImageMetadata.Columns + ) { + throw new Error( + "Individual SEG frames have different geometry dimensions (Rows and Columns) " + + "respect to the source image reference frame. This is not yet supported. " + + "Aborting segmentation loading. " + ); + } +}; + +const getArrayOfLabelMapImagesWithSegmentData = ({ + arrayOfSegmentData, + referencedImageIds +}) => { + let largestArray = []; + let largestArrayIndex; + + for (let i = 0; i < arrayOfSegmentData.length; i++) { + const segmentData = arrayOfSegmentData[i]; + if (segmentData.length > largestArray.length) { + largestArray = segmentData; + largestArrayIndex = i; + } + } + + return arrayOfSegmentData.map(arr => { + const labelMapImages = referencedImageIds + .map((referencedImageId, i) => { + const hasEmptySegmentData = !arr[i]; + + // @TODO: right now cornerstone loses reference of the images when you don't have the complete set of images for each + // grouping of segments, but in order to save memory we would ideally only duplicate images where the there is overlapping + // so when this losing of reference is fixed, we can implement some filter like the one below in order to get rid of empty + // segment images that only take up memory space + // if (hasEmptySegmentData && i !== largestArrayIndex) { + // return; + // } + + const labelMapImage = + imageLoader.createAndCacheDerivedLabelmapImage( + referencedImageId + ); + + const pixelData = labelMapImage.getPixelData(); + + if (!hasEmptySegmentData) { + for (let j = 0; j < pixelData.length; j++) { + pixelData[j] = arr[i][j]; + } + } + + return labelMapImage; + }) + .filter(Boolean); + return labelMapImages; + }); +}; + +export function insertOverlappingPixelDataPlanar({ + segmentsOnFrame, + labelMapImages, + pixelDataChunks, + multiframe, + referencedImageIds, + validOrientations, + metadataProvider, + tolerance, + segmentsPixelIndices, + sopUIDImageIdIndexMap, + imageIdMaps +}) { + const { + SharedFunctionalGroupsSequence, + PerFrameFunctionalGroupsSequence, + Rows, + Columns + } = multiframe; + + const sharedImageOrientationPatient = + SharedFunctionalGroupsSequence.PlaneOrientationSequence + ? SharedFunctionalGroupsSequence.PlaneOrientationSequence + .ImageOrientationPatient + : undefined; + const sliceLength = Columns * Rows; + + const arrayOfSegmentData = getArrayOfSegmentData({ + sliceLength, + Rows, + Columns, + validOrientations, + metadataProvider, + imageIdMaps, + segmentsOnFrame, + tolerance, + pixelDataChunks, + PerFrameFunctionalGroupsSequence, + labelMapImages, + sopUIDImageIdIndexMap, + multiframe, + sharedImageOrientationPatient, + segmentsPixelIndices }); + + const arrayOfLabelMapImagesWithSegmentData = + getArrayOfLabelMapImagesWithSegmentData({ + arrayOfSegmentData, + referencedImageIds + }); + + return { + arrayOfLabelMapImages: arrayOfLabelMapImagesWithSegmentData, + hasOverlappingSegments: true + }; } +const getArrayOfSegmentData = ({ + sliceLength, + Rows, + Columns, + validOrientations, + metadataProvider, + imageIdMaps, + segmentsOnFrame, + tolerance, + pixelDataChunks, + PerFrameFunctionalGroupsSequence, + labelMapImages, + sopUIDImageIdIndexMap, + multiframe, + sharedImageOrientationPatient, + segmentsPixelIndices +}) => { + const arrayOfSegmentData = []; + const numberOfSegments = multiframe.SegmentSequence.length; + for ( + let currentSegmentIndex = 1; + currentSegmentIndex <= numberOfSegments; + ++currentSegmentIndex + ) { + const segmentData = getSegmentData({ + PerFrameFunctionalGroupsSequence, + labelMapImages, + sopUIDImageIdIndexMap, + multiframe, + segmentIndex: currentSegmentIndex, + sliceLength, + Rows, + Columns, + validOrientations, + tolerance, + pixelDataChunks, + sharedImageOrientationPatient, + metadataProvider, + imageIdMaps, + segmentsOnFrame, + segmentsPixelIndices + }); + + compactMergeSegmentDataWithoutInformationLoss({ + arrayOfSegmentData, + newSegmentData: segmentData + }); + } + + return arrayOfSegmentData; +}; + +const getSegmentData = ({ + PerFrameFunctionalGroupsSequence, + labelMapImages, + sopUIDImageIdIndexMap, + multiframe, + segmentIndex, + sliceLength, + Rows, + Columns, + validOrientations, + tolerance, + pixelDataChunks, + sharedImageOrientationPatient, + metadataProvider, + imageIdMaps, + segmentsOnFrame, + segmentsPixelIndices +}) => { + const segmentData = []; + + for ( + let currentLabelMapImageIndex = 0; + currentLabelMapImageIndex < labelMapImages.length; + currentLabelMapImageIndex++ + ) { + const currentLabelMapImage = labelMapImages[currentLabelMapImageIndex]; + const referencedImageId = currentLabelMapImage.referencedImageId; + + const PerFrameFunctionalGroupsIndex = + PerFrameFunctionalGroupsSequence.findIndex( + (PerFrameFunctionalGroups, currentSequenceIndex) => { + const { + segmentIndex: groupsSegmentIndex, + referencedImageId: groupsReferenceImageId + } = extractInfoFromPerFrameFunctionalGroups({ + PerFrameFunctionalGroups, + sequenceIndex: currentSequenceIndex, + sopUIDImageIdIndexMap, + multiframe + }); + + const isCorrectPerFrameFunctionalGroup = + groupsSegmentIndex === segmentIndex && + groupsReferenceImageId === + currentLabelMapImage.referencedImageId; + + return isCorrectPerFrameFunctionalGroup; + } + ); + + if (PerFrameFunctionalGroupsIndex === -1) { + continue; + } + + const PerFrameFunctionalGroups = + PerFrameFunctionalGroupsSequence[PerFrameFunctionalGroupsIndex]; + + const alignedPixelDataI = getAlignedPixelData({ + sharedImageOrientationPatient, + PerFrameFunctionalGroups, + pixelDataChunks, + sequenceIndex: PerFrameFunctionalGroupsIndex, + sliceLength, + Rows, + Columns, + validOrientations, + tolerance + }); + + checkImageDimensions({ + metadataProvider, + Rows, + Columns, + imageId: referencedImageId + }); + + const indexCache = []; + const segmentationDataForImageId = alignedPixelDataI.data.map( + (pixel, pixelIndex) => { + const pixelValue = pixel ? segmentIndex : 0; + if (pixelValue) { + indexCache.push(pixelIndex); + } + return pixel ? segmentIndex : 0; + } + ); + + const hasWrittenSegmentationData = indexCache.length > 0; + + if (hasWrittenSegmentationData) { + segmentData[currentLabelMapImageIndex] = segmentationDataForImageId; + } + + const imageIdIndex = imageIdMaps.indices[referencedImageId]; + + updateSegmentsOnFrame({ + imageIdIndex, + segmentIndex, + segmentsOnFrame + }); + updateSegmentsPixelIndices({ + imageIdIndex, + segmentIndex, + segmentsPixelIndices, + indexCache + }); + } + return segmentData; +}; + export { createLabelmapsFromBufferInternal }; diff --git a/packages/adapters/test/compactMergeSegData.jest.js b/packages/adapters/test/compactMergeSegData.jest.js new file mode 100644 index 0000000000..d882fbdc10 --- /dev/null +++ b/packages/adapters/test/compactMergeSegData.jest.js @@ -0,0 +1,164 @@ +import { describe, it, expect } from "@jest/globals"; +import { compactMergeSegmentDataWithoutInformationLoss } from "../src/adapters/Cornerstone3D/Segmentation/compactMergeSegData"; + +describe("compactMergeSegmentDataWithoutInformationLoss", () => { + it("should have defined compactMergeSegmentDataWithoutInformationLoss", () => { + expect(compactMergeSegmentDataWithoutInformationLoss).toBeDefined(); + }); + + it("should use new array as first item if there are no initial arrays", () => { + const arrayOfSegmentData = []; + const newSegmentData = [ + [1, 2], + [2, 3] + ]; + + compactMergeSegmentDataWithoutInformationLoss({ + arrayOfSegmentData, + newSegmentData: newSegmentData + }); + + expect(arrayOfSegmentData).toEqual([newSegmentData]); + }); + + it("should merge arrays when there's no overlapping", () => { + const arrayOfSegmentData = [ + [ + [1, 0], + [0, 1] + ] + ]; + const newSegmentData = [ + [0, 2], + [2, 0] + ]; + + compactMergeSegmentDataWithoutInformationLoss({ + arrayOfSegmentData, + newSegmentData: newSegmentData + }); + + expect(arrayOfSegmentData).toEqual([ + [ + [1, 2], + [2, 1] + ] + ]); + }); + + it("should not merge arrays when there is overlapping", () => { + const arrayOfSegmentData = [ + [ + [1, 1], + [0, 1] + ] + ]; + const newSegmentData = [ + [0, 2], + [2, 0] + ]; + + compactMergeSegmentDataWithoutInformationLoss({ + arrayOfSegmentData, + newSegmentData: newSegmentData + }); + + expect(arrayOfSegmentData).toEqual([ + [ + [1, 1], + [0, 1] + ], + + [ + [0, 2], + [2, 0] + ] + ]); + }); + + it("should merge with the second array when there is overlapping in the first but not in the second one", () => { + const arrayOfSegmentData = [ + [ + [1, 1], + [0, 1] + ], + [ + [1, 0], + [0, 1] + ] + ]; + const newSegmentData = [ + [0, 2], + [2, 0] + ]; + + compactMergeSegmentDataWithoutInformationLoss({ + arrayOfSegmentData, + newSegmentData: newSegmentData + }); + + expect(arrayOfSegmentData).toEqual([ + [ + [1, 1], + [0, 1] + ], + + [ + [1, 2], + [2, 1] + ] + ]); + }); + + it("should keep undefined (empty) elements if both new and original array have them in the same position", () => { + const arrayOfSegmentData = [[undefined, [0, 1]]]; + const newSegmentData = [undefined, [2, 0]]; + + compactMergeSegmentDataWithoutInformationLoss({ + arrayOfSegmentData, + newSegmentData: newSegmentData + }); + + expect(arrayOfSegmentData).toEqual([[undefined, [2, 1]]]); + }); + + it("should keep the original elements if the corresponding new position is undefined (empty)", () => { + const arrayOfSegmentData = [[[0, 1]]]; + const newSegmentData = [ + [0, 0], + [2, 0] + ]; + + compactMergeSegmentDataWithoutInformationLoss({ + arrayOfSegmentData, + newSegmentData: newSegmentData + }); + + expect(arrayOfSegmentData).toEqual([ + [ + [0, 1], + [2, 0] + ] + ]); + }); + + it("should keep the new elements if the corresponding original position is undefined (empty)", () => { + const arrayOfSegmentData = [[undefined, [0, 1]]]; + const newSegmentData = [ + [2, 2], + [2, 0] + ]; + + compactMergeSegmentDataWithoutInformationLoss({ + arrayOfSegmentData, + newSegmentData: newSegmentData + }); + + expect(arrayOfSegmentData).toEqual([ + [ + [2, 2], + [2, 1] + ] + ]); + }); +}); diff --git a/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts b/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts index 20f0ac5709..38e52b78ba 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 { getCurrentLabelmapImageIdsForViewport } 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 = getCurrentLabelmapImageIdsForViewport( viewportId, representation.segmentationId ); - return derivedImageId === actor.referencedId; + return derivedImageIds?.includes(actor.referencedId); }); if (!validActor) { @@ -130,105 +130,110 @@ function _imageChangeEventListener(evt) { labelmapRepresentations.forEach((representation) => { const { segmentationId } = representation; const currentImageId = viewport.getCurrentImageId(); - const derivedImageId = getCurrentLabelmapImageIdForViewport( + const derivedImageIds = getCurrentLabelmapImageIdsForViewport( 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; - } + const updateSegmentationActor = (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 + ); + } } - } + }; + + derivedImageIds.forEach(updateSegmentationActor); viewport.render(); diff --git a/packages/tools/src/eventListeners/segmentation/labelmap/onLabelmapSegmentationDataModified.ts b/packages/tools/src/eventListeners/segmentation/labelmap/onLabelmapSegmentationDataModified.ts index e3219d5644..a9f7ee1b9d 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?.length) { 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.getCurrentLabelmapImageIdsForViewport( + viewportId, + segmentationId + ); - const segmentationImage = cache.getImage(currentSegmentationImageId); - segImageData.modified(); + const segmentationImage = cache.getImage(currentSegmentationImageId[0]); + 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..9ccf49413b 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. @@ -450,7 +456,7 @@ export default class SegmentationStateManager { labelmapImageIds, updateCallback ): string | undefined { - const currentImageId = viewport.getCurrentImageId(); + const referenceImageId = viewport.getCurrentImageId(); let viewableLabelmapImageIdFound = false; for (const labelmapImageId of labelmapImageIds) { @@ -463,7 +469,12 @@ export default class SegmentationStateManager { viewableLabelmapImageIdFound = true; this._stackLabelmapImageIdReferenceMap .get(segmentationId) - .set(currentImageId, labelmapImageId); + .set(referenceImageId, labelmapImageId); + this._updateLabelmapImageIdReferenceMap({ + segmentationId, + referenceImageId, + labelmapImageId, + }); } } @@ -474,7 +485,7 @@ export default class SegmentationStateManager { return viewableLabelmapImageIdFound ? this._stackLabelmapImageIdReferenceMap .get(segmentationId) - .get(currentImageId) + .get(referenceImageId) : undefined; } @@ -542,7 +553,7 @@ export default class SegmentationStateManager { labelmapImageIds, (stackViewport, segmentationId, labelmapImageIds) => { const imageIds = stackViewport.getImageIds(); - imageIds.forEach((imageId, index) => { + imageIds.forEach((referenceImageId, index) => { for (const labelmapImageId of labelmapImageIds) { const viewableImageId = stackViewport.isReferenceViewable( { referencedImageId: labelmapImageId, sliceIndex: index }, @@ -552,7 +563,12 @@ export default class SegmentationStateManager { if (viewableImageId) { this._stackLabelmapImageIdReferenceMap .get(segmentationId) - .set(imageId, labelmapImageId); + .set(referenceImageId, labelmapImageId); + this._updateLabelmapImageIdReferenceMap({ + segmentationId, + referenceImageId, + labelmapImageId, + }); } } }); @@ -589,6 +605,33 @@ export default class SegmentationStateManager { return labelmapImageIds; } + /** + * Retrieves the stack labelmap imageIds 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. + */ + getCurrentLabelmapImageIdsForViewport( + viewportId: string, + segmentationId: string + ): string[] | undefined { + const enabledElement = getEnabledElementByViewportId(viewportId); + + if (!enabledElement) { + return; + } + + const stackViewport = enabledElement.viewport as Types.IStackViewport; + const referenceImageId = stackViewport.getCurrentImageId(); + + const key = this._generateMapKey({ + segmentationId, + referenceImageId, + }); + return this._labelmapImageIdReferenceMap.get(key); + } + /** * Retrieves the stack labelmap imageId associated with the current imageId * that is rendered on the viewport. @@ -816,6 +859,29 @@ export default class SegmentationStateManager { return removedRepresentations; } + /** + * Updates the _labelmapImageIdReferenceMap according to the correct key and preserving old values + * @param segmentationId + * @param referenceImageId + * @param labelmapImageId + */ + _updateLabelmapImageIdReferenceMap({ + segmentationId, + referenceImageId, + labelmapImageId, + }) { + const key = this._generateMapKey({ segmentationId, referenceImageId }); + + 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); + } + _setActiveSegmentation( state: SegmentationState, viewportId: string, @@ -1083,6 +1149,15 @@ export default class SegmentationStateManager { return result; } + + /** + * Generates a key for the _labelmapImageIdReferenceMap + * @param segmentationId - The ID of the segmentation + * @param referenceImageId - The reference image ID - this is the imageId that is currently being displayed in the viewport (not the derived imageId) + */ + private _generateMapKey({ segmentationId, referenceImageId }) { + return `${segmentationId}-${referenceImageId}`; + } } async function internalComputeVolumeLabelmapFromStack({ diff --git a/packages/tools/src/stateManagement/segmentation/getCurrentLabelmapImageIdForViewport.ts b/packages/tools/src/stateManagement/segmentation/getCurrentLabelmapImageIdForViewport.ts index 1e162817e4..87e978a7c1 100644 --- a/packages/tools/src/stateManagement/segmentation/getCurrentLabelmapImageIdForViewport.ts +++ b/packages/tools/src/stateManagement/segmentation/getCurrentLabelmapImageIdForViewport.ts @@ -6,13 +6,38 @@ import { defaultSegmentationStateManager } from './SegmentationStateManager'; * @param viewportId - The ID of the viewport. * @param segmentationId - The ID of the segmentation. * @returns An array of labelmap image IDs. + * + * @deprecated Use getCurrentLabelmapImageIdsForViewport instead. since we + * have added support for multiple imageIds in the same viewport for the + * same labelmap representation (overlapping segments usecase) */ export function getCurrentLabelmapImageIdForViewport( viewportId: string, segmentationId: string +) { + const imageIds = getCurrentLabelmapImageIdsForViewport( + viewportId, + segmentationId + ); + + return imageIds[0]; +} + +/** + * Retrieves the labelmap image IDs for a specific viewport and segmentation representation. + * If the segmentation has multiple imageIds for in the current view of the same segmentation + * this function will return an array of imageIds. + * + * @param viewportId - The ID of the viewport. + * @param segmentationId - The ID of the segmentation. + * @returns An array of labelmap image IDs. + */ +export function getCurrentLabelmapImageIdsForViewport( + viewportId: string, + segmentationId: string ) { const segmentationStateManager = defaultSegmentationStateManager; - return segmentationStateManager.getCurrentLabelmapImageIdForViewport( + return segmentationStateManager.getCurrentLabelmapImageIdsForViewport( viewportId, segmentationId ); diff --git a/packages/tools/src/stateManagement/segmentation/helpers/getSegmentationActor.ts b/packages/tools/src/stateManagement/segmentation/helpers/getSegmentationActor.ts index 845eb904ad..c7bd3293f4 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. @@ -46,6 +75,23 @@ export function getLabelmapActorUID( 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/stateManagement/segmentation/segmentationState.ts b/packages/tools/src/stateManagement/segmentation/segmentationState.ts index 8341935d9a..6fd9115f27 100644 --- a/packages/tools/src/stateManagement/segmentation/segmentationState.ts +++ b/packages/tools/src/stateManagement/segmentation/segmentationState.ts @@ -19,7 +19,10 @@ import { getNextColorLUTIndex } from './getNextColorLUTIndex'; import { removeColorLUT } from './removeColorLUT'; import { getViewportSegmentations } from './getViewportSegmentations'; import { getViewportIdsWithSegmentation } from './getViewportIdsWithSegmentation'; -import { getCurrentLabelmapImageIdForViewport } from './getCurrentLabelmapImageIdForViewport'; +import { + getCurrentLabelmapImageIdForViewport, + getCurrentLabelmapImageIdsForViewport, +} from './getCurrentLabelmapImageIdForViewport'; import { updateLabelmapSegmentationImageReferences } from './updateLabelmapSegmentationImageReferences'; import { getStackSegmentationImageIdsForViewport } from './getStackSegmentationImageIdsForViewport'; import { @@ -37,6 +40,7 @@ export { // get getColorLUT, getCurrentLabelmapImageIdForViewport, + getCurrentLabelmapImageIdsForViewport, getNextColorLUTIndex, getSegmentation, getSegmentations, diff --git a/packages/tools/src/tools/displayTools/Labelmap/addLabelmapToElement.ts b/packages/tools/src/tools/displayTools/Labelmap/addLabelmapToElement.ts index d3cf82426d..b6413dd5cf 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 { getCurrentLabelmapImageIdsForViewport } 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( @@ -137,17 +136,17 @@ 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( + const segmentationImageIds = getCurrentLabelmapImageIdsForViewport( 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 43643c7519..ec4f4dc946 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 { getCurrentLabelmapImageIdsForViewport } from '../../../stateManagement/segmentation/getCurrentLabelmapImageIdForViewport'; import { getSegmentation } from '../../../stateManagement/segmentation/getSegmentation'; import type vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; import type vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; @@ -27,7 +27,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'; import { getPolySeg } from '../../../config'; import { computeAndAddRepresentation } from '../../../utilities/segmentation/computeAndAddRepresentation'; import { triggerSegmentationDataModified } from '../../../stateManagement/segmentation/triggerSegmentationEvents'; @@ -100,7 +100,10 @@ async function render( let labelmapData = segmentation.representationData[SegmentationRepresentations.Labelmap]; - let labelmapActorEntry = getLabelmapActorEntry(viewport.id, segmentationId); + let labelmapActorEntries = getLabelmapActorEntries( + viewport.id, + segmentationId + ); if ( !labelmapData && @@ -155,7 +158,7 @@ async function render( } if (viewport instanceof VolumeViewport) { - if (!labelmapActorEntry) { + if (!labelmapActorEntries?.length) { // only add the labelmap to ToolGroup viewports if it is not already added await _addLabelmapToViewport( viewport, @@ -165,21 +168,21 @@ async function render( ); } - labelmapActorEntry = getLabelmapActorEntry(viewport.id, segmentationId); + labelmapActorEntries = getLabelmapActorEntries(viewport.id, segmentationId); } else { // stack segmentation - const labelmapImageId = getCurrentLabelmapImageIdForViewport( + const labelmapImageIds = getCurrentLabelmapImageIdsForViewport( 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, @@ -189,14 +192,20 @@ async function render( ); } - labelmapActorEntry = getLabelmapActorEntry(viewport.id, segmentationId); + labelmapActorEntries = getLabelmapActorEntries(viewport.id, segmentationId); } - if (!labelmapActorEntry) { + if (!labelmapActorEntries?.length) { 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..cfa5b55de7 100644 --- a/packages/tools/src/utilities/segmentation/getSegmentIndexAtWorldPoint.ts +++ b/packages/tools/src/utilities/segmentation/getSegmentIndexAtWorldPoint.ts @@ -3,7 +3,7 @@ import type { Types } from '@cornerstonejs/core'; import { SegmentationRepresentations } from '../../enums'; import { getSegmentation, - getCurrentLabelmapImageIdForViewport, + getCurrentLabelmapImageIdsForViewport, } from '../../stateManagement/segmentation/segmentationState'; import type { LabelmapSegmentationDataVolume } from '../../types/LabelmapTypes'; import type { ContourSegmentationAnnotation, Segmentation } from '../../types'; @@ -90,11 +90,20 @@ export function getSegmentIndexAtWorldForLabelmap( } // stack segmentation case - const segmentationImageId = getCurrentLabelmapImageIdForViewport( + const segmentationImageIds = getCurrentLabelmapImageIdsForViewport( viewport.id, segmentation.segmentationId ); + if (segmentationImageIds.length > 1) { + console.warn( + 'Segment selection for labelmaps with multiple imageIds in stack viewports is not supported yet.' + ); + return; + } + + const segmentationImageId = segmentationImageIds[0]; + const image = cache.getImage(segmentationImageId); if (!image) {