From 5eeda0626a2722397aba910ae4bd4a5e650d32c6 Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 22 Jan 2025 11:09:13 -0500 Subject: [PATCH] fix(seg): Refactor LabelmapBaseTool and SphereScissorsTool to fix sphere bug in stack (#1772) --- common/reviews/api/tools.api.md | 104 ++---------------- .../examples/segmentationVolume/index.ts | 52 +++++++++ .../core/src/RenderingEngine/StackViewport.ts | 34 +++--- .../getting-started/vue-angular-react-etc.md | 71 +++++++++--- .../PolySegWasmSurfaceToContour/index.ts | 3 - .../index.ts | 31 +----- .../tools/segmentation/LabelmapBaseTool.ts | 70 ++++++++++-- .../tools/segmentation/SphereScissorsTool.ts | 38 +++---- utils/ExampleRunner/example-info.json | 4 - 9 files changed, 212 insertions(+), 195 deletions(-) diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index ec0ea66966..3e75001775 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -3451,28 +3451,7 @@ export class LabelmapBaseTool extends BaseTool { acceptReject: boolean; }): unknown; // (undocumented) - createEditData(element: any): { - volumeId: string; - referencedVolumeId: any; - segmentsLocked: number[] | []; - imageId?: undefined; - override?: undefined; - } | { - imageId: string; - segmentsLocked: number[] | []; - override: { - voxelManager: Types_2.IVoxelManager | Types_2.IVoxelManager; - imageData: vtkImageData; - }; - volumeId?: undefined; - referencedVolumeId?: undefined; - } | { - imageId: string; - segmentsLocked: number[] | []; - volumeId?: undefined; - referencedVolumeId?: undefined; - override?: undefined; - }; + protected createEditData(element: any): EditDataReturnType; // (undocumented) protected createHoverData(element: any, centerCanvas?: any): { brushCursor: { @@ -3514,78 +3493,15 @@ export class LabelmapBaseTool extends BaseTool { segmentColor: Types_2.Color; }; // (undocumented) - protected getOperationData(element?: any): { - points: any; - segmentIndex: number; - previewColors: any; - viewPlaneNormal: any; - toolGroupId: string; - segmentationId: string; - viewUp: any; - strategySpecificConfiguration: any; - preview: unknown; - createMemo: (segmentId: string, segmentationVoxelManager: any, preview: any) => LabelmapMemo.LabelmapMemo; - override: { - voxelManager: Types_2.IVoxelManager; - imageData: vtkImageData; - }; - segmentsLocked: number[]; - imageId?: string; - imageIds?: string[]; - volumeId?: string; - referencedVolumeId?: string; - } | { - points: any; - segmentIndex: number; - previewColors: any; - viewPlaneNormal: any; - toolGroupId: string; - segmentationId: string; - viewUp: any; - strategySpecificConfiguration: any; - preview: unknown; - createMemo: (segmentId: string, segmentationVoxelManager: any, preview: any) => LabelmapMemo.LabelmapMemo; - volumeId: string; - referencedVolumeId: any; - segmentsLocked: number[] | []; - imageId?: undefined; - override?: undefined; - } | { - points: any; - segmentIndex: number; - previewColors: any; - viewPlaneNormal: any; - toolGroupId: string; - segmentationId: string; - viewUp: any; - strategySpecificConfiguration: any; - preview: unknown; - createMemo: (segmentId: string, segmentationVoxelManager: any, preview: any) => LabelmapMemo.LabelmapMemo; - imageId: string; - segmentsLocked: number[] | []; - override: { - voxelManager: Types_2.IVoxelManager | Types_2.IVoxelManager; - imageData: vtkImageData; - }; - volumeId?: undefined; - referencedVolumeId?: undefined; - } | { - points: any; - segmentIndex: number; - previewColors: any; - viewPlaneNormal: any; - toolGroupId: string; - segmentationId: string; - viewUp: any; - strategySpecificConfiguration: any; - preview: unknown; - createMemo: (segmentId: string, segmentationVoxelManager: any, preview: any) => LabelmapMemo.LabelmapMemo; - imageId: string; - segmentsLocked: number[] | []; - volumeId?: undefined; - referencedVolumeId?: undefined; - override?: undefined; - }; + protected getEditData({ viewport, representationData, segmentsLocked, segmentationId, volumeOperation, }: { + viewport: any; + representationData: any; + segmentsLocked: any; + segmentationId: any; + volumeOperation?: boolean; + }): EditDataReturnType; + // (undocumented) + protected getOperationData(element?: any): ModifiedLabelmapToolOperationData; // (undocumented) protected _hoverData?: { brushCursor: any; diff --git a/packages/adapters/examples/segmentationVolume/index.ts b/packages/adapters/examples/segmentationVolume/index.ts index 61bc2b3eac..f9ba0d3daa 100644 --- a/packages/adapters/examples/segmentationVolume/index.ts +++ b/packages/adapters/examples/segmentationVolume/index.ts @@ -35,6 +35,7 @@ import { restart, createSegmentation } from "../segmentationVolume/utils"; +import addDropDownToToolbar from "../../../../utils/demo/helpers/addDropdownToToolbar"; setTitleAndDescription( "DICOM SEG VOLUME", @@ -119,6 +120,7 @@ const state = { segmentationId: "LOAD_SEG_ID:" + cornerstone.utilities.uuidv4(), referenceImageIds: [], skipOverlapping: false, + segmentationIds: [], segImageIds: [], devConfig: { ...dicomMap.values().next().value } }; @@ -169,6 +171,7 @@ function createSegmentationRepresentation() { csToolsSegmentation.addLabelmapRepresentationToViewportMap(segMap); } + // ============================= // addButtonToToolbar({ id: "LOAD_DICOM", @@ -203,6 +206,7 @@ addButtonToToolbar({ await loadSegmentation(arrayBuffer, state); createSegmentationRepresentation(); + updateSegmentationDropdown(); }, container: group1 }); @@ -221,8 +225,11 @@ addButtonToToolbar({ id: "CREATE_SEGMENTATION", title: "Create Empty SEG", onClick: async () => { + const segmentationId = cornerstone.utilities.uuidv4(); + state.segmentationId = segmentationId; await createSegmentation(state); createSegmentationRepresentation(); + updateSegmentationDropdown(); }, container: group2 }); @@ -240,10 +247,55 @@ addUploadToToolbar({ } createSegmentationRepresentation(); + updateSegmentationDropdown(); }, container: group2 }); +function updateSegmentationDropdown() { + // remove the previous dropdown + const previousDropdown = document.getElementById("segmentation-dropdown"); + if (previousDropdown) { + previousDropdown.remove(); + } + + state.segmentationIds = csToolsSegmentation.state + .getSegmentations() + .map(seg => seg.segmentationId); + + state.segmentationId = + csToolsSegmentation.activeSegmentation.getActiveSegmentation( + state.viewportIds[0] + ).segmentationId; + + // Create a map with objects that can have properties set on them + const optionsMap = new Map( + state.segmentationIds.map(id => [ + id, + { id, label: id, selected: id === state.segmentationId } + ]) + ); + + addDropDownToToolbar({ + container: group2, + id: "segmentation-dropdown", + options: { + defaultIndex: state.segmentationIds.indexOf(state.segmentationId), + map: optionsMap + }, + onSelectedValueChange: (key: string | number) => { + state.viewportIds.forEach(viewportId => { + csToolsSegmentation.activeSegmentation.setActiveSegmentation( + viewportId, + key.toString() + ); + }); + + updateSegmentationDropdown(); + } + }); +} + addButtonToToolbar({ id: "EXPORT_SEGMENTATION", title: "Export SEG", diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index 059c827a64..f96d973e21 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -863,26 +863,28 @@ class StackViewport extends Viewport { this.setInterpolationType(InterpolationType.LINEAR); - const transferFunction = this.getTransferFunction(); - setTransferFunctionNodes( - transferFunction, - this.initialTransferFunctionNodes - ); + if (!this.useCPURendering) { + const transferFunction = this.getTransferFunction(); + setTransferFunctionNodes( + transferFunction, + this.initialTransferFunctionNodes + ); - const nodes = getTransferFunctionNodes(transferFunction); + const nodes = getTransferFunctionNodes(transferFunction); - const RGBPoints = nodes.reduce((acc, node) => { - acc.push(node[0], node[1], node[2], node[3]); - return acc; - }, []); + const RGBPoints = nodes.reduce((acc, node) => { + acc.push(node[0], node[1], node[2], node[3]); + return acc; + }, []); - const defaultActor = this.getDefaultActor(); - const matchedColormap = colormapUtils.findMatchingColormap( - RGBPoints, - defaultActor.actor - ); + const defaultActor = this.getDefaultActor(); + const matchedColormap = colormapUtils.findMatchingColormap( + RGBPoints, + defaultActor.actor + ); - this.setColormap(matchedColormap); + this.setColormap(matchedColormap); + } } public resetToDefaultProperties(): void { diff --git a/packages/docs/docs/getting-started/vue-angular-react-etc.md b/packages/docs/docs/getting-started/vue-angular-react-etc.md index bf8820e911..d26f32f62d 100644 --- a/packages/docs/docs/getting-started/vue-angular-react-etc.md +++ b/packages/docs/docs/getting-started/vue-angular-react-etc.md @@ -3,7 +3,6 @@ id: vue-angular-react-etc title: 'React, Vue, Angular, etc.' --- - Here are some examples of how to use cornerstone3D with React, Vue, Angular, etc. We have made it easy to use cornerstone3D with your favorite framework. @@ -15,26 +14,65 @@ Follow the links below to see how to use cornerstone3D with your favorite framew - [Community maintained project](https://github.com/yanqzsu/ng-cornerstone) - [Cornerstone3D with Next.js](https://github.com/cornerstonejs/nextjs-cornerstone3d) - ## Vite To update your Vite configuration, use the CommonJS plugin, exclude `dicom-image-loader` from optimization, and include `dicom-parser`. We plan to convert `dicom-image-loader` to an ES module, eliminating the need for exclusion in the future. ```javascript -import { viteCommonjs } from "@originjs/vite-plugin-commonjs" - +import { viteCommonjs } from '@originjs/vite-plugin-commonjs'; export default defineConfig({ plugins: [viteCommonjs()], optimizeDeps: { - exclude: ["@cornerstonejs/dicom-image-loader"], - include: ["dicom-parser"], + exclude: ['@cornerstonejs/dicom-image-loader'], + include: ['dicom-parser'], }, -}) +}); +``` + +## Troubleshooting + +### 1. @icr/polyseg-wasm Build Issues + +If you're using 3D segmentation features and encounter issues with `@icr/polyseg-wasm`, add the following to your Vite configuration: + +```javascript +build: { + rollupOptions: { + external: ["@icr/polyseg-wasm"], + } +}, ``` +### 2. Path Resolution Issues with @cornerstonejs/core -## Webpack +If you encounter the error "No known conditions for "./types" specifier in "@cornerstonejs/core" package" during build (while development works fine), add the following alias to your Vite configuration: + +```javascript +resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@root': fileURLToPath(new URL('./', import.meta.url)), + "@cornerstonejs/core": fileURLToPath(new URL('node_modules/@cornerstonejs/core/dist/esm', import.meta.url)), + }, +}, +``` + +### 3. Tool Name Minification Issues + +If you experience issues with tool names being minified (e.g., LengthTool being registered as "FE"), you can prevent minification by adding: + +```javascript +build: { + minify: false, +} +``` + +:::note +These solutions have been tested primarily on macOS but may also apply to other operating systems. If you're using Vuetify or other Vue frameworks, these configurations might need to be adjusted based on your specific setup. +::: + +### 4. Webpack For webpack, simply install the cornerstone3D library and import it into your project. @@ -60,18 +98,15 @@ Similar to the configuration above, use the CommonJS plugin converting commonjs ```javascript import { defineConfig } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; -import { viteCommonjs } from "@originjs/vite-plugin-commonjs" +import { viteCommonjs } from '@originjs/vite-plugin-commonjs'; export default defineConfig({ - plugins: [ - svelte(), - viteCommonjs(), - ], - optimizeDeps: { - exclude: ["@cornerstonejs/dicom-image-loader"], - include: ["dicom-parser"], - } -}) + plugins: [svelte(), viteCommonjs()], + optimizeDeps: { + exclude: ['@cornerstonejs/dicom-image-loader'], + include: ['dicom-parser'], + }, +}); ``` :::note Tip diff --git a/packages/tools/examples/PolySegWasmSurfaceToContour/index.ts b/packages/tools/examples/PolySegWasmSurfaceToContour/index.ts index 172a807779..0aa11fdde4 100644 --- a/packages/tools/examples/PolySegWasmSurfaceToContour/index.ts +++ b/packages/tools/examples/PolySegWasmSurfaceToContour/index.ts @@ -1,11 +1,9 @@ -import type { Types } from '@cornerstonejs/core'; import { RenderingEngine, Enums, setVolumesForViewports, volumeLoader, CONSTANTS, - geometryLoader, eventTarget, } from '@cornerstonejs/core'; import { @@ -14,7 +12,6 @@ import { setTitleAndDescription, setCtTransferFunctionForVolumeActor, addButtonToToolbar, - downloadSurfacesData, addManipulationBindings, addLabelToToolbar, } from '../../../../utils/demo/helpers'; diff --git a/packages/tools/examples/PolySegWasmSurfaceToVolumeLabelmap/index.ts b/packages/tools/examples/PolySegWasmSurfaceToVolumeLabelmap/index.ts index 74ae7c606c..1f66a629e8 100644 --- a/packages/tools/examples/PolySegWasmSurfaceToVolumeLabelmap/index.ts +++ b/packages/tools/examples/PolySegWasmSurfaceToVolumeLabelmap/index.ts @@ -18,6 +18,7 @@ import { downloadSurfacesData, addManipulationBindings, addLabelToToolbar, + createAndCacheGeometriesFromSurfaces, } from '../../../../utils/demo/helpers'; import * as cornerstoneTools from '@cornerstonejs/tools'; @@ -191,33 +192,7 @@ async function run() { [{ volumeId, callback: setCtTransferFunctionForVolumeActor }], [viewportId2] ); - - // // set the anatomy at first invisible - // const volumeActor = renderingEngine.getViewport(viewportId3).getDefaultActor() - // .actor as Types.VolumeActor; - // utilities.applyPreset( - // volumeActor, - // CONSTANTS.VIEWPORT_PRESETS.find((preset) => preset.name === 'CT-Bone') - // ); - // volumeActor.setVisibility(false); - - const surfaces = await downloadSurfacesData(); - - const geometriesInfo = surfaces.reduce( - (acc: Map, surface, index) => { - const geometryId = surface.closedSurface.id; - geometryLoader.createAndCacheGeometry(geometryId, { - type: Enums.GeometryType.SURFACE, - geometryData: surface.closedSurface as Types.PublicSurfaceData, - }); - - const segmentIndex = index + 1; - acc.set(segmentIndex, geometryId); - - return acc; - }, - new Map() - ); + const geometryIds = await createAndCacheGeometriesFromSurfaces(); // Add the segmentations to state segmentation.addSegmentations([ @@ -229,7 +204,7 @@ async function run() { // The actual segmentation data, in the case of contour geometry // this is a reference to the geometry data data: { - geometryIds: geometriesInfo, + geometryIds, }, }, }, diff --git a/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts b/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts index 5693a71693..32542b19dc 100644 --- a/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts +++ b/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts @@ -10,7 +10,10 @@ import { import type { Types } from '@cornerstonejs/core'; import { BaseTool } from '../base'; -import type { LabelmapSegmentationDataVolume } from '../../types/LabelmapTypes'; +import type { + LabelmapSegmentationDataStack, + LabelmapSegmentationDataVolume, +} from '../../types/LabelmapTypes'; import SegmentationRepresentations from '../../enums/SegmentationRepresentations'; import type vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; import { getActiveSegmentation } from '../../stateManagement/segmentation/getActiveSegmentation'; @@ -31,6 +34,7 @@ import { filterAnnotationsForDisplay } from '../../utilities/planar'; import { isPointInsidePolyline3D } from '../../utilities/math/polyline'; import { triggerSegmentationDataModified } from '../../stateManagement/segmentation/triggerSegmentationEvents'; import { fillInsideCircle } from './strategies'; +import type { LabelmapToolOperationData } from '../../types/LabelmapToolOperationData'; /** * A type for preview data/information, used to setup previews on hover, or @@ -58,6 +62,35 @@ export type PreviewData = { isDrag: boolean; }; +type EditDataReturnType = + | { + volumeId: string; + referencedVolumeId: string; + segmentsLocked: number[]; + } + | { + imageId: string; + segmentsLocked: number[]; + override?: { + voxelManager: + | Types.IVoxelManager + | Types.IVoxelManager; + imageData: vtkImageData; + }; + } + | null; + +type ModifiedLabelmapToolOperationData = Omit< + LabelmapToolOperationData, + 'voxelManager' | 'override' +> & { + voxelManager?: Types.IVoxelManager | Types.IVoxelManager; + override?: { + voxelManager: Types.IVoxelManager | Types.IVoxelManager; + imageData: vtkImageData; + }; +}; + /** * Labelmap tool containing shared functionality for labelmap tools. */ @@ -117,7 +150,7 @@ export default class LabelmapBaseTool extends BaseTool { return this.memo as LabelmapMemo.LabelmapMemo; } - createEditData(element) { + protected createEditData(element): EditDataReturnType { const enabledElement = getEnabledElement(element); const { viewport } = enabledElement; @@ -141,6 +174,23 @@ export default class LabelmapBaseTool extends BaseTool { const { representationData } = getSegmentation(segmentationId); + const editData = this.getEditData({ + viewport, + representationData, + segmentsLocked, + segmentationId, + }); + + return editData; + } + + protected getEditData({ + viewport, + representationData, + segmentsLocked, + segmentationId, + volumeOperation = false, + }): EditDataReturnType { if (viewport instanceof BaseVolumeViewport) { const { volumeId } = representationData[ SegmentationRepresentations.Labelmap @@ -194,7 +244,10 @@ export default class LabelmapBaseTool extends BaseTool { } // I hate this, but what can you do sometimes - if (this.configuration.activeStrategy.includes('SPHERE')) { + if ( + this.configuration.activeStrategy.includes('SPHERE') || + volumeOperation + ) { const referencedImageIds = viewport.getImageIds(); const isValidVolumeForSphere = csUtils.isValidVolume(referencedImageIds); @@ -217,10 +270,11 @@ export default class LabelmapBaseTool extends BaseTool { }, }; } else { - const labelmapImageIds = getStackSegmentationImageIdsForViewport( - viewport.id, - segmentationId - ); + // We don't need to call `getStackSegmentationImageIdsForViewport` here + // because we've already ensured the stack constructs a volume, + // making the scenario for multi-image non-consistent metadata is not likely. + const { imageIds: labelmapImageIds } = + representationData.Labelmap as LabelmapSegmentationDataStack; if (!labelmapImageIds || labelmapImageIds.length === 1) { return { @@ -317,7 +371,7 @@ export default class LabelmapBaseTool extends BaseTool { }; } - protected getOperationData(element?) { + protected getOperationData(element?): ModifiedLabelmapToolOperationData { const editData = this._editData || this.createEditData(element); const { segmentIndex, segmentationId, brushCursor } = this._hoverData || this.createHoverData(element); diff --git a/packages/tools/src/tools/segmentation/SphereScissorsTool.ts b/packages/tools/src/tools/segmentation/SphereScissorsTool.ts index 0928d81814..6bf7a82cff 100644 --- a/packages/tools/src/tools/segmentation/SphereScissorsTool.ts +++ b/packages/tools/src/tools/segmentation/SphereScissorsTool.ts @@ -1,11 +1,6 @@ -import { - BaseVolumeViewport, - cache, - getEnabledElement, -} from '@cornerstonejs/core'; +import { getEnabledElement } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; -import { BaseTool } from '../base'; import type { PublicToolProps, ToolProps, @@ -32,7 +27,6 @@ import { } from '../../stateManagement/segmentation'; import { getSegmentation } from '../../stateManagement/segmentation/segmentationState'; -import type { LabelmapSegmentationDataVolume } from '../../types/LabelmapTypes'; import LabelmapBaseTool from './LabelmapBaseTool'; /** @@ -184,23 +178,19 @@ class SphereScissorsTool extends LabelmapBaseTool { }; const { representationData } = getSegmentation(segmentationId); - const labelmapData = - representationData[SegmentationRepresentations.Labelmap]; - - if (viewport instanceof BaseVolumeViewport) { - const { volumeId } = labelmapData as LabelmapSegmentationDataVolume; - const segmentation = cache.getVolume(volumeId); - - this.editData = { - ...this.editData, - volumeId, - referencedVolumeId: segmentation.referencedVolumeId, - }; - } else { - this.editData = { - ...this.editData, - }; - } + + const editData = this.getEditData({ + viewport, + representationData, + segmentsLocked, + segmentationId, + volumeOperation: true, + }); + + this.editData = { + ...this.editData, + ...editData, + }; this._activateDraw(element); diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index 2c7fcedad7..53fc123f35 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -557,10 +557,6 @@ } }, "adapters": { - "segmentationExport": { - "name": "DICOM SEG export", - "description": "Demonstrates how to export a segmentation to DICOM SEG" - }, "segmentationStack": { "name": "DICOM SEG Stack", "description": "Demonstrates how to import or export a segmentation to DICOM SEG from a Cornerstone3D stack"