From daddf13e084a9b271e3d9b417dd191cc4a782cb2 Mon Sep 17 00:00:00 2001 From: sedghi Date: Thu, 5 Dec 2024 15:48:43 -0500 Subject: [PATCH 1/2] fix --- .../examples/segmentationVolume/index.ts | 767 ++++-------------- .../adapters/Cornerstone/Segmentation_4X.js | 28 +- .../planar/filterAnnotationsForDisplay.ts | 4 + 3 files changed, 159 insertions(+), 640 deletions(-) diff --git a/packages/adapters/examples/segmentationVolume/index.ts b/packages/adapters/examples/segmentationVolume/index.ts index 54498b9c51..4398d92213 100644 --- a/packages/adapters/examples/segmentationVolume/index.ts +++ b/packages/adapters/examples/segmentationVolume/index.ts @@ -8,11 +8,10 @@ import * as cornerstoneAdapters from "@cornerstonejs/adapters"; import { dicomMap } from "./demo"; import { - addBrushSizeSlider, addButtonToToolbar, addDropdownToToolbar, - addLabelToToolbar, addManipulationBindings, + addToggleButtonToToolbar, addUploadToToolbar, createImageIdsAndCacheMetaData, createInfoSection, @@ -32,7 +31,6 @@ const { Enums: csEnums, RenderingEngine, cache, - eventTarget, imageLoader, metaData, setVolumesForViewports, @@ -44,10 +42,8 @@ const { ViewportType } = csEnums; const { Enums: csToolsEnums, ToolGroupManager, - segmentation: csToolsSegmentation, - utilities: csToolsUtilities + segmentation: csToolsSegmentation } = cornerstoneTools; -const { MouseBindings } = csToolsEnums; const { wadouri } = cornerstoneDicomImageLoader; @@ -61,10 +57,12 @@ const renderingEngineId = "MY_RENDERING_ENGINE_ID"; let toolGroup; const toolGroupId = "MY_TOOL_GROUP_ID"; const viewportIds = ["CT_AXIAL", "CT_SAGITTAL", "CT_CORONAL"]; -let imageIds: string[] = []; const volumeLoaderScheme = "cornerstoneStreamingImageVolume"; let volumeId; +const segmentationId = "LOAD_SEG_ID:" + csUtilities.uuidv4(); +let referenceImageIds: string[] = []; +const segImageIds: string[] = []; // ======== Set up page ======== // setTitleAndDescription( @@ -73,6 +71,7 @@ setTitleAndDescription( ); const size = "500px"; +let skipOverlapping = false; const demoToolbar = document.getElementById("demo-toolbar"); @@ -84,18 +83,6 @@ const group2 = document.createElement("div"); group2.style.marginBottom = "10px"; demoToolbar.appendChild(group2); -const group3 = document.createElement("div"); -group3.style.marginBottom = "10px"; -demoToolbar.appendChild(group3); - -const group4 = document.createElement("div"); -group4.style.marginBottom = "10px"; -demoToolbar.appendChild(group4); - -const group5 = document.createElement("div"); -group5.style.marginBottom = "10px"; -demoToolbar.appendChild(group5); - const content = document.getElementById("content"); const viewportGrid = document.createElement("div"); @@ -132,13 +119,6 @@ createInfoSection(content) .addInstruction("Axial | Sagittal | Coronal") .closeNestedSection(); -createInfoSection(content) - .addInstruction('You can try configuring "dev" in the console:') - .openNestedSection() - .addInstruction("fetchDicom") - .addInstruction("fetchSegmentation") - .closeNestedSection(); - // ============================= // let devConfig = { @@ -156,14 +136,6 @@ const dev = { // ============================= // -async function fetchDicom() { - // Get Cornerstone imageIds for the source data and fetch metadata into RAM - imageIds = await createImageIdsAndCacheMetaData(dev.getConfig.fetchDicom); - - // - await loadDicom(imageIds.reverse()); -} - async function readDicom(files: FileList) { if (files.length <= 1) { console.error( @@ -172,17 +144,14 @@ async function readDicom(files: FileList) { return; } - imageIds = []; - for (const file of files) { const imageId = wadouri.fileManager.add(file); - await imageLoader.loadAndCacheImage(imageId); - imageIds.push(imageId); + referenceImageIds.push(imageId); } - await loadDicom(imageIds); + await loadDicom(referenceImageIds); } async function loadDicom(imageIds: string[]) { @@ -196,55 +165,18 @@ async function loadDicom(imageIds: string[]) { imageIds }); - // Generate segmentation id - const newSegmentationId = "MY_SEG_ID:" + csUtilities.uuidv4(); - // Add some segmentations based on the source data volume - await addSegmentationsToState(newSegmentationId); - // Update the dropdown - updateSegmentationDropdown(); - // toolGroup.addViewport(viewportIds[0], renderingEngineId); toolGroup.addViewport(viewportIds[1], renderingEngineId); toolGroup.addViewport(viewportIds[2], renderingEngineId); // Set the volume to load - volume.load(); + await volume.load(); // Set volumes on the viewports await setVolumesForViewports(renderingEngine, [{ volumeId }], viewportIds); // Render the image - renderingEngine.renderViewports(viewportIds); -} - -async function fetchSegmentation() { - if (!volumeId) { - return; - } - - const configSeg = dev.getConfig.fetchSegmentation; - - const client = new api.DICOMwebClient({ - url: configSeg.wadoRsRoot - }); - const arrayBuffer = await client.retrieveInstance({ - studyInstanceUID: configSeg.StudyInstanceUID, - seriesInstanceUID: configSeg.SeriesInstanceUID, - sopInstanceUID: configSeg.SOPInstanceUID - }); - - // - await loadSegmentation(arrayBuffer); -} - -async function importSegmentation(files: FileList) { - if (!volumeId) { - return; - } - - for (const file of files) { - await readSegmentation(file); - } + renderingEngine.render(); } async function readSegmentation(file: File) { @@ -269,25 +201,75 @@ async function readSegmentation(file: File) { } async function loadSegmentation(arrayBuffer: ArrayBuffer) { - // Generate segmentation id - const newSegmentationId = "LOAD_SEG_ID:" + csUtilities.uuidv4(); - - // const generateToolState = await Cornerstone3D.Segmentation.generateToolState( - imageIds, + referenceImageIds, arrayBuffer, - metaData + metaData, + { + skipOverlapping + } + ); + + if (generateToolState.labelmapBufferArray.length !== 1) { + alert( + "Overlapping segments in your segmentation are not supported yet. You can turn on the skipOverlapping option but it will override the overlapping segments." ); + return; + } + + const derivedSegmentationImages = + await imageLoader.createAndCacheDerivedLabelmapImages( + referenceImageIds as string[] + ); + + const derivedSegmentationImageIds = derivedSegmentationImages.map( + image => image.imageId + ); // - const derivedVolume = await addSegmentationsToState(newSegmentationId); - derivedVolume?.voxelManager?.setCompleteScalarDataArray?.( - new Uint8Array(generateToolState.labelmapBufferArray[0]) + // Add the segmentations to state + csToolsSegmentation.addSegmentations([ + { + segmentationId, + representation: { + // The type of segmentation + type: csToolsEnums.SegmentationRepresentations.Labelmap, + // The actual segmentation data, in the case of labelmap this is a + // reference to the source volume of the segmentation. + data: { + imageIds: derivedSegmentationImageIds + } + } + } + ]); + + const segMap = { + [viewportIds[0]]: [{ segmentationId }], + [viewportIds[1]]: [{ segmentationId }], + [viewportIds[2]]: [{ segmentationId }] + }; + // Add the segmentation representation to the viewport + await csToolsSegmentation.addLabelmapRepresentationToViewportMap(segMap); + + const volumeScalarData = new Uint8Array( + generateToolState.labelmapBufferArray[0] ); - // Update the dropdown - updateSegmentationDropdown(newSegmentationId); + // We should parse the segmentation as separate slices to support overlapping segments. + // This parsing should occur in the CornerstoneJS library adapters. + // For now, we use the volume returned from the library and chop it here. + for (let i = 0; i < derivedSegmentationImages.length; i++) { + const voxelManager = derivedSegmentationImages[i].voxelManager; + const scalarData = voxelManager.getScalarData(); + scalarData.set( + volumeScalarData.slice( + i * scalarData.length, + (i + 1) * scalarData.length + ) + ); + voxelManager.setScalarData(scalarData); + } } async function exportSegmentation() { @@ -326,7 +308,28 @@ async function exportSegmentation() { segmentIndex ); - const segmentMetadata = generateMockMetadata(segmentIndex, color); + const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB( + color.slice(0, 3).map(value => value / 255) + ).map(value => Math.round(value)); + + const segmentMetadata = { + SegmentedPropertyCategoryCodeSequence: { + CodeValue: "T-D0050", + CodingSchemeDesignator: "SRT", + CodeMeaning: "Tissue" + }, + SegmentNumber: segmentIndex.toString(), + SegmentLabel: "Tissue " + segmentIndex.toString(), + SegmentAlgorithmType: "SEMIAUTOMATIC", + SegmentAlgorithmName: "Slicer Prototype", + RecommendedDisplayCIELabValue, + SegmentedPropertyTypeCodeSequence: { + CodeValue: "T-D0050", + CodingSchemeDesignator: "SRT", + CodeMeaning: "Tissue" + } + }; + labelmapData.metadata[segmentIndex] = segmentMetadata; }); @@ -341,174 +344,6 @@ async function exportSegmentation() { downloadDICOMData(generatedSegmentation.dataset, "mySEG.dcm"); } -async function addActiveSegmentation() { - if (!volumeId) { - return; - } - - // Generate segmentation id - const newSegmentationId = "NEW_SEG_ID:" + csUtilities.uuidv4(); - // Add some segmentations based on the source data stack - await addSegmentationsToState(newSegmentationId); - // Update the dropdown - updateSegmentationDropdown(newSegmentationId); -} - -function removeActiveSegmentation() { - // - const segmentationIds = getSegmentationIds(); - // - if (segmentationIds.length <= 1) { - return; - } - - // Get active segmentation representation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - viewportIds[0] - ); - - // - csToolsSegmentation.removeSegmentationRepresentations(viewportIds[0], { - segmentationId: activeSegmentation.segmentationId - }); - - // - csToolsSegmentation.state.removeSegmentation( - activeSegmentation.segmentationId - ); - // - cache.removeVolumeLoadObject(activeSegmentation.segmentationId); - - // Update the dropdown - updateSegmentationDropdown(); -} - -function plusActiveSegment() { - if (!volumeId) { - return; - } - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - // - if (!activeSegmentation) { - return; - } - - const activeSegmentIndex = - csToolsSegmentation.segmentIndex.getActiveSegmentIndex( - activeSegmentation.segmentationId - ); - - if (activeSegmentIndex + 1 <= 255) { - csToolsSegmentation.segmentIndex.setActiveSegmentIndex( - activeSegmentation.segmentationId, - activeSegmentIndex + 1 - ); - - // Update the dropdown - updateSegmentDropdown(); - } -} - -function minusActiveSegment() { - if (!volumeId) { - return; - } - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - // - if (!activeSegmentation) { - return; - } - - const activeSegmentIndex = - csToolsSegmentation.segmentIndex.getActiveSegmentIndex( - activeSegmentation.segmentationId - ); - - if (activeSegmentIndex - 1 >= 1) { - csToolsSegmentation.segmentIndex.setActiveSegmentIndex( - activeSegmentation.segmentationId, - activeSegmentIndex - 1 - ); - - // Update the dropdown - updateSegmentDropdown(); - } -} - -function removeActiveSegment() { - if (!volumeId) { - return; - } - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - // - if (!activeSegmentation) { - return; - } - - // Get volume - const volume = cache.getVolume(activeSegmentation.segmentationId); - - // Get scalar data - // Todo: need to move to the new model with voxel manager - const scalarData = volume.voxelManager.getCompleteScalarDataArray(); - - // - const frameLength = volume.dimensions[0] * volume.dimensions[1]; - const numFrames = volume.dimensions[2]; - - // - let index = 0; - - // - const modifiedFrames = new Set(); - - const activeSegmentIndex = - csToolsSegmentation.segmentIndex.getActiveSegmentIndex( - activeSegmentation.segmentationId - ); - // - for (let f = 0; f < numFrames; f++) { - // - for (let p = 0; p < frameLength; p++) { - if (scalarData[index] === activeSegmentIndex) { - scalarData[index] = 0; - - modifiedFrames.add(f); - } - - index++; - } - } - - // - const modifiedFramesArray = Array.from(modifiedFrames); - - // Event trigger (SEGMENTATION_DATA_MODIFIED) - csToolsSegmentation.triggerSegmentationEvents.triggerSegmentationDataModified( - activeSegmentation.segmentationId, - modifiedFramesArray - ); - - // Update the dropdown - updateSegmentDropdown(); -} - // ============================= // addDropdownToToolbar({ @@ -529,7 +364,15 @@ addButtonToToolbar({ style: { marginRight: "5px" }, - onClick: fetchDicom, + onClick: async () => { + // Get Cornerstone imageIds for the source data and fetch metadata into RAM + referenceImageIds = await createImageIdsAndCacheMetaData( + dev.getConfig.fetchDicom + ); + + // + await loadDicom(referenceImageIds); + }, container: group1 }); @@ -539,7 +382,26 @@ addButtonToToolbar({ style: { marginRight: "5px" }, - onClick: fetchSegmentation, + onClick: async () => { + if (!volumeId) { + alert("load source dicom first"); + return; + } + + const configSeg = dev.getConfig.fetchSegmentation; + + const client = new api.DICOMwebClient({ + url: configSeg.wadoRsRoot + }); + const arrayBuffer = await client.retrieveInstance({ + studyInstanceUID: configSeg.StudyInstanceUID, + seriesInstanceUID: configSeg.SeriesInstanceUID, + sopInstanceUID: configSeg.SOPInstanceUID + }); + + // + await loadSegmentation(arrayBuffer); + }, container: group1 }); @@ -559,167 +421,32 @@ addUploadToToolbar({ style: { marginRight: "5px" }, - onChange: importSegmentation, - container: group2 -}); - -addButtonToToolbar({ - id: "EXPORT_SEGMENTATION", - title: "Export SEG", - onClick: exportSegmentation, - container: group2 -}); - -addDropdownToToolbar({ - id: "LABELMAP_TOOLS_DROPDOWN", - style: { - width: "150px", - marginRight: "10px" - }, - options: { map: labelmapTools.toolMap, defaultIndex: 0 }, - onSelectedValueChange: nameAsStringOrNumber => { - const tool = String(nameAsStringOrNumber); - - const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); - - if (!toolGroup) { + onChange: async (files: FileList) => { + if (!volumeId) { return; } - // Set the currently active tool disabled - const toolName = toolGroup.getActivePrimaryMouseButtonTool(); - - if (toolName) { - toolGroup.setToolDisabled(toolName); + for (const file of files) { + await readSegmentation(file); } - - toolGroup.setToolActive(tool, { - bindings: [{ mouseButton: MouseBindings.Primary }] - }); - }, - labelText: "Tools: ", - container: group3 -}); - -addBrushSizeSlider({ - toolGroupId: toolGroupId, - container: group3 -}); - -addDropdownToToolbar({ - id: "ACTIVE_SEGMENTATION_DROPDOWN", - style: { - width: "200px", - marginRight: "10px" - }, - options: { values: [], defaultValue: "" }, - placeholder: "No active segmentation...", - onSelectedValueChange: nameAsStringOrNumber => { - const segmentationId = String(nameAsStringOrNumber); - - const segmentationRepresentations = - csToolsSegmentation.state.getSegmentationRepresentationsBySegmentationId( - segmentationId - ); - - csToolsSegmentation.activeSegmentation.setActiveSegmentation( - viewportIds[0], - segmentationRepresentations[0].representations[0].segmentationId - ); - - // Update the dropdown - updateSegmentationDropdown(segmentationId); }, - labelText: "Set Active Segmentation: ", - container: group4 -}); - -addButtonToToolbar({ - id: "ADD_ACTIVE_SEGMENTATION", - style: { - marginRight: "10px" - }, - title: "Add Active Segmentation", - onClick: addActiveSegmentation, - container: group4 -}); - -addButtonToToolbar({ - id: "REMOVE_ACTIVE_SEGMENTATION", - title: "Remove Active Segmentation", - onClick: removeActiveSegmentation, - container: group4 -}); - -addLabelToToolbar({ - id: "CURRENT_ACTIVE_SEGMENT_LABEL", - title: "Current Active Segment: 1", - style: { - marginRight: "10px" - }, - container: group5 -}); - -addButtonToToolbar({ - id: "PLUS_ACTIVE_SEGMENT", - attr: { - title: "Plus Active Segment" - }, - style: { - marginRight: "10px" - }, - title: "+", - onClick: plusActiveSegment, - container: group5 + container: group2 }); addButtonToToolbar({ - id: "MINUS_ACTIVE_SEGMENT", - attr: { - title: "Minus Active Segment" - }, - style: { - marginRight: "10px" - }, - title: "-", - onClick: minusActiveSegment, - container: group5 + id: "EXPORT_SEGMENTATION", + title: "Export SEG", + onClick: exportSegmentation, + container: group2 }); -addDropdownToToolbar({ - id: "ACTIVE_SEGMENT_DROPDOWN", - style: { - width: "200px", - marginRight: "10px" - }, - options: { values: [], defaultValue: "" }, - placeholder: "No active segment...", - onSelectedValueChange: nameAsStringOrNumber => { - const segmentIndex = Number(nameAsStringOrNumber); - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - - csToolsSegmentation.segmentIndex.setActiveSegmentIndex( - activeSegmentation.segmentationId, - segmentIndex - ); - - // Update the dropdown - updateSegmentDropdown(); +addToggleButtonToToolbar({ + id: "SKIP_OVERLAPPING", + title: "Override Overlapping Segments", + onClick: () => { + skipOverlapping = !skipOverlapping; }, - labelText: "Set Active Segment: ", - container: group5 -}); - -addButtonToToolbar({ - id: "REMOVE_ACTIVE_SEGMENT", - title: "Remove Active Segment", - onClick: removeActiveSegment, - container: group5 + container: group1 }); // ============================= // @@ -734,7 +461,7 @@ function restart() { cache.removeVolumeLoadObject(volumeId); // - csToolsSegmentation.removeSegmentationRepresentations(viewportIds[0]); + csToolsSegmentation.removeAllSegmentationRepresentations(); // const segmentationIds = getSegmentationIds(); @@ -751,208 +478,6 @@ function getSegmentationIds() { .map(x => x.segmentationId); } -async function addSegmentationsToState(segmentationId: string) { - // Create a segmentation of the same resolution as the source data - const derivedVolume = volumeLoader.createAndCacheDerivedLabelmapVolume( - volumeId, - { - volumeId: segmentationId - } - ); - - // Add the segmentations to state - csToolsSegmentation.addSegmentations([ - { - segmentationId, - representation: { - // The type of segmentation - type: csToolsEnums.SegmentationRepresentations.Labelmap, - // The actual segmentation data, in the case of labelmap this is a - // reference to the source volume of the segmentation. - data: { - volumeId: segmentationId - } - } - } - ]); - - // Add the segmentation representation to the viewport - await csToolsSegmentation.addSegmentationRepresentations(viewportIds[0], [ - { - segmentationId, - type: csToolsEnums.SegmentationRepresentations.Labelmap - } - ]); - - await csToolsSegmentation.addSegmentationRepresentations(viewportIds[1], [ - { - segmentationId, - type: csToolsEnums.SegmentationRepresentations.Labelmap - } - ]); - - await csToolsSegmentation.addSegmentationRepresentations(viewportIds[2], [ - { - segmentationId, - type: csToolsEnums.SegmentationRepresentations.Labelmap - } - ]); - - // - return derivedVolume; -} - -function generateMockMetadata(segmentIndex, color) { - const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB( - color.slice(0, 3).map(value => value / 255) - ).map(value => Math.round(value)); - - return { - SegmentedPropertyCategoryCodeSequence: { - CodeValue: "T-D0050", - CodingSchemeDesignator: "SRT", - CodeMeaning: "Tissue" - }, - SegmentNumber: segmentIndex.toString(), - SegmentLabel: "Tissue " + segmentIndex.toString(), - SegmentAlgorithmType: "SEMIAUTOMATIC", - SegmentAlgorithmName: "Slicer Prototype", - RecommendedDisplayCIELabValue, - SegmentedPropertyTypeCodeSequence: { - CodeValue: "T-D0050", - CodingSchemeDesignator: "SRT", - CodeMeaning: "Tissue" - } - }; -} - -function updateSegmentationDropdown(activeSegmentationId?) { - const dropdown = document.getElementById( - "ACTIVE_SEGMENTATION_DROPDOWN" - ) as HTMLSelectElement; - - dropdown.innerHTML = ""; - - const segmentationIds = getSegmentationIds(); - - // - if (segmentationIds.length) { - segmentationIds.forEach((segmentationId: string) => { - const option = document.createElement("option"); - option.value = segmentationId; - option.innerText = segmentationId; - dropdown.appendChild(option); - }); - - if (activeSegmentationId) { - dropdown.value = activeSegmentationId; - } - } - // - else { - const option = document.createElement("option"); - option.setAttribute("disabled", ""); - option.setAttribute("hidden", ""); - option.setAttribute("selected", ""); - option.innerText = "No active segmentation..."; - dropdown.appendChild(option); - } - - // - updateSegmentDropdown(); -} - -function updateSegmentDropdown() { - const dropdown = document.getElementById( - "ACTIVE_SEGMENT_DROPDOWN" - ) as HTMLSelectElement; - - dropdown.innerHTML = ""; - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - - // - if (!activeSegmentation) { - const option = document.createElement("option"); - option.setAttribute("disabled", ""); - option.setAttribute("hidden", ""); - option.setAttribute("selected", ""); - option.innerText = "No active segment..."; - dropdown.appendChild(option); - - return; - } - - // - const activeSegmentIndex = - csToolsSegmentation.segmentIndex.getActiveSegmentIndex( - activeSegmentation.segmentationId - ); - - const segmentIndices = - csToolsUtilities.segmentation.getUniqueSegmentIndices( - activeSegmentation.segmentationId - ); - - // - const optionDraw = function () { - const option = document.createElement("option"); - option.setAttribute("disabled", ""); - option.setAttribute("hidden", ""); - option.setAttribute("selected", ""); - option.innerText = "Draw or set segment index"; - dropdown.appendChild(option); - }; - - // - if (segmentIndices.length) { - if (!segmentIndices.includes(activeSegmentIndex)) { - optionDraw(); - } - - segmentIndices.forEach((segmentIndex: number) => { - const option = document.createElement("option"); - option.value = segmentIndex.toString(); - option.innerText = segmentIndex.toString(); - dropdown.appendChild(option); - }); - - if (segmentIndices.includes(activeSegmentIndex)) { - dropdown.value = activeSegmentIndex.toString(); - } - } - // - else { - optionDraw(); - } - - // - updateSegmentLabel(); -} - -function updateSegmentLabel() { - const label = document.getElementById( - "CURRENT_ACTIVE_SEGMENT_LABEL" - ) as HTMLSelectElement; - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - - const activeSegmentIndex = - csToolsSegmentation.segmentIndex.getActiveSegmentIndex( - activeSegmentation.segmentationId - ); - - label.innerHTML = "Current Active Segment: " + activeSegmentIndex; -} - function handleFileSelect(evt) { evt.stopPropagation(); evt.preventDefault(); @@ -979,17 +504,9 @@ async function run() { // Init Cornerstone and related libraries await initDemo(); - // - labelmapTools.toolMap.forEach(x => { - if (x.configuration?.preview) { - x.configuration.preview.enabled = false; - } - }); - // Define tool groups to add the segmentation display tool to toolGroup = ToolGroupManager.createToolGroup(toolGroupId); addManipulationBindings(toolGroup, { toolMap: labelmapTools.toolMap }); - // // Instantiate a rendering engine renderingEngine = new RenderingEngine(renderingEngineId); @@ -1027,14 +544,6 @@ async function run() { // renderingEngine.setViewports(viewportInputArray); - - // - eventTarget.addEventListener( - csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED, - function () { - updateSegmentDropdown(); - } - ); } run(); diff --git a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js index 36a92bd71a..e73b4969ba 100644 --- a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js +++ b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js @@ -228,7 +228,7 @@ function _createSegFromImages(images, isMultiframe, options) { * generateToolState - Given a set of cornerstoneTools imageIds and a Segmentation buffer, * derive cornerstoneTools toolState and brush metadata. * - * @param {string[]} imageIds - An array of the imageIds. + * @param {string[]} referencedImageIds - An array for referenced image imageIds. * @param {ArrayBuffer} arrayBuffer - The SEG arrayBuffer. * @param {*} metadataProvider. * @param {obj} options - Options object. @@ -240,7 +240,7 @@ function _createSegFromImages(images, isMultiframe, options) { * (available only for the overlapping case). */ async function generateToolState( - imageIds, + referencedImageIds, arrayBuffer, metadataProvider, options @@ -260,12 +260,12 @@ async function generateToolState( const imagePlaneModule = metadataProvider.get( "imagePlaneModule", - imageIds[0] + referencedImageIds[0] ); const generalSeriesModule = metadataProvider.get( "generalSeriesModule", - imageIds[0] + referencedImageIds[0] ); const SeriesInstanceUID = generalSeriesModule.seriesInstanceUID; @@ -326,14 +326,18 @@ async function generateToolState( const orientation = checkOrientation( multiframe, validOrientations, - [imagePlaneModule.rows, imagePlaneModule.columns, imageIds.length], + [ + imagePlaneModule.rows, + imagePlaneModule.columns, + referencedImageIds.length + ], tolerance ); // Pre-compute the sop UID to imageId index map so that in the for loop // we don't have to call metadataProvider.get() for each imageId over // and over again. - const sopUIDImageIdIndexMap = imageIds.reduce((acc, imageId) => { + const sopUIDImageIdIndexMap = referencedImageIds.reduce((acc, imageId) => { const { sopInstanceUID } = metadataProvider.get( "generalImageModule", imageId @@ -347,7 +351,7 @@ async function generateToolState( overlapping = checkSEGsOverlapping( pixelDataChunks, multiframe, - imageIds, + referencedImageIds, validOrientations, metadataProvider, tolerance, @@ -388,13 +392,15 @@ async function generateToolState( const segmentsOnFrame = []; const arrayBufferLength = - sliceLength * imageIds.length * TypedArrayConstructor.BYTES_PER_ELEMENT; + sliceLength * + referencedImageIds.length * + TypedArrayConstructor.BYTES_PER_ELEMENT; const labelmapBufferArray = []; labelmapBufferArray[0] = new ArrayBuffer(arrayBufferLength); // Pre-compute the indices and metadata so that we don't have to call // a function for each imageId in the for loop. - const imageIdMaps = imageIds.reduce( + const imageIdMaps = referencedImageIds.reduce( (acc, curr, index) => { acc.indices[curr] = index; acc.metadata[curr] = metadataProvider.get("instance", curr); @@ -415,7 +421,7 @@ async function generateToolState( labelmapBufferArray, pixelDataChunks, multiframe, - imageIds, + referencedImageIds, validOrientations, metadataProvider, tolerance, @@ -435,7 +441,7 @@ async function generateToolState( imageIdIndexBufferIndex, multiframe, metadataProvider, - imageIds + referencedImageIds ); centroidXYZ.set(segmentIndex, centroids); diff --git a/packages/tools/src/utilities/planar/filterAnnotationsForDisplay.ts b/packages/tools/src/utilities/planar/filterAnnotationsForDisplay.ts index ecb329b0d6..1954bc340c 100644 --- a/packages/tools/src/utilities/planar/filterAnnotationsForDisplay.ts +++ b/packages/tools/src/utilities/planar/filterAnnotationsForDisplay.ts @@ -36,6 +36,10 @@ export default function filterAnnotationsForDisplay( // 1. Get the currently displayed imageId from the StackViewport const imageId = viewport.getCurrentImageId(); + if (!imageId) { + return []; + } + // 2. remove the dataLoader scheme since it might be an annotation that was // created on the volumeViewport initially and has the volumeLoader scheme // but shares the same imageId From 238dd51eef6ba6a6287572b0050f8e8e41292ae4 Mon Sep 17 00:00:00 2001 From: sedghi Date: Thu, 5 Dec 2024 21:35:36 -0500 Subject: [PATCH 2/2] refactor(segmentation): streamline segmentation handling and improve toolbar functionality - Consolidated segmentation-related functions into a new utils module for better organization. - Updated toolbar buttons to utilize the new state management for DICOM and SEG file handling. - Enhanced user instructions for loading and exporting segmentation data. - Introduced a toggle for handling overlapping segments during segmentation loading. - Improved event handling for file selection and drag-and-drop functionality. - Refactored rendering engine and viewport setup for clarity and maintainability. --- .../examples/segmentationStack/index.ts | 1088 ++--------------- .../examples/segmentationVolume/index.ts | 475 ++----- .../examples/segmentationVolume/utils.ts | 249 ++++ .../adapters/Cornerstone/Segmentation_4X.js | 4 +- .../eventListeners/mouse/mouseMoveListener.ts | 5 + .../segmentation/imageChangeEventListener.ts | 12 +- 6 files changed, 505 insertions(+), 1328 deletions(-) create mode 100644 packages/adapters/examples/segmentationVolume/utils.ts diff --git a/packages/adapters/examples/segmentationStack/index.ts b/packages/adapters/examples/segmentationStack/index.ts index 422490e986..0c7f839994 100644 --- a/packages/adapters/examples/segmentationStack/index.ts +++ b/packages/adapters/examples/segmentationStack/index.ts @@ -2,65 +2,45 @@ import { api } from "dicomweb-client"; import * as cornerstone from "@cornerstonejs/core"; import * as cornerstoneTools from "@cornerstonejs/tools"; -import * as cornerstoneDicomImageLoader from "@cornerstonejs/dicom-image-loader"; import * as cornerstoneAdapters from "@cornerstonejs/adapters"; import { dicomMap } from "./demo"; import { - addBrushSizeSlider, addButtonToToolbar, addDropdownToToolbar, - addLabelToToolbar, addManipulationBindings, + addToggleButtonToToolbar, addUploadToToolbar, createImageIdsAndCacheMetaData, - createInfoSection, initDemo, labelmapTools, setTitleAndDescription } from "../../../../utils/demo/helpers"; -import dcmjs from "dcmjs"; +import { BrushTool } from "@cornerstonejs/tools"; // This is for debugging purposes console.warn( "Click on index.ts to open source code for this example --------->" ); -const { - Enums: csEnums, - RenderingEngine, - cache, - eventTarget, - imageLoader, - metaData, - utilities: csUtilities -} = cornerstone; +const { Enums: csEnums, RenderingEngine, utilities: csUtilities } = cornerstone; const { ViewportType } = csEnums; - -const { - Enums: csToolsEnums, - ToolGroupManager, - segmentation: csToolsSegmentation, - utilities: csToolsUtilities -} = cornerstoneTools; -const { MouseBindings } = csToolsEnums; - -const { wadouri } = cornerstoneDicomImageLoader; - -const { adaptersSEG, helpers } = cornerstoneAdapters; -const { Cornerstone3D } = adaptersSEG; -const { downloadDICOMData } = helpers; - -// -let renderingEngine; -const renderingEngineId = "MY_RENDERING_ENGINE_ID"; -let toolGroup; -const toolGroupId = "MY_TOOL_GROUP_ID"; -const viewportIds = ["CT_STACK"]; -let imageIds: string[] = []; - +import { + readDicom, + loadDicom, + readSegmentation, + loadSegmentation, + exportSegmentation, + restart, + getSegmentationIds, + handleFileSelect, + handleDragOver +} from "../segmentationVolume/utils"; + +const referenceImageIds: string[] = []; +const segImageIds: string[] = []; // ======== Set up page ======== // setTitleAndDescription( @@ -68,22 +48,8 @@ setTitleAndDescription( "Here we demonstrate how to import or export a DICOM SEG from a Cornerstone3D stack." ); -// TODO -const descriptionContainer = document.getElementById( - "demo-description-container" -); - -const warn = document.createElement("div"); -descriptionContainer.prepend(warn); - -const textA = document.createElement("p"); -textA.style.color = "red"; -textA.innerHTML = - "Warning:
Load or import into dicom or segmentation, just one frame. Several frames are not yet completed.
When exporting segmentation, also just one frame."; -warn.appendChild(textA); -// END TODO - const size = "500px"; +const skipOverlapping = false; const demoToolbar = document.getElementById("demo-toolbar"); @@ -95,18 +61,6 @@ const group2 = document.createElement("div"); group2.style.marginBottom = "10px"; demoToolbar.appendChild(group2); -const group3 = document.createElement("div"); -group3.style.marginBottom = "10px"; -demoToolbar.appendChild(group3); - -const group4 = document.createElement("div"); -group4.style.marginBottom = "10px"; -demoToolbar.appendChild(group4); - -const group5 = document.createElement("div"); -group5.style.marginBottom = "10px"; -demoToolbar.appendChild(group5); - const content = document.getElementById("content"); const viewportGrid = document.createElement("div"); @@ -119,975 +73,183 @@ viewportGrid.addEventListener("drop", handleFileSelect, false); const element1 = document.createElement("div"); element1.style.width = size; element1.style.height = size; +const element2 = document.createElement("div"); +element2.style.width = size; +element2.style.height = size; +const element3 = document.createElement("div"); +element3.style.width = size; +element3.style.height = size; // Disable right click context menu so we can have right click tools element1.oncontextmenu = e => e.preventDefault(); +element2.oncontextmenu = e => e.preventDefault(); +element3.oncontextmenu = e => e.preventDefault(); viewportGrid.appendChild(element1); +viewportGrid.appendChild(element2); +viewportGrid.appendChild(element3); content.appendChild(viewportGrid); - -createInfoSection(content) - .addInstruction('You can try configuring "dev" in the console:') - .openNestedSection() - .addInstruction("fetchDicom") - .closeNestedSection(); - -// ============================= // - -let devConfig = { - ...dicomMap.values().next().value -}; -const dev = { - get getConfig() { - return devConfig; - }, - set setConfig(obj: object) { - devConfig = csUtilities.deepMerge(devConfig, obj); - } +const info = document.createElement("div"); +content.appendChild(info); + +function addInstruction(text) { + const instructions = document.createElement("p"); + instructions.innerText = `- ${text}`; + info.appendChild(instructions); +} + +const state = { + renderingEngine: null, + renderingEngineId: "MY_RENDERING_ENGINE_ID", + toolGroup: null, + toolGroupId: "MY_TOOL_GROUP_ID", + viewportIds: ["CT_AXIAL"], + segmentationId: "LOAD_SEG_ID:" + cornerstone.utilities.uuidv4(), + referenceImageIds: [], + segImageIds: [], + skipOverlapping: false, + devConfig: { ...dicomMap.values().next().value } }; -(window as any).dev = dev; - -// ============================= // - -async function fetchDicom() { - restart(); - - // Get Cornerstone imageIds for the source data and fetch metadata into RAM - imageIds = await createImageIdsAndCacheMetaData(dev.getConfig.fetchDicom); - - // TODO - if ( - dev.getConfig.fetchDicom.StudyInstanceUID === - "1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046" - ) { - imageIds = imageIds.slice(50, 51); - } - - // - imageIds = imageIds.slice(0, 1); - - await loadDicom(imageIds); -} -async function readDicom(files: FileList) { - restart(); - - // TODO - const arr = [files[0]]; - - imageIds = []; - - for (const file of arr) { - const imageId = wadouri.fileManager.add(file); +viewportGrid.addEventListener("dragover", evt => handleDragOver(evt), false); +viewportGrid.addEventListener( + "drop", + evt => handleFileSelect(evt, state), + false +); - await imageLoader.loadAndCacheImage(imageId); +function loadDicom() { + restart(state); - imageIds.push(imageId); - } + const { toolGroup, viewportIds, renderingEngineId, renderingEngine } = + state; - await loadDicom(imageIds); -} - -async function loadDicom(imageIds: string[]) { - // toolGroup.addViewport(viewportIds[0], renderingEngineId); - // - const viewport = renderingEngine.getViewport(viewportIds[0]); - // - await viewport.setStack(imageIds, 0); - - // Generate segmentation id - const newSegmentationId = "NEW_SEG_ID:" + csUtilities.uuidv4(); - // Add some segmentations based on the source data stack - await addSegmentationsToState(newSegmentationId); - // Update the dropdown - updateSegmentationDropdown(); - - // Render the image - renderingEngine.renderViewports(viewportIds); -} - -async function fetchSegmentation() { - if (!imageIds.length) { - return; - } - - const configSeg = dev.getConfig.fetchSegmentation; - - // @ts-expect-error - const client = new api.DICOMwebClient({ - url: configSeg.wadoRsRoot - }); - const arrayBuffer = await client.retrieveInstance({ - studyInstanceUID: configSeg.StudyInstanceUID, - seriesInstanceUID: configSeg.SeriesInstanceUID, - sopInstanceUID: configSeg.SOPInstanceUID - }); - - // - await loadSegmentation(arrayBuffer); -} - -async function importSegmentation(files: FileList) { - if (!imageIds.length) { - return; - } - - for (const file of files) { - await readSegmentation(file); - } -} - -async function readSegmentation(file: File) { - const imageId = wadouri.fileManager.add(file); - - const image = await imageLoader.loadAndCacheImage(imageId); - - if (!image) { - return; - } - - const instance = metaData.get("instance", imageId); - - if (instance.Modality !== "SEG") { - console.error("This is not segmentation: " + file.name); - return; - } - - const arrayBuffer = image.data.byteArray.buffer; - - loadSegmentation(arrayBuffer); -} - -async function loadSegmentation(arrayBuffer: ArrayBuffer) { - // Generate segmentation id - const newSegmentationId = "LOAD_SEG_ID:" + csUtilities.uuidv4(); - - // Add some segmentations based on the source data stack - const derivedImages = await addSegmentationsToState(newSegmentationId); - // Update the dropdown - updateSegmentationDropdown(newSegmentationId); - - // - const generateToolState = - await Cornerstone3D.Segmentation.generateToolState( - imageIds, - arrayBuffer, - metaData - ); - - // - derivedImages.forEach(image => { - const cachedImage = cache.getImage(image.imageId); - - if (cachedImage) { - const pixelData = cachedImage.getPixelData(); - - // - pixelData.set( - new Uint8Array(generateToolState.labelmapBufferArray[0]) - ); - } - }); - - // TODO - setTimeout(function () { - // - csToolsSegmentation.triggerSegmentationEvents.triggerSegmentationDataModified( - newSegmentationId - ); - }, 200); -} - -function exportSegmentation() { - // - const segmentationIds = getSegmentationIds(); - // - if (!segmentationIds.length) { - return; - } - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - // Get active segmentation representation - - if (!activeSegmentation) { - return; - } - - // - const labelmap = activeSegmentation.representationData[ - csToolsEnums.SegmentationRepresentations.Labelmap - ] as cornerstoneTools.Types.LabelmapToolOperationDataStack; + const viewport = state.renderingEngine.getStackViewport(viewportIds[0]); - // - if (labelmap.imageIds) { - // - labelmap.imageIds.forEach(async (derivedImagesId: string) => { - // - await imageLoader.loadAndCacheImage(derivedImagesId); - // - const cacheImage = cache.getImage(derivedImagesId); + viewport.setStack(state.referenceImageIds); + cornerstoneTools.utilities.stackContextPrefetch.enable(element1); - // - const cacheSegmentationImage = cache.getImage(derivedImagesId); - - // TODO - // generateLabelMaps2DFrom3D required "scalarData" and "dimensions" - cacheSegmentationImage.scalarData = - cacheSegmentationImage.getPixelData(); - cacheSegmentationImage.dimensions = [ - cacheSegmentationImage.columns, - cacheSegmentationImage.rows, - 1 - ]; - - // - const labelmapData = - Cornerstone3D.Segmentation.generateLabelMaps2DFrom3D( - cacheSegmentationImage - ); - - // Generate fake metadata as an example - labelmapData.metadata = []; - labelmapData.segmentsOnLabelmap.forEach((segmentIndex: number) => { - const color = - csToolsSegmentation.config.color.getSegmentIndexColor( - viewportIds[0], - activeSegmentation.segmentationId, - segmentIndex - ); - - const segmentMetadata = generateMockMetadata( - segmentIndex, - color - ); - labelmapData.metadata[segmentIndex] = segmentMetadata; - }); - - // TODO - // https://github.com/cornerstonejs/cornerstone3D/issues/1059#issuecomment-2181016046 - const generatedSegmentation = - Cornerstone3D.Segmentation.generateSegmentation( - [cacheImage, cacheImage], - labelmapData, - metaData - ); - - downloadDICOMData(generatedSegmentation.dataset, "mySEG.dcm"); - }); - } -} - -async function addActiveSegmentation() { - if (!imageIds.length) { - return; - } - - // Generate segmentation id - const newSegmentationId = "NEW_SEG_ID:" + csUtilities.uuidv4(); - // Add some segmentations based on the source data stack - await addSegmentationsToState(newSegmentationId); - // Update the dropdown - updateSegmentationDropdown(newSegmentationId); -} - -function removeActiveSegmentation() { - // - const segmentationIds = getSegmentationIds(); - // - if (!segmentationIds.length) { - return; - } - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - // Get active segmentation representation - - if (!activeSegmentation) { - return; - } - - // - csToolsSegmentation.removeSegmentationRepresentations(viewportIds[0], { - segmentationId: activeSegmentation.segmentationId - }); - - // - csToolsSegmentation.state.removeSegmentation( - activeSegmentation.segmentationId - ); - - // - const labelmap = activeSegmentation.representationData[ - csToolsEnums.SegmentationRepresentations.Labelmap - ] as cornerstoneTools.Types.LabelmapToolOperationDataStack; - - // - if (labelmap.imageIds) { - // - labelmap.imageIds.forEach((derivedImagesId: string) => { - // - cache.removeImageLoadObject(derivedImagesId); - }); - } - - // Update the dropdown - updateSegmentationDropdown(); -} - -function plusActiveSegment() { - if (!imageIds.length) { - return; - } - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - // - if (!activeSegmentation) { - return; - } - - const activeSegmentIndex = - csToolsSegmentation.segmentIndex.getActiveSegmentIndex( - activeSegmentation.segmentationId - ); - - if (activeSegmentIndex + 1 <= 255) { - csToolsSegmentation.segmentIndex.setActiveSegmentIndex( - activeSegmentation.segmentationId, - activeSegmentIndex + 1 - ); - - // Update the dropdown - updateSegmentDropdown(); - } -} - -function minusActiveSegment() { - if (!imageIds.length) { - return; - } - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - // - if (!activeSegmentation) { - return; - } - - const activeSegmentIndex = - csToolsSegmentation.segmentIndex.getActiveSegmentIndex( - activeSegmentation.segmentationId - ); - - if (activeSegmentIndex - 1 >= 1) { - csToolsSegmentation.segmentIndex.setActiveSegmentIndex( - activeSegmentation.segmentationId, - activeSegmentIndex - 1 - ); - - // Update the dropdown - updateSegmentDropdown(); - } -} - -function removeActiveSegment() { - if (!imageIds.length) { - return; - } - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - // - if (!activeSegmentation) { - return; - } - - // - const labelmap = activeSegmentation.representationData[ - csToolsEnums.SegmentationRepresentations.Labelmap - ] as cornerstoneTools.Types.LabelmapToolOperationDataStack; - - // - const modifiedFrames = new Set(); - - const activeSegmentIndex = - csToolsSegmentation.segmentIndex.getActiveSegmentIndex( - activeSegmentation.segmentationId - ); - - // - if (labelmap.imageIds) { - // - labelmap.imageIds.forEach((derivedImagesId: string) => { - // Get image - const image = cache.getImage(derivedImagesId); - - // Get pixel data - const pixelData = image.getPixelData(); - - // - const frameLength = image.columns * image.rows; - const numFrames = 1; - - // - let index = 0; - - // - for (let f = 0; f < numFrames; f++) { - // - for (let p = 0; p < frameLength; p++) { - if (pixelData[index] === activeSegmentIndex) { - pixelData[index] = 0; - - modifiedFrames.add(f); - } - - index++; - } - } - }); - } - - // - const modifiedFramesArray = Array.from(modifiedFrames); - - // Event trigger (SEGMENTATION_DATA_MODIFIED) - csToolsSegmentation.triggerSegmentationEvents.triggerSegmentationDataModified( - activeSegmentation.segmentationId, - modifiedFramesArray - ); - - // Update the dropdown - updateSegmentDropdown(); + renderingEngine.render(); } // ============================= // - -// TODO -const inputConfig = { - attr: { - multiple: false - } -}; - -addDropdownToToolbar({ - id: "DICOM_DROPDOWN", - style: { - marginRight: "10px" - }, - options: { map: dicomMap, defaultIndex: 0 }, - onSelectedValueChange: (key, value) => { - dev.setConfig = value; - }, - container: group1 -}); - addButtonToToolbar({ id: "LOAD_DICOM", title: "Load DICOM", - style: { - marginRight: "5px" + onClick: async () => { + state.referenceImageIds = await createImageIdsAndCacheMetaData( + state.devConfig.fetchDicom + ); + + loadDicom(); }, - onClick: fetchDicom, container: group1 }); addButtonToToolbar({ id: "LOAD_SEGMENTATION", title: "Load SEG", - style: { - marginRight: "5px" + onClick: async () => { + if (!state.referenceImageIds.length) { + alert("load source dicom first"); + return; + } + + const configSeg = state.devConfig.fetchSegmentation; + const client = new api.DICOMwebClient({ + url: configSeg.wadoRsRoot + }); + const arrayBuffer = await client.retrieveInstance({ + studyInstanceUID: configSeg.StudyInstanceUID, + seriesInstanceUID: configSeg.SeriesInstanceUID, + sopInstanceUID: configSeg.SOPInstanceUID + }); + + await loadSegmentation(arrayBuffer, state); }, - onClick: fetchSegmentation, container: group1 }); addUploadToToolbar({ id: "IMPORT_DICOM", title: "Import DICOM", - style: { - marginRight: "5px" + onChange: (files: FileList) => { + readDicom(files, state); + loadDicom(); }, - onChange: readDicom, - container: group2, - input: inputConfig + container: group2 }); addUploadToToolbar({ id: "IMPORT_SEGMENTATION", title: "Import SEG", - style: { - marginRight: "5px" + onChange: async (files: FileList) => { + for (const file of files) { + await readSegmentation(file, state); + } }, - onChange: importSegmentation, - container: group2, - input: inputConfig + container: group2 }); addButtonToToolbar({ id: "EXPORT_SEGMENTATION", title: "Export SEG", - onClick: exportSegmentation, - container: group2 -}); - -addDropdownToToolbar({ - id: "LABELMAP_TOOLS_DROPDOWN", - style: { - width: "150px", - marginRight: "10px" - }, - options: { map: labelmapTools.toolMap }, - onSelectedValueChange: nameAsStringOrNumber => { - const tool = String(nameAsStringOrNumber); - - const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); - - if (!toolGroup) { - return; - } - - // Set the currently active tool disabled - const toolName = toolGroup.getActivePrimaryMouseButtonTool(); - - if (toolName) { - toolGroup.setToolDisabled(toolName); - } - - toolGroup.setToolActive(tool, { - bindings: [{ mouseButton: MouseBindings.Primary }] - }); - }, - labelText: "Tools: ", - container: group3 -}); - -addBrushSizeSlider({ - toolGroupId: toolGroupId, - container: group3 -}); - -addDropdownToToolbar({ - id: "ACTIVE_SEGMENTATION_DROPDOWN", - style: { - width: "200px", - marginRight: "10px" - }, - options: { values: [], defaultValue: "" }, - placeholder: "No active segmentation...", - onSelectedValueChange: nameAsStringOrNumber => { - const segmentationId = String(nameAsStringOrNumber); - - csToolsSegmentation.activeSegmentation.setActiveSegmentation( - viewportIds[0], - segmentationId - ); - - // Update the dropdown - updateSegmentationDropdown(segmentationId); - }, - labelText: "Set Active Segmentation: ", - container: group4 + onClick: () => exportSegmentation(state) }); -addButtonToToolbar({ - id: "ADD_ACTIVE_SEGMENTATION", - style: { - marginRight: "10px" +addToggleButtonToToolbar({ + id: "SKIP_OVERLAPPING", + title: "Override Overlapping Segments", + onClick: () => { + state.skipOverlapping = !state.skipOverlapping; }, - title: "Add Active Segmentation", - onClick: addActiveSegmentation, - container: group4 -}); - -addButtonToToolbar({ - id: "REMOVE_ACTIVE_SEGMENTATION", - title: "Remove Active Segmentation", - onClick: removeActiveSegmentation, - container: group4 -}); - -addLabelToToolbar({ - id: "CURRENT_ACTIVE_SEGMENT_LABEL", - title: "Current Active Segment: 1", - style: { - marginRight: "10px" - }, - container: group5 -}); - -addButtonToToolbar({ - id: "PLUS_ACTIVE_SEGMENT", - attr: { - title: "Plus Active Segment" - }, - style: { - marginRight: "10px" - }, - title: "+", - onClick: plusActiveSegment, - container: group5 -}); - -addButtonToToolbar({ - id: "MINUS_ACTIVE_SEGMENT", - attr: { - title: "Minus Active Segment" - }, - style: { - marginRight: "10px" - }, - title: "-", - onClick: minusActiveSegment, - container: group5 -}); - -addDropdownToToolbar({ - id: "ACTIVE_SEGMENT_DROPDOWN", - style: { - width: "200px", - marginRight: "10px" - }, - options: { values: [], defaultValue: "" }, - placeholder: "No active segment...", - onSelectedValueChange: nameAsStringOrNumber => { - const segmentIndex = Number(nameAsStringOrNumber); - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - - csToolsSegmentation.segmentIndex.setActiveSegmentIndex( - activeSegmentation.segmentationId, - segmentIndex - ); - - // Update the dropdown - updateSegmentDropdown(); - }, - labelText: "Set Active Segment: ", - container: group5 -}); - -addButtonToToolbar({ - id: "REMOVE_ACTIVE_SEGMENT", - title: "Remove Active Segment", - onClick: removeActiveSegment, - container: group5 + container: group1 }); - // ============================= // -// If you import the dicom again, before clearing the cache or starting from scratch -function restart() { - if (!imageIds.length) { - return; - } - - // - imageIds.forEach(imageId => { - if (cache.getImage(imageId)) { - cache.removeImageLoadObject(imageId); - } - }); - - // - csToolsSegmentation.removeSegmentationRepresentations(viewportIds[0]); - - // - const segmentations = csToolsSegmentation.state.getSegmentations(); - // - segmentations.forEach(segmentation => { - csToolsSegmentation.state.removeSegmentation( - segmentation.segmentationId - ); - - // - const labelmap = segmentation.representationData[ - csToolsEnums.SegmentationRepresentations.Labelmap - ] as cornerstoneTools.Types.LabelmapToolOperationDataStack; - - // - if (labelmap.imageIds) { - // - labelmap.imageIds.forEach(derivedImagesId => { - cache.removeImageLoadObject(derivedImagesId); - }); - } - }); -} - -function getSegmentationIds(): string[] { - return csToolsSegmentation.state - .getSegmentations() - .map(x => x.segmentationId); -} - -async function addSegmentationsToState(segmentationId: string) { - // - const derivedImages = - imageLoader.createAndCacheDerivedLabelmapImages(imageIds); - - // Add the segmentations to state - csToolsSegmentation.addSegmentations([ - { - segmentationId, - representation: { - type: csToolsEnums.SegmentationRepresentations.Labelmap, - data: { - imageIds: derivedImages.map(x => x.imageId) - } - } - } - ]); - - // Add the segmentation representation to the toolgroup - await csToolsSegmentation.addSegmentationRepresentations(viewportIds[0], [ - { - segmentationId, - type: csToolsEnums.SegmentationRepresentations.Labelmap - } - ]); - - // - return derivedImages; -} - -function generateMockMetadata(segmentIndex, color) { - const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB( - color.slice(0, 3).map(value => value / 255) - ).map(value => Math.round(value)); - - return { - SegmentedPropertyCategoryCodeSequence: { - CodeValue: "T-D0050", - CodingSchemeDesignator: "SRT", - CodeMeaning: "Tissue" - }, - SegmentNumber: segmentIndex.toString(), - SegmentLabel: "Tissue " + segmentIndex.toString(), - SegmentAlgorithmType: "SEMIAUTOMATIC", - SegmentAlgorithmName: "Slicer Prototype", - RecommendedDisplayCIELabValue, - SegmentedPropertyTypeCodeSequence: { - CodeValue: "T-D0050", - CodingSchemeDesignator: "SRT", - CodeMeaning: "Tissue" - } - }; -} - -function updateSegmentationDropdown(activeSegmentationId?: string) { - const dropdown = document.getElementById( - "ACTIVE_SEGMENTATION_DROPDOWN" - ) as HTMLSelectElement; - - dropdown.innerHTML = ""; - - // Get segmentationIds - const segmentationIds = getSegmentationIds(); - - // - if (segmentationIds.length) { - segmentationIds.forEach((segmentationId: string) => { - const option = document.createElement("option"); - option.value = segmentationId; - option.innerText = segmentationId; - dropdown.appendChild(option); - }); - - if (activeSegmentationId) { - dropdown.value = activeSegmentationId; - } - } - // - else { - const option = document.createElement("option"); - option.setAttribute("disabled", ""); - option.setAttribute("hidden", ""); - option.setAttribute("selected", ""); - option.innerText = "No active segmentation..."; - dropdown.appendChild(option); - } - - // - updateSegmentDropdown(); -} - -function updateSegmentDropdown() { - const dropdown = document.getElementById( - "ACTIVE_SEGMENT_DROPDOWN" - ) as HTMLSelectElement; - - dropdown.innerHTML = ""; - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - - // - if (!activeSegmentation) { - const option = document.createElement("option"); - option.setAttribute("disabled", ""); - option.setAttribute("hidden", ""); - option.setAttribute("selected", ""); - option.innerText = "No active segment..."; - dropdown.appendChild(option); - - return; - } - - // - const activeSegmentIndex = - csToolsSegmentation.segmentIndex.getActiveSegmentIndex( - activeSegmentation.segmentationId - ); - - const segmentIndices = - csToolsUtilities.segmentation.getUniqueSegmentIndices( - activeSegmentation.segmentationId - ); - - // - const optionDraw = function () { - const option = document.createElement("option"); - option.setAttribute("disabled", ""); - option.setAttribute("hidden", ""); - option.setAttribute("selected", ""); - option.innerText = "Draw or set segment index"; - dropdown.appendChild(option); - }; - - // - if (segmentIndices.length) { - if (!segmentIndices.includes(activeSegmentIndex)) { - optionDraw(); - } - - segmentIndices.forEach((segmentIndex: number) => { - const option = document.createElement("option"); - option.value = segmentIndex.toString(); - option.innerText = segmentIndex.toString(); - dropdown.appendChild(option); - }); - - if (segmentIndices.includes(activeSegmentIndex)) { - dropdown.value = activeSegmentIndex.toString(); - } - } - // - else { - optionDraw(); - } - - // - updateSegmentLabel(); -} - -function updateSegmentLabel() { - const label = document.getElementById( - "CURRENT_ACTIVE_SEGMENT_LABEL" - ) as HTMLSelectElement; - - // Get active segmentation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - toolGroupId - ); - - const activeSegmentIndex = - csToolsSegmentation.segmentIndex.getActiveSegmentIndex( - activeSegmentation.segmentationId - ); - - label.innerHTML = "Current Active Segment: " + activeSegmentIndex; -} - -function handleFileSelect(evt) { - evt.stopPropagation(); - evt.preventDefault(); - - // - const files = evt.dataTransfer.files; - - // - readDicom(files); -} - -function handleDragOver(evt) { - evt.stopPropagation(); - evt.preventDefault(); - evt.dataTransfer.dropEffect = "copy"; -} - // ============================= // /** * Runs the demo */ async function run() { - // Init Cornerstone and related libraries await initDemo(); - // - labelmapTools.toolMap.forEach(x => { - if (x.configuration?.preview) { - x.configuration.preview.enabled = false; - } + state.toolGroup = cornerstoneTools.ToolGroupManager.createToolGroup( + state.toolGroupId + ); + addManipulationBindings(state.toolGroup, { + toolMap: labelmapTools.toolMap }); - // Define tool groups to add the segmentation display tool to - toolGroup = ToolGroupManager.createToolGroup(toolGroupId); - addManipulationBindings(toolGroup, { toolMap: labelmapTools.toolMap }); - // - // Instantiate a rendering engine - renderingEngine = new RenderingEngine(renderingEngineId); + cornerstoneTools.addTool(BrushTool); + + state.toolGroup.addToolInstance("CircularBrush", BrushTool.toolName, { + activeStrategy: "FILL_INSIDE_CIRCLE" + }); + + state.toolGroup.setToolActive("CircularBrush", { + bindings: [ + { mouseButton: cornerstoneTools.Enums.MouseBindings.Primary } + ] + }); + + state.renderingEngine = new cornerstone.RenderingEngine( + state.renderingEngineId + ); - // Create the viewports const viewportInputArray = [ { - viewportId: viewportIds[0], - type: ViewportType.STACK, - element: element1, - defaultOptions: { - background: [0.2, 0, 0.2] - } + viewportId: state.viewportIds[0], + type: cornerstone.Enums.ViewportType.STACK, + element: element1 } ]; - // - renderingEngine.setViewports(viewportInputArray); - - // - eventTarget.addEventListener( - csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED, - function () { - updateSegmentDropdown(); - } - ); + state.renderingEngine.setViewports(viewportInputArray); } run(); diff --git a/packages/adapters/examples/segmentationVolume/index.ts b/packages/adapters/examples/segmentationVolume/index.ts index 4398d92213..873452c7ce 100644 --- a/packages/adapters/examples/segmentationVolume/index.ts +++ b/packages/adapters/examples/segmentationVolume/index.ts @@ -2,68 +2,37 @@ import { api } from "dicomweb-client"; import * as cornerstone from "@cornerstonejs/core"; import * as cornerstoneTools from "@cornerstonejs/tools"; -import * as cornerstoneDicomImageLoader from "@cornerstonejs/dicom-image-loader"; -import * as cornerstoneAdapters from "@cornerstonejs/adapters"; import { dicomMap } from "./demo"; import { addButtonToToolbar, - addDropdownToToolbar, addManipulationBindings, addToggleButtonToToolbar, addUploadToToolbar, createImageIdsAndCacheMetaData, - createInfoSection, initDemo, labelmapTools, setTitleAndDescription } from "../../../../utils/demo/helpers"; -import dcmjs from "dcmjs"; +import { BrushTool } from "@cornerstonejs/tools"; // This is for debugging purposes console.warn( "Click on index.ts to open source code for this example --------->" ); -const { - Enums: csEnums, - RenderingEngine, - cache, - imageLoader, - metaData, - setVolumesForViewports, - utilities: csUtilities, - volumeLoader -} = cornerstone; -const { ViewportType } = csEnums; - -const { - Enums: csToolsEnums, - ToolGroupManager, - segmentation: csToolsSegmentation -} = cornerstoneTools; - -const { wadouri } = cornerstoneDicomImageLoader; - -const { adaptersSEG, helpers } = cornerstoneAdapters; -const { Cornerstone3D } = adaptersSEG; -const { downloadDICOMData } = helpers; - -// -let renderingEngine; -const renderingEngineId = "MY_RENDERING_ENGINE_ID"; -let toolGroup; -const toolGroupId = "MY_TOOL_GROUP_ID"; -const viewportIds = ["CT_AXIAL", "CT_SAGITTAL", "CT_CORONAL"]; -const volumeLoaderScheme = "cornerstoneStreamingImageVolume"; -let volumeId; -const segmentationId = "LOAD_SEG_ID:" + csUtilities.uuidv4(); - -let referenceImageIds: string[] = []; -const segImageIds: string[] = []; -// ======== Set up page ======== // +const { utilities: csUtilities } = cornerstone; +import { + readDicom, + readSegmentation, + loadSegmentation, + exportSegmentation, + handleFileSelect, + handleDragOver, + restart +} from "../segmentationVolume/utils"; setTitleAndDescription( "DICOM SEG VOLUME", @@ -71,7 +40,6 @@ setTitleAndDescription( ); const size = "500px"; -let skipOverlapping = false; const demoToolbar = document.getElementById("demo-toolbar"); @@ -112,266 +80,93 @@ viewportGrid.appendChild(element2); viewportGrid.appendChild(element3); content.appendChild(viewportGrid); +const info = document.createElement("div"); +content.appendChild(info); +function addInstruction(text) { + const instructions = document.createElement("p"); + instructions.innerText = `- ${text}`; + info.appendChild(instructions); +} -createInfoSection(content) - .addInstruction("Viewports:") - .openNestedSection() - .addInstruction("Axial | Sagittal | Coronal") - .closeNestedSection(); - -// ============================= // +addInstruction( + "Load a source DICOM volume first, either using the 'Load DICOM' button or by dragging and dropping multiple DICOM files onto the viewports." +); +addInstruction( + "Once a volume is loaded, you can import a DICOM SEG file by clicking 'Load SEG' " +); +addInstruction( + "Use the brush tool to edit segmentation labels on the displayed volume." +); +addInstruction( + "The 'Override Overlapping Segments' toggle allows you to choose how overlapping segments are handled during load" +); +addInstruction( + "After making changes, click 'Export SEG' to download the updated segmentation as a DICOM SEG file." +); +addInstruction( + "You can also upload local DICOM images or SEG files by using the 'Import DICOM' and 'Import SEG' buttons." +); -let devConfig = { - ...dicomMap.values().next().value +const state = { + renderingEngine: null, + renderingEngineId: "MY_RENDERING_ENGINE_ID", + toolGroup: null, + toolGroupId: "MY_TOOL_GROUP_ID", + viewportIds: ["CT_AXIAL", "CT_SAGITTAL", "CT_CORONAL"], + volumeId: "", + segmentationId: "LOAD_SEG_ID:" + cornerstone.utilities.uuidv4(), + referenceImageIds: [], + skipOverlapping: false, + segImageIds: [], + devConfig: { ...dicomMap.values().next().value } }; -const dev = { - get getConfig() { - return devConfig; - }, - set setConfig(obj: object) { - devConfig = csUtilities.deepMerge(devConfig, obj); - } -}; -(window as any).dev = dev; - -// ============================= // - -async function readDicom(files: FileList) { - if (files.length <= 1) { - console.error( - "Viewport volume does not support just one image, it must be two or more images" - ); - return; - } - - for (const file of files) { - const imageId = wadouri.fileManager.add(file); - await imageLoader.loadAndCacheImage(imageId); - referenceImageIds.push(imageId); - } +viewportGrid.addEventListener("dragover", evt => handleDragOver(evt), false); +viewportGrid.addEventListener( + "drop", + evt => handleFileSelect(evt, state), + false +); - await loadDicom(referenceImageIds); -} +async function loadDicom() { + restart(state); -async function loadDicom(imageIds: string[]) { - restart(); + const volumeLoaderScheme = "cornerstoneStreamingImageVolume"; + state.volumeId = volumeLoaderScheme + ":" + csUtilities.uuidv4(); - // Generate volume id - volumeId = volumeLoaderScheme + ":" + csUtilities.uuidv4(); + const volume = await cornerstone.volumeLoader.createAndCacheVolume( + state.volumeId, + { + imageIds: state.referenceImageIds + } + ); - // Define a volume in memory - const volume = await volumeLoader.createAndCacheVolume(volumeId, { - imageIds - }); + const { toolGroup, viewportIds, renderingEngineId, renderingEngine } = + state; - // toolGroup.addViewport(viewportIds[0], renderingEngineId); toolGroup.addViewport(viewportIds[1], renderingEngineId); toolGroup.addViewport(viewportIds[2], renderingEngineId); - // Set the volume to load await volume.load(); - // Set volumes on the viewports - await setVolumesForViewports(renderingEngine, [{ volumeId }], viewportIds); - - // Render the image - renderingEngine.render(); -} - -async function readSegmentation(file: File) { - const imageId = wadouri.fileManager.add(file); - - const image = await imageLoader.loadAndCacheImage(imageId); - - if (!image) { - return; - } - - const instance = metaData.get("instance", imageId); - - if (instance.Modality !== "SEG") { - console.error("This is not segmentation: " + file.name); - return; - } - - const arrayBuffer = image.data.byteArray.buffer; - - loadSegmentation(arrayBuffer); -} - -async function loadSegmentation(arrayBuffer: ArrayBuffer) { - const generateToolState = - await Cornerstone3D.Segmentation.generateToolState( - referenceImageIds, - arrayBuffer, - metaData, - { - skipOverlapping - } - ); - - if (generateToolState.labelmapBufferArray.length !== 1) { - alert( - "Overlapping segments in your segmentation are not supported yet. You can turn on the skipOverlapping option but it will override the overlapping segments." - ); - return; - } - - const derivedSegmentationImages = - await imageLoader.createAndCacheDerivedLabelmapImages( - referenceImageIds as string[] - ); - - const derivedSegmentationImageIds = derivedSegmentationImages.map( - image => image.imageId - ); - - // - // Add the segmentations to state - csToolsSegmentation.addSegmentations([ - { - segmentationId, - representation: { - // The type of segmentation - type: csToolsEnums.SegmentationRepresentations.Labelmap, - // The actual segmentation data, in the case of labelmap this is a - // reference to the source volume of the segmentation. - data: { - imageIds: derivedSegmentationImageIds - } - } - } - ]); - - const segMap = { - [viewportIds[0]]: [{ segmentationId }], - [viewportIds[1]]: [{ segmentationId }], - [viewportIds[2]]: [{ segmentationId }] - }; - // Add the segmentation representation to the viewport - await csToolsSegmentation.addLabelmapRepresentationToViewportMap(segMap); - - const volumeScalarData = new Uint8Array( - generateToolState.labelmapBufferArray[0] + await cornerstone.setVolumesForViewports( + renderingEngine, + [{ volumeId: state.volumeId }], + viewportIds ); - // We should parse the segmentation as separate slices to support overlapping segments. - // This parsing should occur in the CornerstoneJS library adapters. - // For now, we use the volume returned from the library and chop it here. - for (let i = 0; i < derivedSegmentationImages.length; i++) { - const voxelManager = derivedSegmentationImages[i].voxelManager; - const scalarData = voxelManager.getScalarData(); - scalarData.set( - volumeScalarData.slice( - i * scalarData.length, - (i + 1) * scalarData.length - ) - ); - voxelManager.setScalarData(scalarData); - } -} - -async function exportSegmentation() { - // - const segmentationIds = getSegmentationIds(); - // - if (!segmentationIds.length) { - return; - } - - // Get cache volume - const cacheVolume = cache.getVolume(volumeId); - const csImages = cacheVolume.getCornerstoneImages(); - - // Get active segmentation representation - const activeSegmentation = - csToolsSegmentation.activeSegmentation.getActiveSegmentation( - viewportIds[0] - ); - - const cacheSegmentationVolume = cache.getVolume( - activeSegmentation.segmentationId - ); - - // - const labelmapData = Cornerstone3D.Segmentation.generateLabelMaps2DFrom3D( - cacheSegmentationVolume - ); - - // Generate fake metadata as an example - labelmapData.metadata = []; - labelmapData.segmentsOnLabelmap.forEach(segmentIndex => { - const color = csToolsSegmentation.config.color.getSegmentIndexColor( - viewportIds[0], - activeSegmentation.segmentationId, - segmentIndex - ); - - const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB( - color.slice(0, 3).map(value => value / 255) - ).map(value => Math.round(value)); - - const segmentMetadata = { - SegmentedPropertyCategoryCodeSequence: { - CodeValue: "T-D0050", - CodingSchemeDesignator: "SRT", - CodeMeaning: "Tissue" - }, - SegmentNumber: segmentIndex.toString(), - SegmentLabel: "Tissue " + segmentIndex.toString(), - SegmentAlgorithmType: "SEMIAUTOMATIC", - SegmentAlgorithmName: "Slicer Prototype", - RecommendedDisplayCIELabValue, - SegmentedPropertyTypeCodeSequence: { - CodeValue: "T-D0050", - CodingSchemeDesignator: "SRT", - CodeMeaning: "Tissue" - } - }; - - labelmapData.metadata[segmentIndex] = segmentMetadata; - }); - - // - const generatedSegmentation = - Cornerstone3D.Segmentation.generateSegmentation( - csImages, - labelmapData, - metaData - ); - - downloadDICOMData(generatedSegmentation.dataset, "mySEG.dcm"); + renderingEngine.render(); } // ============================= // - -addDropdownToToolbar({ - id: "DICOM_DROPDOWN", - style: { - marginRight: "10px" - }, - options: { map: dicomMap, defaultIndex: 0 }, - onSelectedValueChange: (key, value) => { - dev.setConfig = value; - }, - container: group1 -}); - addButtonToToolbar({ id: "LOAD_DICOM", title: "Load DICOM", - style: { - marginRight: "5px" - }, onClick: async () => { - // Get Cornerstone imageIds for the source data and fetch metadata into RAM - referenceImageIds = await createImageIdsAndCacheMetaData( - dev.getConfig.fetchDicom + state.referenceImageIds = await createImageIdsAndCacheMetaData( + state.devConfig.fetchDicom ); - - // - await loadDicom(referenceImageIds); + await loadDicom(state.referenceImageIds, state); }, container: group1 }); @@ -379,17 +174,13 @@ addButtonToToolbar({ addButtonToToolbar({ id: "LOAD_SEGMENTATION", title: "Load SEG", - style: { - marginRight: "5px" - }, onClick: async () => { - if (!volumeId) { + if (!state.volumeId) { alert("load source dicom first"); return; } - const configSeg = dev.getConfig.fetchSegmentation; - + const configSeg = state.devConfig.fetchSegmentation; const client = new api.DICOMwebClient({ url: configSeg.wadoRsRoot }); @@ -399,8 +190,7 @@ addButtonToToolbar({ sopInstanceUID: configSeg.SOPInstanceUID }); - // - await loadSegmentation(arrayBuffer); + await loadSegmentation(arrayBuffer, state); }, container: group1 }); @@ -408,26 +198,20 @@ addButtonToToolbar({ addUploadToToolbar({ id: "IMPORT_DICOM", title: "Import DICOM", - style: { - marginRight: "5px" - }, - onChange: readDicom, + onChange: (files: FileList) => readDicom(files, state), container: group2 }); addUploadToToolbar({ id: "IMPORT_SEGMENTATION", title: "Import SEG", - style: { - marginRight: "5px" - }, onChange: async (files: FileList) => { - if (!volumeId) { + if (!state.volumeId) { return; } for (const file of files) { - await readSegmentation(file); + await readSegmentation(file, state); } }, container: group2 @@ -436,114 +220,81 @@ addUploadToToolbar({ addButtonToToolbar({ id: "EXPORT_SEGMENTATION", title: "Export SEG", - onClick: exportSegmentation, - container: group2 + onClick: () => exportSegmentation(state) }); addToggleButtonToToolbar({ id: "SKIP_OVERLAPPING", title: "Override Overlapping Segments", onClick: () => { - skipOverlapping = !skipOverlapping; + state.skipOverlapping = !state.skipOverlapping; }, container: group1 }); - // ============================= // -function restart() { - // If you import the dicom again, before clearing the cache or starting from scratch - if (!volumeId) { - return; - } - - // - cache.removeVolumeLoadObject(volumeId); - - // - csToolsSegmentation.removeAllSegmentationRepresentations(); - - // - const segmentationIds = getSegmentationIds(); - // - segmentationIds.forEach(segmentationId => { - csToolsSegmentation.state.removeSegmentation(segmentationId); - cache.removeVolumeLoadObject(segmentationId); - }); -} - -function getSegmentationIds() { - return csToolsSegmentation.state - .getSegmentations() - .map(x => x.segmentationId); -} - -function handleFileSelect(evt) { - evt.stopPropagation(); - evt.preventDefault(); - - // - const files = evt.dataTransfer.files; - - // - readDicom(files); -} - -function handleDragOver(evt) { - evt.stopPropagation(); - evt.preventDefault(); - evt.dataTransfer.dropEffect = "copy"; -} - // ============================= // /** * Runs the demo */ async function run() { - // Init Cornerstone and related libraries await initDemo(); - // Define tool groups to add the segmentation display tool to - toolGroup = ToolGroupManager.createToolGroup(toolGroupId); - addManipulationBindings(toolGroup, { toolMap: labelmapTools.toolMap }); + state.toolGroup = cornerstoneTools.ToolGroupManager.createToolGroup( + state.toolGroupId + ); + addManipulationBindings(state.toolGroup, { + toolMap: labelmapTools.toolMap + }); + + cornerstoneTools.addTool(BrushTool); + + state.toolGroup.addToolInstance("CircularBrush", BrushTool.toolName, { + activeStrategy: "FILL_INSIDE_CIRCLE" + }); + + state.toolGroup.setToolActive("CircularBrush", { + bindings: [ + { mouseButton: cornerstoneTools.Enums.MouseBindings.Primary } + ] + }); - // Instantiate a rendering engine - renderingEngine = new RenderingEngine(renderingEngineId); + state.renderingEngine = new cornerstone.RenderingEngine( + state.renderingEngineId + ); - // Create the viewports const viewportInputArray = [ { - viewportId: viewportIds[0], - type: ViewportType.ORTHOGRAPHIC, + viewportId: state.viewportIds[0], + type: cornerstone.Enums.ViewportType.ORTHOGRAPHIC, element: element1, defaultOptions: { - orientation: csEnums.OrientationAxis.AXIAL, + orientation: cornerstone.Enums.OrientationAxis.AXIAL, background: [0.2, 0, 0.2] } }, { - viewportId: viewportIds[1], - type: ViewportType.ORTHOGRAPHIC, + viewportId: state.viewportIds[1], + type: cornerstone.Enums.ViewportType.ORTHOGRAPHIC, element: element2, defaultOptions: { - orientation: csEnums.OrientationAxis.SAGITTAL, + orientation: cornerstone.Enums.OrientationAxis.SAGITTAL, background: [0.2, 0, 0.2] } }, { - viewportId: viewportIds[2], - type: ViewportType.ORTHOGRAPHIC, + viewportId: state.viewportIds[2], + type: cornerstone.Enums.ViewportType.ORTHOGRAPHIC, element: element3, defaultOptions: { - orientation: csEnums.OrientationAxis.CORONAL, + orientation: cornerstone.Enums.OrientationAxis.CORONAL, background: [0.2, 0, 0.2] } } ]; - // - renderingEngine.setViewports(viewportInputArray); + state.renderingEngine.setViewports(viewportInputArray); } run(); diff --git a/packages/adapters/examples/segmentationVolume/utils.ts b/packages/adapters/examples/segmentationVolume/utils.ts new file mode 100644 index 0000000000..bb6f5902c7 --- /dev/null +++ b/packages/adapters/examples/segmentationVolume/utils.ts @@ -0,0 +1,249 @@ +import * as cornerstone from "@cornerstonejs/core"; +import * as cornerstoneTools from "@cornerstonejs/tools"; +import * as cornerstoneDicomImageLoader from "@cornerstonejs/dicom-image-loader"; +import * as cornerstoneAdapters from "@cornerstonejs/adapters"; +import dcmjs from "dcmjs"; + +const { + cache, + imageLoader, + metaData, + utilities: csUtilities, + volumeLoader +} = cornerstone; +const { segmentation: csToolsSegmentation } = cornerstoneTools; +const { wadouri } = cornerstoneDicomImageLoader; +const { downloadDICOMData } = cornerstoneAdapters.helpers; +const { Cornerstone3D } = cornerstoneAdapters.adaptersSEG; + +export async function readDicom(files: FileList, state) { + if (files.length <= 1) { + console.error( + "Viewport volume does not support just one image, it must be two or more images" + ); + return; + } + + for (const file of files) { + const imageId = wadouri.fileManager.add(file); + await imageLoader.loadAndCacheImage(imageId); + state.referenceImageIds.push(imageId); + } +} + +export async function readSegmentation(file: File, state) { + const imageId = wadouri.fileManager.add(file); + const image = await imageLoader.loadAndCacheImage(imageId); + + if (!image) { + return; + } + + const instance = metaData.get("instance", imageId); + + if (instance.Modality !== "SEG") { + console.error("This is not segmentation: " + file.name); + return; + } + + const arrayBuffer = image.data.byteArray.buffer; + + await loadSegmentation(arrayBuffer, state); +} + +export async function loadSegmentation(arrayBuffer: ArrayBuffer, state) { + const { referenceImageIds, skipOverlapping, viewportIds, segmentationId } = + state; + + const generateToolState = + await Cornerstone3D.Segmentation.generateToolState( + referenceImageIds, + arrayBuffer, + metaData, + { + skipOverlapping + } + ); + + if (generateToolState.labelmapBufferArray.length !== 1) { + alert( + "Overlapping segments in your segmentation are not supported yet. You can turn on the skipOverlapping option but it will override the overlapping segments." + ); + return; + } + + const derivedSegmentationImages = + await imageLoader.createAndCacheDerivedLabelmapImages( + referenceImageIds + ); + + const derivedSegmentationImageIds = derivedSegmentationImages.map( + image => image.imageId + ); + + csToolsSegmentation.addSegmentations([ + { + segmentationId, + representation: { + type: cornerstoneTools.Enums.SegmentationRepresentations + .Labelmap, + data: { + imageIds: derivedSegmentationImageIds + } + } + } + ]); + + const segMap = { + [viewportIds[0]]: [{ segmentationId }], + [viewportIds[1]]: [{ segmentationId }], + [viewportIds[2]]: [{ segmentationId }] + }; + + await csToolsSegmentation.addLabelmapRepresentationToViewportMap(segMap); + + const volumeScalarData = new Uint8Array( + generateToolState.labelmapBufferArray[0] + ); + + for (let i = 0; i < derivedSegmentationImages.length; i++) { + const voxelManager = derivedSegmentationImages[i].voxelManager; + const scalarData = voxelManager.getScalarData(); + scalarData.set( + volumeScalarData.slice( + i * scalarData.length, + (i + 1) * scalarData.length + ) + ); + voxelManager.setScalarData(scalarData); + } +} + +export async function exportSegmentation(state) { + const { segmentationId, viewportIds } = state; + const segmentationIds = getSegmentationIds(); + if (!segmentationIds.length) { + return; + } + + const segmentation = + csToolsSegmentation.state.getSegmentation(segmentationId); + + const { imageIds } = segmentation.representationData.Labelmap; + + const segImages = imageIds.map(imageId => cache.getImage(imageId)); + const referencedImages = segImages.map(image => + cache.getImage(image.referencedImageId) + ); + + const labelmaps2D = []; + + let z = 0; + + for (const segImage of segImages) { + const segmentsOnLabelmap = new Set(); + const pixelData = segImage.getPixelData(); + const { rows, columns } = segImage; + + for (let i = 0; i < pixelData.length; i++) { + const segment = pixelData[i]; + if (segment !== 0) { + segmentsOnLabelmap.add(segment); + } + } + + labelmaps2D[z++] = { + segmentsOnLabelmap: Array.from(segmentsOnLabelmap), + pixelData, + rows, + columns + }; + } + + const allSegmentsOnLabelmap = labelmaps2D.map( + labelmap => labelmap.segmentsOnLabelmap + ); + + const labelmap3D = { + segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())), + metadata: [], + labelmaps2D + }; + + labelmap3D.segmentsOnLabelmap.forEach(segmentIndex => { + const color = csToolsSegmentation.config.color.getSegmentIndexColor( + viewportIds[0], + segmentationId, + segmentIndex + ); + const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB( + color.slice(0, 3).map(value => value / 255) + ).map(value => Math.round(value)); + + const segmentMetadata = { + SegmentNumber: segmentIndex.toString(), + SegmentLabel: `Segment ${segmentIndex}`, + SegmentAlgorithmType: "MANUAL", + SegmentAlgorithmName: "OHIF Brush", + RecommendedDisplayCIELabValue, + SegmentedPropertyCategoryCodeSequence: { + CodeValue: "T-D0050", + CodingSchemeDesignator: "SRT", + CodeMeaning: "Tissue" + }, + SegmentedPropertyTypeCodeSequence: { + CodeValue: "T-D0050", + CodingSchemeDesignator: "SRT", + CodeMeaning: "Tissue" + } + }; + labelmap3D.metadata[segmentIndex] = segmentMetadata; + }); + + const generatedSegmentation = + Cornerstone3D.Segmentation.generateSegmentation( + referencedImages, + labelmap3D, + metaData + ); + + downloadDICOMData(generatedSegmentation.dataset, "mySEG.dcm"); +} + +export function restart(state) { + const { volumeId } = state; + + if (!volumeId) { + return; + } + + cache.removeVolumeLoadObject(volumeId); + + csToolsSegmentation.removeAllSegmentationRepresentations(); + + const segmentationIds = getSegmentationIds(); + segmentationIds.forEach(segmentationId => { + csToolsSegmentation.state.removeSegmentation(segmentationId); + cache.removeVolumeLoadObject(segmentationId); + }); +} + +export function getSegmentationIds() { + return csToolsSegmentation.state + .getSegmentations() + .map(x => x.segmentationId); +} + +export function handleFileSelect(evt, state) { + evt.stopPropagation(); + evt.preventDefault(); + + const files = evt.dataTransfer.files; + readDicom(files, state); +} + +export function handleDragOver(evt) { + evt.stopPropagation(); + evt.preventDefault(); + evt.dataTransfer.dropEffect = "copy"; +} diff --git a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js index e73b4969ba..97ad936479 100644 --- a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js +++ b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js @@ -250,8 +250,8 @@ async function generateToolState( tolerance = 1e-3, TypedArrayConstructor = Uint8Array, maxBytesPerChunk = 199000000, - eventTarget, - triggerEvent + eventTarget = null, + triggerEvent = null } = options; const dicomData = DicomMessage.readFile(arrayBuffer); const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); diff --git a/packages/tools/src/eventListeners/mouse/mouseMoveListener.ts b/packages/tools/src/eventListeners/mouse/mouseMoveListener.ts index 5463ce4aff..530a16d0a1 100644 --- a/packages/tools/src/eventListeners/mouse/mouseMoveListener.ts +++ b/packages/tools/src/eventListeners/mouse/mouseMoveListener.ts @@ -14,6 +14,11 @@ const eventName = Events.MOUSE_MOVE; function mouseMoveListener(evt: MouseEvent) { const element = evt.currentTarget; const enabledElement = getEnabledElement(element); + + if (!enabledElement) { + return; + } + const { renderingEngineId, viewportId } = enabledElement; const currentPoints = getMouseEventPoints(evt); diff --git a/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts b/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts index 28e67122ae..20f0ac5709 100644 --- a/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts +++ b/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts @@ -17,7 +17,17 @@ import { getLabelmapActorEntry } from '../../stateManagement/segmentation/helper import { getSegmentationRepresentations } from '../../stateManagement/segmentation/getSegmentationRepresentation'; const enable = function (element: HTMLDivElement): void { - const { viewport } = getEnabledElement(element); + if (!element) { + return; + } + + const enabledElement = getEnabledElement(element); + + if (!enabledElement) { + return; + } + + const { viewport } = enabledElement; if (viewport instanceof BaseVolumeViewport) { return;