From fe654590d16414e76361e1b2826fd64c3734ae87 Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 27 Feb 2025 15:10:22 -0500 Subject: [PATCH] feat: Cornerstone3D 3.0 (#1865) Co-authored-by: Bill Wallace --- .circleci/config.yml | 36 +- .eslintrc.json | 10 +- .github/workflows/format-check.yml | 38 ++ CHANGELOG.md | 21 +- addOns/externals/polyseg-wasm/CHANGELOG.md | 114 ----- addOns/externals/polyseg-wasm/package.json | 15 - bun.lock | 130 +++-- commit.txt | 2 +- common/reviews/api/core.api.md | 10 +- .../reviews/api/labelmap-interpolation.api.md | 18 + .../api/polymorphic-segmentation.api.md | 48 ++ common/reviews/api/tools.api.md | 265 +++++++--- jest.config.base.js | 8 + lerna.json | 6 +- package.json | 8 +- packages/adapters/.babelrc | 9 - packages/adapters/CHANGELOG.md | 120 +---- packages/adapters/babel.config.js | 1 + packages/adapters/jest.config.js | 11 + packages/adapters/package.json | 10 +- packages/adapters/rollup.config.mjs | 2 +- .../src/adapters/Cornerstone/ArrowAnnotate.js | 3 +- .../adapters/Cornerstone/MeasurementReport.js | 18 +- .../src/adapters/Cornerstone3D/Angle.ts | 31 +- .../{ArrowAnnotate.js => ArrowAnnotate.ts} | 48 +- .../adapters/Cornerstone3D/BaseAdapter3D.ts | 178 +++++++ .../adapters/Cornerstone3D/Bidirectional.ts | 29 +- .../src/adapters/Cornerstone3D/CircleROI.ts | 19 +- .../src/adapters/Cornerstone3D/CobbAngle.ts | 32 +- .../adapters/Cornerstone3D/EllipticalROI.ts | 17 +- .../src/adapters/Cornerstone3D/KeyImage.ts | 59 +++ .../Cornerstone3D/{Length.js => Length.ts} | 36 +- .../Cornerstone3D/MeasurementReport.ts | 300 +++++++---- .../Cornerstone3D/PlanarFreehandROI.ts | 33 +- .../src/adapters/Cornerstone3D/Probe.js | 106 ---- .../src/adapters/Cornerstone3D/Probe.ts | 65 +++ .../adapters/Cornerstone3D/RectangleROI.ts | 34 +- .../Cornerstone3D/UltrasoundDirectional.ts | 31 +- .../src/adapters/Cornerstone3D/index.ts | 4 + packages/adapters/test/KeyImage.jest.js | 28 + packages/adapters/test/Probe.jest.js | 16 + packages/ai/CHANGELOG.md | 18 +- packages/ai/package.json | 16 +- packages/core/CHANGELOG.md | 20 +- packages/core/jest.config.js | 1 + packages/core/package.json | 2 +- packages/core/src/cache/cache.ts | 33 ++ packages/core/src/enums/Events.ts | 5 - packages/core/src/utilities/VoxelManager.ts | 18 +- .../calculateSpacingBetweenImageIds.ts | 150 ++++++ packages/core/src/utilities/fnv1aHash.ts | 14 + packages/core/src/utilities/index.ts | 5 +- .../src/utilities/pointInShapeCallback.ts | 15 +- .../utilities/sortImageIdsAndGetSpacing.ts | 85 +--- .../src/webWorkerManager/webWorkerManager.js | 3 + .../test/stackViewport_node_render.jest.js | 117 +++++ packages/dicomImageLoader/CHANGELOG.md | 18 +- packages/dicomImageLoader/package.json | 4 +- .../getting-started/vue-angular-react-etc.md | 127 ----- .../getting-started/vue-angular-react-vite.md | 244 +++++++++ .../docs/migration-guides/2x/1-general.md | 6 +- .../docs/migration-guides/3x/1-polyseg.md | 91 ++++ .../migration-guides/3x/2-threshold-tools.md | 99 ++++ .../3x/3-labelmap-interpolation.md | 71 +++ .../migration-guides/3x/4-get-statistics.md | 40 ++ .../docs/migration-guides/3x/5-adapters.md | 64 +++ packages/docs/package.json | 12 +- packages/labelmap-interpolation/CHANGELOG.md | 22 + packages/labelmap-interpolation/README.md | 60 +++ .../labelmap-interpolation/api-extractor.json | 9 + .../examples/labelmapInterpolation/index.ts | 300 +++++++++++ packages/labelmap-interpolation/package.json | 58 +++ packages/labelmap-interpolation/src/index.ts | 3 + .../src/registerWorker.ts | 34 ++ .../src/utilities/interpolateLabelmap.ts | 99 ++++ .../src/workers/interpolationWorker.js | 174 +++++++ packages/labelmap-interpolation/tsconfig.json | 8 + packages/nifti-volume-loader/CHANGELOG.md | 18 +- packages/nifti-volume-loader/package.json | 4 +- .../polymorphic-segmentation/CHANGELOG.md | 22 + packages/polymorphic-segmentation/README.md | 40 ++ .../api-extractor.json | 9 + .../index.ts | 0 .../PolySegWasmContourToSurface/index.ts | 0 .../index.ts | 0 .../index.ts | 0 .../PolySegWasmSurfaceToContour/index.ts | 0 .../index.ts | 2 +- .../index.ts | 0 .../index.ts | 0 .../index.ts | 0 .../polymorphic-segmentation/package.json | 56 ++ .../Contour/contourComputationStrategies.ts | 21 +- ...ContourSegmentationsFromClippedSurfaces.ts | 9 +- .../src}/Contour/utils/extractContourData.ts | 2 +- .../utils/updateContoursOnCameraModified.ts | 2 +- .../src}/Labelmap/convertContourToLabelmap.ts | 27 +- .../src}/Labelmap/convertSurfaceToLabelmap.ts | 8 +- .../Labelmap/labelmapComputationStrategies.ts | 34 +- .../src}/Surface/convertContourToSurface.ts | 18 +- .../src}/Surface/convertLabelmapToSurface.ts | 28 +- .../Surface/createAndCacheSurfacesFromRaw.ts | 8 +- .../Surface/surfaceComputationStrategies.ts | 28 +- .../src}/Surface/updateSurfaceData.ts | 23 +- .../src}/canComputeRequestedRepresentation.ts | 21 +- .../polymorphic-segmentation/src/index.ts | 25 + .../src}/registerPolySegWorker.ts | 4 +- .../src/types/PolySegConversionOptions.ts} | 0 .../src/types/index.ts | 3 + .../clipAndCacheSurfacesForViewport.ts | 10 +- .../src/utilities/index.ts | 3 + .../src/workers/polySegConverters.js | 30 +- .../polymorphic-segmentation/tsconfig.json | 8 + packages/tools/CHANGELOG.md | 20 +- .../examples/labelmapInterpolation/index.ts | 338 ------------ .../index.ts | 20 +- .../examples/labelmapStatistics/index.ts | 480 ++++++------------ .../stackLabelmapSegmentation/index.ts | 48 +- packages/tools/examples/stackRange/index.ts | 11 + .../tools/examples/videoSegmentation/index.ts | 7 +- packages/tools/package.json | 4 +- packages/tools/src/config.ts | 106 ++++ packages/tools/src/enums/StrategyCallbacks.ts | 6 + packages/tools/src/enums/WorkerTypes.ts | 4 + packages/tools/src/init.ts | 6 +- .../annotation/helpers/state.ts | 8 +- .../helpers/normalizeSegmentationInput.ts | 2 +- .../helpers/validateSegmentationInput.ts | 2 +- .../src/stateManagement/segmentation/index.ts | 7 +- .../computeAndAddContourRepresentation.ts | 23 - .../computeAndAddLabelmapRepresentation.ts | 36 -- .../computeAndAddSurfaceRepresentation.ts | 24 - .../segmentation/polySeg/index.ts | 12 - packages/tools/src/tools/CrosshairsTool.ts | 3 - .../src/tools/annotation/KeyImageTool.ts | 244 ++++++--- .../tools/src/tools/annotation/ProbeTool.ts | 13 - .../tools/src/tools/base/AnnotationTool.ts | 15 + .../displayTools/Contour/contourDisplay.ts | 21 +- .../src/tools/displayTools/Labelmap/index.ts | 2 +- .../displayTools/Labelmap/labelmapDisplay.ts | 34 +- .../displayTools/Surface/surfaceDisplay.ts | 34 +- .../tools/src/tools/segmentation/BrushTool.ts | 11 +- .../tools/segmentation/CircleScissorsTool.ts | 1 - .../tools/segmentation/LabelmapBaseTool.ts | 80 +-- .../segmentation/RectangleScissorsTool.ts | 1 - .../tools/segmentation/SphereScissorsTool.ts | 1 - .../segmentation/strategies/BrushStrategy.ts | 48 +- .../compositions/determineSegmentIndex.ts | 20 +- .../compositions/dynamicThreshold.ts | 41 +- .../compositions/ensureImageVolume.ts | 50 ++ .../compositions/ensureSegmentationVolume.ts | 26 + .../strategies/compositions/index.ts | 6 +- .../compositions/islandRemovalComposition.ts | 7 +- .../compositions/labelmapInterpolation.ts | 105 ---- .../compositions/labelmapStatistics.ts | 149 +----- .../strategies/compositions/preview.ts | 14 +- .../strategies/compositions/threshold.ts | 20 +- .../segmentation/strategies/fillCircle.ts | 6 +- .../segmentation/strategies/fillRectangle.ts | 2 - .../segmentation/strategies/fillSphere.ts | 11 +- .../strategies/utils/getItkImage.ts | 75 --- .../strategies/utils/getStrategyData.ts | 179 +++++-- .../strategies/utils/isWithinThreshold.ts | 14 +- packages/tools/src/types/CalculatorTypes.ts | 2 + packages/tools/src/types/EventTypes.ts | 11 +- .../src/types/LabelmapToolOperationData.ts | 4 +- .../src/types/ToolSpecificAnnotationTypes.ts | 9 + packages/tools/src/types/index.ts | 24 +- .../{ => utilities}/geometricSurfaceUtils.ts | 0 .../tools/src/utilities/getPixelValueUnits.ts | 10 +- packages/tools/src/utilities/index.ts | 8 +- .../math/basic/BasicStatsCalculator.ts | 35 +- .../src/utilities/registerComputeWorker.ts | 34 ++ .../brushThresholdForToolGroup.ts | 35 +- .../computeAndAddRepresentation.ts | 14 +- .../getOrCreateSegmentationVolume.ts | 44 ++ .../utilities/segmentation/getStatistics.ts | 330 ++++++++++++ .../tools/src/utilities/segmentation/index.ts | 10 + .../segmentation}/validateLabelmap.ts | 4 +- packages/tools/src/workers/computeWorker.js | 129 +++++ tsconfig.json | 2 + utils/ExampleRunner/build-all-examples-cli.js | 5 + utils/ExampleRunner/example-info.json | 8 +- utils/ExampleRunner/example-runner-cli.js | 5 + utils/ExampleRunner/template-config.js | 14 + .../template-multiexample-config.js | 14 + .../demo/helpers/{initDemo.js => initDemo.ts} | 19 +- utils/demo/helpers/labelmapTools.ts | 22 +- version.json | 6 +- version.txt | 2 +- yarn.lock | 95 +++- 191 files changed, 4986 insertions(+), 2783 deletions(-) create mode 100644 .github/workflows/format-check.yml delete mode 100644 addOns/externals/polyseg-wasm/CHANGELOG.md delete mode 100644 addOns/externals/polyseg-wasm/package.json create mode 100644 common/reviews/api/labelmap-interpolation.api.md create mode 100644 common/reviews/api/polymorphic-segmentation.api.md delete mode 100644 packages/adapters/.babelrc create mode 100644 packages/adapters/babel.config.js create mode 100644 packages/adapters/jest.config.js rename packages/adapters/src/adapters/Cornerstone3D/{ArrowAnnotate.js => ArrowAnnotate.ts} (77%) create mode 100644 packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts create mode 100644 packages/adapters/src/adapters/Cornerstone3D/KeyImage.ts rename packages/adapters/src/adapters/Cornerstone3D/{Length.js => Length.ts} (79%) delete mode 100644 packages/adapters/src/adapters/Cornerstone3D/Probe.js create mode 100644 packages/adapters/src/adapters/Cornerstone3D/Probe.ts create mode 100644 packages/adapters/test/KeyImage.jest.js create mode 100644 packages/adapters/test/Probe.jest.js create mode 100644 packages/core/src/utilities/calculateSpacingBetweenImageIds.ts create mode 100644 packages/core/src/utilities/fnv1aHash.ts create mode 100644 packages/core/test/stackViewport_node_render.jest.js delete mode 100644 packages/docs/docs/getting-started/vue-angular-react-etc.md create mode 100644 packages/docs/docs/getting-started/vue-angular-react-vite.md create mode 100644 packages/docs/docs/migration-guides/3x/1-polyseg.md create mode 100644 packages/docs/docs/migration-guides/3x/2-threshold-tools.md create mode 100644 packages/docs/docs/migration-guides/3x/3-labelmap-interpolation.md create mode 100644 packages/docs/docs/migration-guides/3x/4-get-statistics.md create mode 100644 packages/docs/docs/migration-guides/3x/5-adapters.md create mode 100644 packages/labelmap-interpolation/CHANGELOG.md create mode 100644 packages/labelmap-interpolation/README.md create mode 100644 packages/labelmap-interpolation/api-extractor.json create mode 100644 packages/labelmap-interpolation/examples/labelmapInterpolation/index.ts create mode 100644 packages/labelmap-interpolation/package.json create mode 100644 packages/labelmap-interpolation/src/index.ts create mode 100644 packages/labelmap-interpolation/src/registerWorker.ts create mode 100644 packages/labelmap-interpolation/src/utilities/interpolateLabelmap.ts create mode 100644 packages/labelmap-interpolation/src/workers/interpolationWorker.js create mode 100644 packages/labelmap-interpolation/tsconfig.json create mode 100644 packages/polymorphic-segmentation/CHANGELOG.md create mode 100644 packages/polymorphic-segmentation/README.md create mode 100644 packages/polymorphic-segmentation/api-extractor.json rename packages/{tools => polymorphic-segmentation}/examples/PolySegWasmContourToStackLabelmap/index.ts (100%) rename packages/{tools => polymorphic-segmentation}/examples/PolySegWasmContourToSurface/index.ts (100%) rename packages/{tools => polymorphic-segmentation}/examples/PolySegWasmContourToVolumeLabelmap/index.ts (100%) rename packages/{tools => polymorphic-segmentation}/examples/PolySegWasmStackLabelmapToSurface/index.ts (100%) rename packages/{tools => polymorphic-segmentation}/examples/PolySegWasmSurfaceToContour/index.ts (100%) rename packages/{tools => polymorphic-segmentation}/examples/PolySegWasmSurfaceToStackLabelmap/index.ts (98%) rename packages/{tools => polymorphic-segmentation}/examples/PolySegWasmSurfaceToVolumeLabelmap/index.ts (100%) rename packages/{tools => polymorphic-segmentation}/examples/PolySegWasmVolumeLabelmapToContour/index.ts (100%) rename packages/{tools => polymorphic-segmentation}/examples/PolySegWasmVolumeLabelmapToSurface/index.ts (100%) create mode 100644 packages/polymorphic-segmentation/package.json rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/Contour/contourComputationStrategies.ts (90%) rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/Contour/utils/createAndAddContourSegmentationsFromClippedSurfaces.ts (93%) rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/Contour/utils/extractContourData.ts (90%) rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/Contour/utils/updateContoursOnCameraModified.ts (94%) rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/Labelmap/convertContourToLabelmap.ts (91%) rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/Labelmap/convertSurfaceToLabelmap.ts (88%) rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/Labelmap/labelmapComputationStrategies.ts (85%) rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/Surface/convertContourToSurface.ts (80%) rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/Surface/convertLabelmapToSurface.ts (73%) rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/Surface/createAndCacheSurfacesFromRaw.ts (88%) rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/Surface/surfaceComputationStrategies.ts (86%) rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/Surface/updateSurfaceData.ts (84%) rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/canComputeRequestedRepresentation.ts (86%) create mode 100644 packages/polymorphic-segmentation/src/index.ts rename packages/{tools/src/stateManagement/segmentation/polySeg => polymorphic-segmentation/src}/registerPolySegWorker.ts (81%) rename packages/{tools/src/types/PolySeg.ts => polymorphic-segmentation/src/types/PolySegConversionOptions.ts} (100%) create mode 100644 packages/polymorphic-segmentation/src/types/index.ts rename packages/{tools/src/stateManagement/segmentation/helpers => polymorphic-segmentation/src/utilities}/clipAndCacheSurfacesForViewport.ts (95%) create mode 100644 packages/polymorphic-segmentation/src/utilities/index.ts rename packages/{tools => polymorphic-segmentation}/src/workers/polySegConverters.js (97%) create mode 100644 packages/polymorphic-segmentation/tsconfig.json delete mode 100644 packages/tools/examples/labelmapInterpolation/index.ts create mode 100644 packages/tools/src/config.ts delete mode 100644 packages/tools/src/stateManagement/segmentation/polySeg/Contour/computeAndAddContourRepresentation.ts delete mode 100644 packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/computeAndAddLabelmapRepresentation.ts delete mode 100644 packages/tools/src/stateManagement/segmentation/polySeg/Surface/computeAndAddSurfaceRepresentation.ts delete mode 100644 packages/tools/src/stateManagement/segmentation/polySeg/index.ts create mode 100644 packages/tools/src/tools/segmentation/strategies/compositions/ensureImageVolume.ts create mode 100644 packages/tools/src/tools/segmentation/strategies/compositions/ensureSegmentationVolume.ts delete mode 100644 packages/tools/src/tools/segmentation/strategies/compositions/labelmapInterpolation.ts delete mode 100644 packages/tools/src/tools/segmentation/strategies/utils/getItkImage.ts rename packages/tools/src/{ => utilities}/geometricSurfaceUtils.ts (100%) create mode 100644 packages/tools/src/utilities/registerComputeWorker.ts rename packages/tools/src/{stateManagement/segmentation/polySeg => utilities/segmentation}/computeAndAddRepresentation.ts (86%) create mode 100644 packages/tools/src/utilities/segmentation/getOrCreateSegmentationVolume.ts create mode 100644 packages/tools/src/utilities/segmentation/getStatistics.ts rename packages/tools/src/{tools/displayTools/Labelmap => utilities/segmentation}/validateLabelmap.ts (94%) create mode 100644 packages/tools/src/workers/computeWorker.js rename utils/demo/helpers/{initDemo.js => initDemo.ts} (84%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2a0b4224a4..f3a6a7ce7f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -40,7 +40,8 @@ jobs: - run: bun install - persist_to_workspace: root: ~/repo - paths: . + paths: + - "." BUILD: <<: *defaults @@ -64,22 +65,6 @@ jobs: - commit.txt - version.json - API_CHECK: - <<: *defaults - steps: - - attach_workspace: - at: ~/repo - - install_bun - - run: bun install -d ajv - - run: bun run api-check - FORMAT_CHECK: - <<: *defaults - steps: - - attach_workspace: - at: ~/repo - - install_bun - - run: bun run format-check - # https://circleci.com/docs/2.0/collect-test-data/#karma TEST: <<: *defaults @@ -117,7 +102,8 @@ jobs: mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - add_ssh_keys: - fingerprints: 7e:0f:5b:bb:e3:7a:2e:2f:b4:85:bd:66:09:69:cb:f2 + fingerprints: + - "7e:0f:5b:bb:e3:7a:2e:2f:b4:85:bd:66:09:69:cb:f2" - run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc - run: git config --global user.email "contact@ohif.org" - run: git config --global user.name "ohif-bot" @@ -150,12 +136,6 @@ workflows: - BUILD: requires: - CHECKOUT - - FORMAT_CHECK: - requires: - - BUILD - - API_CHECK: - requires: - - BUILD - TEST: requires: - CHECKOUT # TODO: Also require build? @@ -168,15 +148,19 @@ workflows: branches: only: - main - # - beta + - beta - BUILD: requires: - CHECKOUT - TEST: requires: - CHECKOUT - - NPM_PUBLISH: + - HOLD_FOR_APPROVAL: + type: approval requires: - BUILD + - NPM_PUBLISH: + requires: + - HOLD_FOR_APPROVAL # VS Code Extension Version: 1.5.1 diff --git a/.eslintrc.json b/.eslintrc.json index d17eb9bf44..a52bf493f6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -55,7 +55,15 @@ "tsconfigRootDir": "./", "warnOnUnsupportedTypeScriptVersion": false }, - "ignorePatterns": ["packages/docs", "dist", "**/*_test.js", "**/*_jest.js", "**/*babel*", "**/*.d.ts", "**/*_types.ts"], + "ignorePatterns": [ + "packages/docs", + "dist", + "**/*_test.js", + "**/*_jest.js", + "**/*babel*", + "**/*.d.ts", + "**/*_types.ts" + ], "rules": { "import/no-cycle": ["error", { "maxDepth": 15 }], // Enforce consistent brace style for all control statements for readability diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml new file mode 100644 index 0000000000..8337b29e2e --- /dev/null +++ b/.github/workflows/format-check.yml @@ -0,0 +1,38 @@ +name: Format Check + +on: + pull_request: + branches: [ '*' ] + push: + branches: [ main, beta ] + +jobs: + format-check: + timeout-minutes: 30 + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.18.1' + cache: 'npm' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Build dependencies + run: bun run build:esm + + - name: Run format check + run: bun run format-check + env: + NODE_OPTIONS: --max_old_space_size=32896 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ae3e8fca0..445da08d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,31 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [2.19.16](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.15...v2.19.16) (2025-02-26) +# [3.0.0-beta.6](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2025-02-27) + +### Features + +- Add key image adapters for key image point mark ([#1754](https://github.com/cornerstonejs/cornerstone3D/issues/1754)) ([a1fd3f9](https://github.com/cornerstonejs/cornerstone3D/commit/a1fd3f9d0ea40d53cafd792d59bc1dbfc90663a5)) + +# [3.0.0-beta.5](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2025-02-25) **Note:** Version bump only for package root -## [2.19.15](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.14...v2.19.15) (2025-02-26) +# [3.0.0-beta.4](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2025-02-25) **Note:** Version bump only for package root +# [3.0.0-beta.3](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2025-02-25) + +**Note:** Version bump only for package root + +# [3.0.0-beta.2](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.14...v3.0.0-beta.2) (2025-02-24) + +### Bug Fixes + +- publish beta for 3.0 ([6000ed8](https://github.com/cornerstonejs/cornerstone3D/commit/6000ed8cb2bee031a93fe57eeda81d926ee31240)) +- publish beta for 3.0 ([8bf65df](https://github.com/cornerstonejs/cornerstone3D/commit/8bf65df9bec5f52459de1c49c4834b316f680f1b)) + ## [2.19.14](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.13...v2.19.14) (2025-02-24) ### Bug Fixes diff --git a/addOns/externals/polyseg-wasm/CHANGELOG.md b/addOns/externals/polyseg-wasm/CHANGELOG.md deleted file mode 100644 index c39250d36e..0000000000 --- a/addOns/externals/polyseg-wasm/CHANGELOG.md +++ /dev/null @@ -1,114 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [2.1.10](https://github.com/cornerstonejs/cornerstone3D/compare/v2.1.9...v2.1.10) (2024-11-05) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.1.9](https://github.com/cornerstonejs/cornerstone3D/compare/v2.1.8...v2.1.9) (2024-11-05) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.1.8](https://github.com/cornerstonejs/cornerstone3D/compare/v2.1.7...v2.1.8) (2024-11-05) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.1.7](https://github.com/cornerstonejs/cornerstone3D/compare/v2.1.6...v2.1.7) (2024-11-04) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.1.6](https://github.com/cornerstonejs/cornerstone3D/compare/v2.1.5...v2.1.6) (2024-11-04) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.1.5](https://github.com/cornerstonejs/cornerstone3D/compare/v2.1.4...v2.1.5) (2024-10-31) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.1.4](https://github.com/cornerstonejs/cornerstone3D/compare/v2.1.3...v2.1.4) (2024-10-31) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.1.3](https://github.com/cornerstonejs/cornerstone3D/compare/v2.1.2...v2.1.3) (2024-10-31) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.1.2](https://github.com/cornerstonejs/cornerstone3D/compare/v2.1.1...v2.1.2) (2024-10-31) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.1.1](https://github.com/cornerstonejs/cornerstone3D/compare/v2.1.0...v2.1.1) (2024-10-30) - -**Note:** Version bump only for package @externals/polyseg-wasm - -# [2.1.0](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.5...v2.1.0) (2024-10-29) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.0.5](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.4...v2.0.5) (2024-10-29) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.0.4](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.3...v2.0.4) (2024-10-29) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.0.3](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.2...v2.0.3) (2024-10-29) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.0.2](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.1...v2.0.2) (2024-10-29) - -**Note:** Version bump only for package @externals/polyseg-wasm - -## [2.0.1](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.0...v2.0.1) (2024-10-29) - -**Note:** Version bump only for package @externals/polyseg-wasm - -# [2.0.0](https://github.com/cornerstonejs/cornerstone3D/compare/v1.86.0...v2.0.0) (2024-10-29) - -### Features - -- Cornerstone3D 2.0 ([#1400](https://github.com/cornerstonejs/cornerstone3D/issues/1400)) ([692d9af](https://github.com/cornerstonejs/cornerstone3D/commit/692d9afc6a8bcfa801c4aff0eec7706812bbfed8)) - -# [2.0.0-beta.30](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.0-beta.29...v2.0.0-beta.30) (2024-10-04) - -**Note:** Version bump only for package @externals/polyseg-wasm - -# [2.0.0-beta.29](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.0-beta.28...v2.0.0-beta.29) (2024-10-01) - -**Note:** Version bump only for package @externals/polyseg-wasm - -# [2.0.0-beta.28](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.0-beta.27...v2.0.0-beta.28) (2024-09-12) - -**Note:** Version bump only for package @externals/polyseg-wasm - -# [2.0.0-beta.27](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.0-beta.26...v2.0.0-beta.27) (2024-08-26) - -**Note:** Version bump only for package @externals/polyseg-wasm - -# [2.0.0-beta.26](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.0-beta.25...v2.0.0-beta.26) (2024-08-23) - -**Note:** Version bump only for package @externals/polyseg-wasm - -# [2.0.0-beta.25](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.0-beta.24...v2.0.0-beta.25) (2024-08-23) - -**Note:** Version bump only for package @externals/polyseg-wasm - -# [2.0.0-beta.24](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.0-beta.23...v2.0.0-beta.24) (2024-08-22) - -**Note:** Version bump only for package @externals/polyseg-wasm - -# [2.0.0-beta.23](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.0-beta.22...v2.0.0-beta.23) (2024-08-22) - -**Note:** Version bump only for package @externals/polyseg-wasm - -# [2.0.0-beta.22](https://github.com/cornerstonejs/cornerstone3D/compare/v2.0.0-beta.21...v2.0.0-beta.22) (2024-08-22) - -**Note:** Version bump only for package @externals/polyseg-wasm - -# [2.0.0-beta.21](https://github.com/cornerstonejs/cornerstone3D/compare/v1.82.5...v2.0.0-beta.21) (2024-08-21) - -**Note:** Version bump only for package @externals/polyseg-wasm diff --git a/addOns/externals/polyseg-wasm/package.json b/addOns/externals/polyseg-wasm/package.json deleted file mode 100644 index c662a6cb2e..0000000000 --- a/addOns/externals/polyseg-wasm/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@externals/polyseg-wasm", - "description": "External reference to @icr/polyseg-wasm", - "private": true, - "version": "2.1.10", - "license": "MIT", - "engines": { - "node": ">=18", - "yarn": ">=1.19.1" - }, - "dependencies": { - "@icr/polyseg-wasm": "^0.4.0", - "@itk-wasm/morphological-contour-interpolation": "1.0.1" - } -} diff --git a/bun.lock b/bun.lock index 4d76380783..041dc85208 100644 --- a/bun.lock +++ b/bun.lock @@ -43,6 +43,7 @@ "autoprefixer": "^10.4.14", "babel-loader": "9.1.2", "babel-plugin-istanbul": "^6.1.1", + "babel-plugin-transform-import-meta": "2.3.2", "chai": "^5.1.2", "clean-webpack-plugin": "^4.0.0", "commander": "^10.0.1", @@ -128,17 +129,9 @@ "dicom-microscopy-viewer": "^0.46.1", }, }, - "addOns/externals/polyseg-wasm": { - "name": "@externals/polyseg-wasm", - "version": "2.1.10", - "dependencies": { - "@icr/polyseg-wasm": "^0.4.0", - "@itk-wasm/morphological-contour-interpolation": "1.0.1", - }, - }, "packages/adapters": { "name": "@cornerstonejs/adapters", - "version": "2.19.15", + "version": "3.0.0-beta.6", "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", @@ -147,13 +140,13 @@ "ndarray": "^1.0.19", }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.15", - "@cornerstonejs/tools": "^2.19.15", + "@cornerstonejs/core": "^3.0.0-beta.6", + "@cornerstonejs/tools": "^3.0.0-beta.6", }, }, "packages/ai": { "name": "@cornerstonejs/ai", - "version": "2.19.15", + "version": "3.0.0-beta.6", "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", @@ -165,13 +158,13 @@ "onnxruntime-web": "1.17.1", }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.7", - "@cornerstonejs/tools": "^2.19.7", + "@cornerstonejs/core": "^3.0.0-beta.5", + "@cornerstonejs/tools": "^3.0.0-beta.5", }, }, "packages/core": { "name": "@cornerstonejs/core", - "version": "2.19.15", + "version": "3.0.0-beta.6", "dependencies": { "@kitware/vtk.js": "32.9.0", "comlink": "^4.4.1", @@ -180,7 +173,7 @@ }, "packages/dicomImageLoader": { "name": "@cornerstonejs/dicom-image-loader", - "version": "2.19.15", + "version": "3.0.0-beta.6", "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", @@ -192,19 +185,19 @@ "uuid": "^9.0.0", }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.15", + "@cornerstonejs/core": "^3.0.0-beta.6", "dicom-parser": "^1.8.9", }, }, "packages/docs": { "name": "docs", - "version": "2.1.10", + "version": "3.0.0-beta.5", "dependencies": { - "@cornerstonejs/adapters": "^2.19.15", - "@cornerstonejs/core": "^2.19.15", - "@cornerstonejs/dicom-image-loader": "^2.19.15", - "@cornerstonejs/nifti-volume-loader": "^2.19.15", - "@cornerstonejs/tools": "^2.19.15", + "@cornerstonejs/adapters": "^3.0.0-beta.6", + "@cornerstonejs/core": "^3.0.0-beta.6", + "@cornerstonejs/dicom-image-loader": "^3.0.0-beta.6", + "@cornerstonejs/nifti-volume-loader": "^3.0.0-beta.6", + "@cornerstonejs/tools": "^3.0.0-beta.6", "@docusaurus/core": "3.6.3", "@docusaurus/faster": "3.6.3", "@docusaurus/module-type-aliases": "3.6.3", @@ -240,19 +233,44 @@ "typedoc": "0.26.10", }, }, + "packages/labelmap-interpolation": { + "name": "@cornerstonejs/labelmap-interpolation", + "version": "3.0.0-beta.6", + "dependencies": { + "@itk-wasm/morphological-contour-interpolation": "1.1.0", + "itk-wasm": "1.0.0-b.165", + }, + "peerDependencies": { + "@cornerstonejs/core": "^2.19.11", + "@cornerstonejs/tools": "^2.19.11", + "@kitware/vtk.js": "^32.9.0", + }, + }, "packages/nifti-volume-loader": { "name": "@cornerstonejs/nifti-volume-loader", - "version": "2.19.15", + "version": "3.0.0-beta.6", "dependencies": { "nifti-reader-js": "^0.6.8", }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.15", + "@cornerstonejs/core": "^3.0.0-beta.6", + }, + }, + "packages/polymorphic-segmentation": { + "name": "@cornerstonejs/polymorphic-segmentation", + "version": "3.0.0-beta.6", + "dependencies": { + "@icr/polyseg-wasm": "0.4.0", + }, + "peerDependencies": { + "@cornerstonejs/core": "^2.19.11", + "@cornerstonejs/tools": "^2.19.11", + "@kitware/vtk.js": "^32.9.0", }, }, "packages/tools": { "name": "@cornerstonejs/tools", - "version": "2.19.15", + "version": "3.0.0-beta.6", "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", @@ -262,7 +280,7 @@ "canvas": "^2.11.2", }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.15", + "@cornerstonejs/core": "^3.0.0-beta.6", "@kitware/vtk.js": "32.9.0", "@types/d3-array": "^3.0.4", "@types/d3-interpolate": "^3.0.1", @@ -525,7 +543,7 @@ "@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg=="], - "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.25.9", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.10.6", "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ=="], + "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.26.9", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.10.6", "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Jf+8y9wXQbbxvVYTM8gO5oEF2POdNji0NMltEkG7FtmzD9PVz7/lxpqSdTvwsjTMU5HIHuDVNf2SOxLkWi+wPQ=="], "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng=="], @@ -557,9 +575,9 @@ "@babel/runtime": ["@babel/runtime@7.21.5", "", { "dependencies": { "regenerator-runtime": "^0.13.11" } }, "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q=="], - "@babel/runtime-corejs2": ["@babel/runtime-corejs2@7.26.7", "", { "dependencies": { "core-js": "^2.6.12", "regenerator-runtime": "^0.14.0" } }, "sha512-C7fo97gUfsUP54j6GcQ+rJXyW6vgRRqF7J1ZxXesWcQtSRyzH1+eYrqFGzmU2JSUGFV0hQA2zLY/Z8AMrEx0qg=="], + "@babel/runtime-corejs2": ["@babel/runtime-corejs2@7.26.9", "", { "dependencies": { "core-js": "^2.6.12", "regenerator-runtime": "^0.14.0" } }, "sha512-DHEUkm9RD4RfIThlLTemmHNFVTj9Z/augVFMuyheFjwoFv1jFjauncHrcSW6Kv1TpzTupB01zFk2iRFAh2iE9A=="], - "@babel/runtime-corejs3": ["@babel/runtime-corejs3@7.26.7", "", { "dependencies": { "core-js-pure": "^3.30.2", "regenerator-runtime": "^0.14.0" } }, "sha512-55gRV8vGrCIYZnaQHQrD92Lo/hYE3Sj5tmbuf0hhHR7sj2CWhEhHU89hbq+UVDXvFG1zUVXJhUkEq1eAfqXtFw=="], + "@babel/runtime-corejs3": ["@babel/runtime-corejs3@7.26.9", "", { "dependencies": { "core-js-pure": "^3.30.2", "regenerator-runtime": "^0.14.0" } }, "sha512-5EVjbTegqN7RSJle6hMWYxO4voo4rI+9krITk+DWR+diJgGrjZjrIBnJhjrHYYQsFgI7j1w1QnrvV7YSKBfYGg=="], "@babel/template": ["@babel/template@7.25.9", "", { "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/parser": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg=="], @@ -601,8 +619,12 @@ "@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@workspace:packages/dicomImageLoader"], + "@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@workspace:packages/labelmap-interpolation"], + "@cornerstonejs/nifti-volume-loader": ["@cornerstonejs/nifti-volume-loader@workspace:packages/nifti-volume-loader"], + "@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@workspace:packages/polymorphic-segmentation"], + "@cornerstonejs/tools": ["@cornerstonejs/tools@workspace:packages/tools"], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], @@ -813,8 +835,6 @@ "@externals/dicom-microscopy-viewer": ["@externals/dicom-microscopy-viewer@workspace:addOns/externals/dicom-microscopy-viewer"], - "@externals/polyseg-wasm": ["@externals/polyseg-wasm@workspace:addOns/externals/polyseg-wasm"], - "@fastify/accept-negotiator": ["@fastify/accept-negotiator@1.1.0", "", {}, "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ=="], "@fastify/ajv-compiler": ["@fastify/ajv-compiler@3.6.0", "", { "dependencies": { "ajv": "^8.11.0", "ajv-formats": "^2.1.1", "fast-uri": "^2.0.0" } }, "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ=="], @@ -869,7 +889,7 @@ "@itk-wasm/dam": ["@itk-wasm/dam@1.1.1", "", { "dependencies": { "axios": "^1.4.0", "commander": "^10.0.1", "decompress": "^4.2.1", "files-from-path": "^1.0.0", "ipfs-car": "^1.0.0", "tar": "^6.1.13" }, "bin": { "dam": "cli.js" } }, "sha512-7+9L3lrLMKF4y6B6qjs8GqfbpxT0waOJUM14NdMNEA6M+BoBS8fdHREhQHo2s7QMA5O7I+Jv7m+dyqlisGnbdQ=="], - "@itk-wasm/morphological-contour-interpolation": ["@itk-wasm/morphological-contour-interpolation@1.0.1", "", { "dependencies": { "itk-wasm": "1.0.0-b.165" } }, "sha512-wxLB4nX6CiWpNQyTWC7oeFXogiZbtmSuLhyAtY66sM0SEnMoOcAuSX2+osPcOo13rfYnHLA02uQiICp8hvUGwA=="], + "@itk-wasm/morphological-contour-interpolation": ["@itk-wasm/morphological-contour-interpolation@1.1.0", "", { "dependencies": { "itk-wasm": "1.0.0-b.173" } }, "sha512-n6JIyDcSCCjlpfCW8mnTTzwPTE8U1QT87hNmyAknxdpGR4dfAzIutuKNrwgvr9UiKEBcit0X3HNx9dkzDwcIcw=="], "@jest/console": ["@jest/console@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "slash": "^3.0.0" } }, "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg=="], @@ -1821,6 +1841,8 @@ "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.3", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q=="], + "babel-plugin-transform-import-meta": ["babel-plugin-transform-import-meta@2.3.2", "", { "dependencies": { "@babel/template": "^7.25.9", "tslib": "^2.8.1" }, "peerDependencies": { "@babel/core": "^7.10.0" } }, "sha512-902o4GiQqI1GqAXfD5rEoz0PJamUfJ3VllpdWaNsFTwdaNjFSFHawvBO+cp5K2j+g2h3bZ4lnM1Xb6yFYGihtA=="], + "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.1.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw=="], "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], @@ -4843,7 +4865,7 @@ "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], - "strip-dirs": ["strip-dirs@3.0.0", "", { "dependencies": { "inspect-with-kind": "^1.0.5", "is-plain-obj": "^1.1.0" } }, "sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ=="], + "strip-dirs": ["strip-dirs@2.1.0", "", { "dependencies": { "is-natural-number": "^4.0.1" } }, "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g=="], "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], @@ -5429,8 +5451,12 @@ "@docsearch/react/algoliasearch": ["algoliasearch@5.20.0", "", { "dependencies": { "@algolia/client-abtesting": "5.20.0", "@algolia/client-analytics": "5.20.0", "@algolia/client-common": "5.20.0", "@algolia/client-insights": "5.20.0", "@algolia/client-personalization": "5.20.0", "@algolia/client-query-suggestions": "5.20.0", "@algolia/client-search": "5.20.0", "@algolia/ingestion": "1.20.0", "@algolia/monitoring": "1.20.0", "@algolia/recommend": "5.20.0", "@algolia/requester-browser-xhr": "5.20.0", "@algolia/requester-fetch": "5.20.0", "@algolia/requester-node-http": "5.20.0" } }, "sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ=="], + "@docusaurus/babel/@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.25.9", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.10.6", "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ=="], + "@docusaurus/babel/@babel/runtime": ["@babel/runtime@7.26.7", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ=="], + "@docusaurus/babel/@babel/runtime-corejs3": ["@babel/runtime-corejs3@7.26.7", "", { "dependencies": { "core-js-pure": "^3.30.2", "regenerator-runtime": "^0.14.0" } }, "sha512-55gRV8vGrCIYZnaQHQrD92Lo/hYE3Sj5tmbuf0hhHR7sj2CWhEhHU89hbq+UVDXvFG1zUVXJhUkEq1eAfqXtFw=="], + "@docusaurus/babel/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], "@docusaurus/bundler/babel-loader": ["babel-loader@9.2.1", "", { "dependencies": { "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" }, "peerDependencies": { "@babel/core": "^7.12.0", "webpack": ">=5" } }, "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA=="], @@ -5539,6 +5565,8 @@ "@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "@itk-wasm/morphological-contour-interpolation/itk-wasm": ["itk-wasm@1.0.0-b.173", "", { "dependencies": { "@itk-wasm/dam": "^1.1.1", "@thewtex/zstddec": "^0.2.0", "@types/emscripten": "^1.39.10", "axios": "^1.6.2", "chalk": "^5.3.0", "comlink": "^4.4.1", "commander": "^11.1.0", "fs-extra": "^11.2.0", "glob": "^8.1.0", "markdown-table": "^3.0.3", "mime-types": "^2.1.35", "wasm-feature-detect": "^1.6.1" }, "bin": { "itk-wasm": "src/itk-wasm-cli.js" } }, "sha512-SV2lfZ1mClWuSK/noaZgGj9jhroY4MZu19ci9pIucuyhoGdXrVSmWlPH/JYMDi9RP3BogmQwe9wfFc3X1dcEPg=="], + "@jest/core/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "@jest/core/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], @@ -5825,6 +5853,8 @@ "@web3-storage/car-block-validator/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], + "@xhmikosr/decompress/strip-dirs": ["strip-dirs@3.0.0", "", { "dependencies": { "inspect-with-kind": "^1.0.5", "is-plain-obj": "^1.1.0" } }, "sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ=="], + "@xhmikosr/decompress-tar/tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], "@zkochan/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -5997,6 +6027,8 @@ "cssstyle/cssom": ["cssom@0.3.8", "", {}, "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="], + "dcmjs/@babel/runtime-corejs3": ["@babel/runtime-corejs3@7.26.7", "", { "dependencies": { "core-js-pure": "^3.30.2", "regenerator-runtime": "^0.14.0" } }, "sha512-55gRV8vGrCIYZnaQHQrD92Lo/hYE3Sj5tmbuf0hhHR7sj2CWhEhHU89hbq+UVDXvFG1zUVXJhUkEq1eAfqXtFw=="], + "decamelize-keys/decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], "decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="], @@ -6005,8 +6037,6 @@ "decompress/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], - "decompress/strip-dirs": ["strip-dirs@2.1.0", "", { "dependencies": { "is-natural-number": "^4.0.1" } }, "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g=="], - "decompress-tar/file-type": ["file-type@5.2.0", "", {}, "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ=="], "decompress-tar/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], @@ -6851,8 +6881,6 @@ "stringify-object/is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="], - "strip-dirs/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], - "stylelint/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], "stylelint/file-entry-cache": ["file-entry-cache@7.0.2", "", { "dependencies": { "flat-cache": "^3.2.0" } }, "sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g=="], @@ -7039,8 +7067,12 @@ "@docsearch/react/algoliasearch/@algolia/requester-node-http": ["@algolia/requester-node-http@5.20.0", "", { "dependencies": { "@algolia/client-common": "5.20.0" } }, "sha512-kmtQClq/w3vtPteDSPvaW9SPZL/xrIgMrxZyAgsFwrJk0vJxqyC5/hwHmrCraDnStnGSADnLpBf4SpZnwnkwWw=="], + "@docusaurus/babel/@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@docusaurus/babel/@babel/runtime/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], + "@docusaurus/babel/@babel/runtime-corejs3/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], + "@docusaurus/bundler/babel-loader/find-cache-dir": ["find-cache-dir@4.0.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" } }, "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg=="], "@docusaurus/bundler/html-minifier-terser/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -7211,6 +7243,14 @@ "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "@itk-wasm/morphological-contour-interpolation/itk-wasm/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], + + "@itk-wasm/morphological-contour-interpolation/itk-wasm/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "@itk-wasm/morphological-contour-interpolation/itk-wasm/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "@itk-wasm/morphological-contour-interpolation/itk-wasm/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], + "@jest/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "@jest/reporters/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -7469,6 +7509,8 @@ "@vercel/nft/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "@xhmikosr/decompress/strip-dirs/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + "archiver-utils/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "archiver/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -7587,6 +7629,8 @@ "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], + "dcmjs/@babel/runtime-corejs3/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], + "decompress-tar/tar-stream/bl": ["bl@1.2.3", "", { "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" } }, "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww=="], "decompress/make-dir/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], @@ -7617,6 +7661,8 @@ "detective-typescript/@typescript-eslint/typescript-estree/globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + "docs/dcmjs/@babel/runtime-corejs3": ["@babel/runtime-corejs3@7.26.7", "", { "dependencies": { "core-js-pure": "^3.30.2", "regenerator-runtime": "^0.14.0" } }, "sha512-55gRV8vGrCIYZnaQHQrD92Lo/hYE3Sj5tmbuf0hhHR7sj2CWhEhHU89hbq+UVDXvFG1zUVXJhUkEq1eAfqXtFw=="], + "docs/typedoc/markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="], "docs/typedoc/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -8385,6 +8431,8 @@ "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "@itk-wasm/morphological-contour-interpolation/itk-wasm/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "@jsdevtools/coverage-istanbul-loader/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "@lerna/create/execa/npm-run-path/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -8569,6 +8617,8 @@ "detective-typescript/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "docs/dcmjs/@babel/runtime-corejs3/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], + "docs/typedoc/markdown-it/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "docs/typedoc/markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -8909,6 +8959,8 @@ "@docusaurus/utils/webpack/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "@itk-wasm/morphological-contour-interpolation/itk-wasm/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "@lerna/create/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "@netlify/build-info/read-pkg/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], @@ -9045,6 +9097,8 @@ "@docusaurus/core/update-notifier/boxen/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "@itk-wasm/morphological-contour-interpolation/itk-wasm/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@lerna/create/rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@netlify/functions-utils/@netlify/zip-it-and-ship-it/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], diff --git a/commit.txt b/commit.txt index 963e73499b..11312f83d2 100644 --- a/commit.txt +++ b/commit.txt @@ -1 +1 @@ -0dc51241392451a581de0372760ff6d5acf398c3 \ No newline at end of file +a1fd3f9d0ea40d53cafd792d59bc1dbfc90663a5 diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 7a579bc5b1..8902b6d92c 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -275,6 +275,9 @@ type BoundsLPS = [Point3, Point3, Point3]; // @public (undocumented) export const cache: Cache_2; +// @public (undocumented) +function calculateSpacingBetweenImageIds(imageIds: string[]): number; + // @public (undocumented) function calculateViewportsSpatialRegistration(viewport1: StackViewport | IVolumeViewport, viewport2: StackViewport | IVolumeViewport): void; @@ -1191,6 +1194,9 @@ interface FlipDirection { flipVertical?: boolean; } +// @public (undocumented) +function fnv1aHash(str: string): string; + // @public (undocumented) class FrameRange { // (undocumented) @@ -3963,6 +3969,7 @@ declare namespace utilities { scaleRGBTransferFunction as scaleRgbTransferFunction, triggerEvent, imageIdToURI, + fnv1aHash, metadataProvider as calibratedPixelSpacingMetadataProvider, clamp, uuidv4, @@ -4047,7 +4054,8 @@ declare namespace utilities { clip, transformWorldToIndexContinuous, createSubVolume, - getVolumeDirectionVectors + getVolumeDirectionVectors, + calculateSpacingBetweenImageIds } } export { utilities } diff --git a/common/reviews/api/labelmap-interpolation.api.md b/common/reviews/api/labelmap-interpolation.api.md new file mode 100644 index 0000000000..272d39e37d --- /dev/null +++ b/common/reviews/api/labelmap-interpolation.api.md @@ -0,0 +1,18 @@ +## API Report File for "@cornerstonejs/labelmap-interpolation" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export function interpolate({ segmentationId, segmentIndex, configuration, }: { + segmentationId: string; + segmentIndex: number; + configuration?: MorphologicalContourInterpolationOptions & { + preview: boolean; + }; +}): Promise; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/common/reviews/api/polymorphic-segmentation.api.md b/common/reviews/api/polymorphic-segmentation.api.md new file mode 100644 index 0000000000..c2de14194a --- /dev/null +++ b/common/reviews/api/polymorphic-segmentation.api.md @@ -0,0 +1,48 @@ +## API Report File for "@cornerstonejs/polymorphic-segmentation" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type ColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import type { mat3 } from 'gl-matrix'; +import { mat4 } from 'gl-matrix'; +import type { Range as Range_2 } from '@kitware/vtk.js/types'; +import { vec3 } from 'gl-matrix'; +import type vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import type { vtkCamera } from '@kitware/vtk.js/Rendering/Core/Camera'; +import { vtkColorTransferFunction } from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import { vtkImageData } from '@kitware/vtk.js/Common/DataModel/ImageData'; +import type vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice'; +import type { vtkObject } from '@kitware/vtk.js/interfaces'; +import type vtkOpenGLTexture from '@kitware/vtk.js/Rendering/OpenGL/Texture'; +import type vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; +import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane'; +import type vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; +import type vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume'; + +// @public (undocumented) +export function canComputeRequestedRepresentation(segmentationId: string, type: typeof SegmentationRepresentations): boolean; + +// @public (undocumented) +export function computeContourData(segmentationId: string, options?: PolySegConversionOptions): Promise<{ + annotationUIDsMap: Map>; +}>; + +// @public (undocumented) +export function computeLabelmapData(segmentationId: string, options?: PolySegConversionOptions): Promise; + +// @public (undocumented) +export function computeSurfaceData(segmentationId: string, options?: PolySegConversionOptions): Promise<{ + geometryIds: Map; +}>; + +// @public (undocumented) +export function init(): void; + +// @public (undocumented) +export function updateSurfaceData(segmentationId: any): Promise; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 39ab0f21a1..44e8aa903d 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -405,10 +405,10 @@ type AnnotationLockChangeEventType = Types_2.CustomEventType NamedStatistics; // (undocumented) - static statsCallback: ({ value: newValue, pointLPS }: { + static statsCallback: ({ value: newValue, pointLPS, pointIJK, }: { value: any; pointLPS?: any; + pointIJK?: any; }) => void; // (undocumented) static statsInit(options: { @@ -948,9 +977,6 @@ function calibrateImageSpacing(imageId: string, renderingEngine: Types_2.IRender // @public (undocumented) export function cancelActiveManipulations(element: HTMLDivElement): string | undefined; -// @public (undocumented) -function canComputeRequestedRepresentation(segmentationId: string, type: SegmentationRepresentations): boolean; - // @public (undocumented) type CanvasCoordinates = [ Types_2.Point2, @@ -1004,6 +1030,10 @@ enum ChangeTypes { // @public (undocumented) enum ChangeTypes_2 { + // (undocumented) + COMPUTE_STATISTICS = "Computing Statistics", + // (undocumented) + INTERPOLATE_LABELMAP = "Interpolating Labelmap", // (undocumented) POLYSEG_CONTOUR_TO_LABELMAP = "Converting Contour to Labelmap", // (undocumented) @@ -1022,6 +1052,9 @@ function checkAndSetAnnotationLocked(annotationUID: string): boolean; // @public (undocumented) function checkAndSetAnnotationVisibility(annotationUID: string): boolean; +// @public (undocumented) +function checkStandardBasis(directions: number[]): RotationMatrixInformation; + declare namespace cine { export { playClip, @@ -1311,7 +1344,7 @@ export class CircleScissorsTool extends LabelmapBaseTool { } // @public (undocumented) -function clip_2(a: any, b: any, box: any, da?: any, db?: any): 0 | 1; +function clip_2(a: any, b: any, box: any, da?: any, db?: any): 1 | 0; // @public (undocumented) type ClosestControlPoint = ClosestPoint & { @@ -1570,16 +1603,15 @@ type ColorbarTicksStyle = { type ColorbarVOIRange = ColorbarImageRange; // @public (undocumented) -function computeAndAddContourRepresentation(segmentationId: string, options?: PolySegConversionOptions): Promise<{ - annotationUIDsMap: Map>; +function computeStackLabelmapFromVolume({ volumeId, }: { + volumeId: string; +}): Promise<{ + imageIds: string[]; }>; // @public (undocumented) -function computeAndAddLabelmapRepresentation(segmentationId: string, options?: PolySegConversionOptions): Promise; - -// @public (undocumented) -function computeAndAddSurfaceRepresentation(segmentationId: string, options?: PolySegConversionOptions): Promise<{ - geometryIds: Map; +function computeVolumeLabelmapFromStack(args: any): Promise<{ + volumeId: string; }>; declare namespace config { @@ -1823,10 +1855,6 @@ export class CrosshairsTool extends AnnotationTool { // (undocumented) _dragCallback: (evt: EventTypes_2.InteractionEventType) => void; // (undocumented) - editData: { - annotation: Annotation; - } | null; - // (undocumented) _endCallback: (evt: EventTypes_2.InteractionEventType) => void; // (undocumented) _filterAnnotationsByUniqueViewportOrientations: (enabledElement: any, annotations: any) => any[]; @@ -1971,6 +1999,9 @@ const _default_4: { smoothAnnotation: typeof smoothAnnotation; }; +// @public (undocumented) +const defaultSegmentationStateManager: SegmentationStateManager; + // @public (undocumented) function deselectAnnotation(annotationUID?: string): void; @@ -2635,6 +2666,14 @@ function generateImageFromTimeData(dynamicVolume: Types_2.IDynamicImageVolume, o frameNumbers?: number[]; }): Float32Array; +declare namespace geometricSurfaceUtils { + export { + inverse3x3Matrix, + checkStandardBasis, + rotatePoints + } +} + // @public (undocumented) function getAABB(polyline: Types_2.Point2[] | Types_2.Point3[] | number[], options?: { numDimensions: number; @@ -2819,12 +2858,18 @@ function getNormal2(polyline: Types_2.Point2[]): Types_2.Point3; // @public (undocumented) function getNormal3(polyline: Types_2.Point3[]): Types_2.Point3; +// @public (undocumented) +function getOrCreateSegmentationVolume(segmentationId: any): any; + // @public (undocumented) function getOrientationStringLPS(vector: Types_2.Point3): string; // @public (undocumented) function getPixelValueUnits(modality: string, imageId: string, options: pixelUnitsOptions): string; +// @public (undocumented) +function getPixelValueUnitsImageId(imageId: string, options: pixelUnitsOptions): string; + // @public (undocumented) function getPoint(points: any, idx: any): Types_2.Point3; @@ -2903,6 +2948,12 @@ function getStackSegmentationImageIdsForViewport(viewportId: string, segmentatio // @public (undocumented) function getState(annotation?: Annotation): AnnotationStyleStates; +// @public (undocumented) +function getStatistics({ segmentationId, segmentIndices, }: { + segmentationId: string; + segmentIndices: number[] | number; +}): Promise; + // @public (undocumented) function getStyle(specifier: SpecifierWithType): StyleForType; @@ -3134,7 +3185,16 @@ class ImageMouseCursor extends MouseCursor { } // @public (undocumented) -export function init(defaultConfiguration?: {}): void; +type InactiveLabelmapStyle = { + renderOutlineInactive?: boolean; + outlineWidthInactive?: number; + renderFillInactive?: boolean; + fillAlphaInactive?: number; + outlineOpacityInactive?: number; +}; + +// @public (undocumented) +export function init(defaultConfiguration?: Config): void; // @public (undocumented) function initElementCursor(element: HTMLDivElement, cursor: MouseCursor | null): void; @@ -3185,6 +3245,9 @@ function intersectPolyline(sourcePolyline: Types_2.Point2[], targetPolyline: Typ // @public (undocumented) function invalidateBrushCursor(toolGroupId: string): void; +// @public (undocumented) +function inverse3x3Matrix(matrix: number[]): number[]; + // @public (undocumented) function invertOrientationStringLPS(orientationString: string): string; @@ -3397,38 +3460,47 @@ type KeyDownEventDetail = { // @public (undocumented) type KeyDownEventType = Types_2.CustomEventType; +// @public (undocumented) +type KeyImageAnnotation = ProbeAnnotation & { + data: { + isPoint: boolean; + seriesLevel: boolean; + }; +}; + // @public (undocumented) export class KeyImageTool extends AnnotationTool { constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps); // (undocumented) - _activateModify: (element: HTMLDivElement) => void; + _activateModify: (element: any) => void; // (undocumented) addNewAnnotation: (evt: EventTypes_2.InteractionEventType) => Annotation; // (undocumented) - cancel(): void; + cancel(element: HTMLDivElement): string; // (undocumented) - _deactivateModify: (element: HTMLDivElement) => void; + static dataPoint: { + data: { + isPoint: boolean; + }; + }; + // (undocumented) + static dataSeries: { + data: { + seriesLevel: boolean; + }; + }; + // (undocumented) + _deactivateModify: (element: any) => void; // (undocumented) _doneChangingTextCallback(element: any, annotation: any, updatedText: any): void; // (undocumented) doubleClickCallback: (evt: EventTypes_2.TouchTapEventType) => void; // (undocumented) - editData: { - annotation: Annotation; - viewportIdsToRender: string[]; - handleIndex?: number; - movingTextBox?: boolean; - newAnnotation?: boolean; - hasMoved?: boolean; - } | null; + _dragCallback: (evt: any) => void; // (undocumented) _endCallback: (evt: EventTypes_2.InteractionEventType) => void; // (undocumented) - handleSelectedCallback(evt: EventTypes_2.InteractionEventType, annotation: Annotation, handle: ToolHandle): void; - // (undocumented) - isDrawing: boolean; - // (undocumented) - isHandleOutsideImage: boolean; + handleSelectedCallback(evt: EventTypes_2.InteractionEventType, annotation: KeyImageAnnotation): void; // (undocumented) _isInsideVolume(index1: any, index2: any, dimensions: any): boolean; // (undocumented) @@ -3436,6 +3508,8 @@ export class KeyImageTool extends AnnotationTool { // (undocumented) renderAnnotation: (enabledElement: Types_2.IEnabledElement, svgDrawingHelper: SVGDrawingHelper) => boolean; // (undocumented) + static setPoint(annotation: any, isPoint?: boolean, element?: any): void; + // (undocumented) _throttledCalculateCachedStats: Function; // (undocumented) static toolName: string; @@ -3512,12 +3586,11 @@ export class LabelmapBaseTool extends BaseTool { segmentColor: Types_2.Color; }; // (undocumented) - protected getEditData({ viewport, representationData, segmentsLocked, segmentationId, volumeOperation, }: { + protected getEditData({ viewport, representationData, segmentsLocked, segmentationId, }: { viewport: any; representationData: any; segmentsLocked: any; segmentationId: any; - volumeOperation?: boolean; }): EditDataReturnType; // (undocumented) protected getOperationData(element?: any): ModifiedLabelmapToolOperationData; @@ -3562,6 +3635,25 @@ type LabelmapMemo_2 = Types_2.Memo & { memo?: LabelmapMemo_2; }; +// @public (undocumented) +type LabelmapSegmentationData = LabelmapSegmentationDataVolume | LabelmapSegmentationDataStack | { + volumeId?: string; + referencedVolumeId?: string; + referencedImageIds?: string[]; + imageIds?: string[]; +}; + +// @public (undocumented) +type LabelmapSegmentationDataStack = { + imageIds: string[]; +}; + +// @public (undocumented) +type LabelmapSegmentationDataVolume = { + volumeId: string; + referencedVolumeId?: string; +}; + // @public (undocumented) type LabelmapStyle = BaseLabelmapStyle & InactiveLabelmapStyle; @@ -3573,7 +3665,7 @@ type LabelmapToolOperationData = { segmentsLocked: number[]; viewPlaneNormal: number[]; viewUp: number[]; - strategySpecificConfiguration: any; + activeStrategy: string; points: Types_2.Point3[]; voxelManager: any; override: { @@ -4402,22 +4494,6 @@ declare namespace polyline { } } -// @public (undocumented) -type PolySegConversionOptions = { - segmentIndices?: number[]; - segmentationId?: string; - viewport?: Types_2.IStackViewport | Types_2.IVolumeViewport; -}; - -declare namespace polySegManager { - export { - canComputeRequestedRepresentation, - computeAndAddSurfaceRepresentation, - computeAndAddLabelmapRepresentation, - computeAndAddContourRepresentation - } -} - // @public (undocumented) const precalculatePointInEllipse: (ellipse: any, inverts?: Inverts) => Inverts; @@ -4455,19 +4531,8 @@ export class ProbeTool extends AnnotationTool { // (undocumented) _dragCallback: (evt: any) => void; // (undocumented) - editData: { - annotation: Annotation; - viewportIdsToRender: string[]; - newAnnotation?: boolean; - } | null; - // (undocumented) _endCallback: (evt: EventTypes_2.InteractionEventType) => void; // (undocumented) - eventDispatchDetail: { - viewportId: string; - renderingEngineId: string; - }; - // (undocumented) getHandleNearImagePoint(element: HTMLDivElement, annotation: ProbeAnnotation, canvasCoords: Types_2.Point2, proximity: number): ToolHandle | undefined; // (undocumented) handleSelectedCallback(evt: EventTypes_2.InteractionEventType, annotation: ProbeAnnotation): void; @@ -4476,10 +4541,6 @@ export class ProbeTool extends AnnotationTool { annotationUID?: string; }) => ProbeAnnotation; // (undocumented) - isDrawing: boolean; - // (undocumented) - isHandleOutsideImage: boolean; - // (undocumented) isPointNearTool(element: HTMLDivElement, annotation: ProbeAnnotation, canvasCoords: Types_2.Point2, proximity: number): boolean; // (undocumented) static probeDefaults: { @@ -5092,6 +5153,9 @@ interface ROICachedStats { }; } +// @public (undocumented) +function rotatePoints(rotationMatrix: number[], origin: number[], points: number[]): number[]; + // @public (undocumented) const roundNumber_2: typeof utilities_2.roundNumber; @@ -5246,11 +5310,12 @@ declare namespace segmentation { segmentIndex, triggerSegmentationEvents, helpers, - polySegManager as polySeg, removeSegment, getLabelmapImageIds, internalAddRepresentationData as addRepresentationData, - strategies + strategies, + segmentationStyle, + defaultSegmentationStateManager } } export { segmentation } @@ -5281,7 +5346,12 @@ declare namespace segmentation_2 { getBrushToolInstances, growCut, LabelmapMemo, - IslandRemoval + IslandRemoval, + getOrCreateSegmentationVolume, + getStatistics, + validateLabelmap, + computeStackLabelmapFromVolume, + computeVolumeLabelmapFromStack } } @@ -5404,6 +5474,9 @@ type SegmentationState = { }; }; +// @public (undocumented) +const segmentationStyle: SegmentationStyle; + // @public (undocumented) function segmentContourAction(element: HTMLDivElement, configuration: any): any; @@ -5479,7 +5552,11 @@ function setAttributesIfNecessary(attributes: any, svgNode: any): void; function setBrushSizeForToolGroup(toolGroupId: string, brushSize: number, toolName?: string): void; // @public (undocumented) -function setBrushThresholdForToolGroup(toolGroupId: string, threshold: Types_2.Point2, otherArgs?: Record): void; +function setBrushThresholdForToolGroup(toolGroupId: string, threshold: { + range: Types_2.Point2; + isDynamic: boolean; + dynamicRadius: number; +}): void; // @public (undocumented) function setColorLUT(viewportId: string, segmentationId: string, colorLUTsIndex: number): void; @@ -5889,6 +5966,8 @@ type Statistics = { label?: string; value: number | number[]; unit: null | string; + pointIJK?: Types_2.Point3; + pointLPS?: Types_2.Point3; }; // @public (undocumented) @@ -5931,6 +6010,10 @@ enum StrategyCallbacks { // (undocumented) CreateIsInThreshold = "createIsInThreshold", // (undocumented) + EnsureImageVolumeFor3DManipulation = "ensureImageVolumeFor3DManipulation", + // (undocumented) + EnsureSegmentationVolumeFor3DManipulation = "ensureSegmentationVolumeFor3DManipulation", + // (undocumented) Fill = "fill", // (undocumented) GetStatistics = "getStatistics", @@ -5988,6 +6071,14 @@ type StyleSpecifier = { // @public (undocumented) function subtractPolylines(targetPolyline: Types_2.Point2[], sourcePolyline: Types_2.Point2[]): Types_2.Point2[][]; +// @public (undocumented) +type SurfaceSegmentationData = { + geometryIds: Map; +}; + +// @public (undocumented) +type SurfaceStateStyles = {}; + // @public (undocumented) type SurfaceStyle = {}; @@ -6307,6 +6398,7 @@ declare namespace ToolSpecificAnnotationTypes { ROICachedStats, RectangleROIAnnotation, ProbeAnnotation, + KeyImageAnnotation, LengthAnnotation, AdvancedMagnifyAnnotation, CircleROIAnnotation, @@ -6552,14 +6644,20 @@ declare namespace Types { SplineCurveSegment, SplineLineSegment, SplineProps, - PolySegConversionOptions, IBaseTool, RepresentationStyle, Segment, SegmentationPublicInput, LabelmapStyle, ContourStyle, - SurfaceStyle + SurfaceStyle, + SurfaceSegmentationData, + SurfaceStateStyles, + LabelmapSegmentationData, + LabelmapSegmentationDataStack, + LabelmapSegmentationDataVolume, + BaseLabelmapStyle, + InactiveLabelmapStyle } } export { Types } @@ -6711,6 +6809,7 @@ declare namespace utilities { getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, + getPixelValueUnitsImageId, segmentation_2 as segmentation, contours, triggerAnnotationRenderForViewportIds, @@ -6737,11 +6836,25 @@ declare namespace utilities { getClosestImageIdForStackViewport, pointInSurroundingSphereCallback, normalizeViewportPlane, - IslandRemoval + IslandRemoval, + geometricSurfaceUtils } } export { utilities } +// @public (undocumented) +function validate(segmentationRepresentationData: LabelmapSegmentationData): void; + +declare namespace validateLabelmap { + export { + validatePublic, + validate + } +} + +// @public (undocumented) +function validatePublic(segmentationInput: SegmentationPublicInput): void; + declare namespace vec2 { export { findClosestPoint, diff --git a/jest.config.base.js b/jest.config.base.js index 944a17e4b7..cac2667b25 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -18,6 +18,14 @@ module.exports = { // Setup // setupFiles: ["jest-canvas-mock/lib/index.js"], // Coverage + transform: { + '^.+\\.(js|jsx|ts|tsx)$': [ + 'babel-jest', + { + plugins: ['babel-plugin-transform-import-meta'], + }, + ], + }, collectCoverageFrom: [ '/src/**/*.{js,jsx}', // Not diff --git a/lerna.json b/lerna.json index fabee9f984..de063874cc 100644 --- a/lerna.json +++ b/lerna.json @@ -1,12 +1,14 @@ { - "version": "2.19.16", + "version": "3.0.0-beta.6", "packages": [ "packages/core", "packages/tools", "packages/adapters", "packages/nifti-volume-loader", "packages/dicomImageLoader", - "packages/ai" + "packages/ai", + "packages/labelmap-interpolation", + "packages/polymorphic-segmentation" ], "npmClient": "yarn" } diff --git a/package.json b/package.json index c8086e38e0..a1a1575346 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,13 @@ }, "scripts": { "api-check": "npx lerna run api-check", - "format-check": "npx lerna run format-check", + "format-check": "cross-env NODE_OPTIONS=--max_old_space_size=32896 npx lerna run format-check", "build": "npx lerna run build --stream && npx lerna run build:loader", "build:esm": "npx lerna run build:esm --stream", "watch": "npx lerna watch -- lerna run build --scope=$LERNA_PACKAGE_NAME --include-dependents", "build:update-api:ai": "cd packages/ai && npm run build:update-api", + "build:update-api:labelmap-interpolation": "cd packages/labelmap-interpolation && npm run build:update-api", + "build:update-api:polymorphic-segmentation": "cd packages/polymorphic-segmentation && npm run build:update-api", "build:update-api:core": "cd packages/core && npm run build:update-api", "build:update-api:tools": "cd packages/tools && npm run build:update-api", "build:update-api:nifti": "cd packages/nifti-volume-loader && npm run build:update-api", @@ -95,6 +97,7 @@ "autoprefixer": "^10.4.14", "babel-loader": "9.1.2", "babel-plugin-istanbul": "^6.1.1", + "babel-plugin-transform-import-meta": "2.3.2", "chai": "^5.1.2", "clean-webpack-plugin": "^4.0.0", "commander": "^10.0.1", @@ -184,5 +187,6 @@ "not ie < 11", "not op_mini all" ], - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", + "dependencies": {} } diff --git a/packages/adapters/.babelrc b/packages/adapters/.babelrc deleted file mode 100644 index d15bf49ffb..0000000000 --- a/packages/adapters/.babelrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "presets": [ - ["@babel/preset-env", { - "targets": { - "browsers": ["ie 11"] - } - }] - ] -} \ No newline at end of file diff --git a/packages/adapters/CHANGELOG.md b/packages/adapters/CHANGELOG.md index 405ced3300..87910754d2 100644 --- a/packages/adapters/CHANGELOG.md +++ b/packages/adapters/CHANGELOG.md @@ -3,125 +3,11 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [2.19.16](https://github.com/dcmjs-org/dcmjs/compare/v2.19.15...v2.19.16) (2025-02-26) +# [3.0.0-beta.6](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2025-02-27) -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.15](https://github.com/dcmjs-org/dcmjs/compare/v2.19.14...v2.19.15) (2025-02-26) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.14](https://github.com/dcmjs-org/dcmjs/compare/v2.19.13...v2.19.14) (2025-02-24) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.13](https://github.com/dcmjs-org/dcmjs/compare/v2.19.12...v2.19.13) (2025-02-21) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.12](https://github.com/dcmjs-org/dcmjs/compare/v2.19.11...v2.19.12) (2025-02-20) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.11](https://github.com/dcmjs-org/dcmjs/compare/v2.19.10...v2.19.11) (2025-02-18) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.10](https://github.com/dcmjs-org/dcmjs/compare/v2.19.9...v2.19.10) (2025-02-18) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.9](https://github.com/dcmjs-org/dcmjs/compare/v2.19.8...v2.19.9) (2025-02-18) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.8](https://github.com/dcmjs-org/dcmjs/compare/v2.19.7...v2.19.8) (2025-02-18) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.7](https://github.com/dcmjs-org/dcmjs/compare/v2.19.6...v2.19.7) (2025-02-06) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.6](https://github.com/dcmjs-org/dcmjs/compare/v2.19.5...v2.19.6) (2025-02-05) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.5](https://github.com/dcmjs-org/dcmjs/compare/v2.19.4...v2.19.5) (2025-02-04) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.4](https://github.com/dcmjs-org/dcmjs/compare/v2.19.3...v2.19.4) (2025-02-04) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.3](https://github.com/dcmjs-org/dcmjs/compare/v2.19.2...v2.19.3) (2025-02-04) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.2](https://github.com/dcmjs-org/dcmjs/compare/v2.19.1...v2.19.2) (2025-02-03) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.19.1](https://github.com/dcmjs-org/dcmjs/compare/v2.19.0...v2.19.1) (2025-02-03) - -**Note:** Version bump only for package @cornerstonejs/adapters - -# [2.19.0](https://github.com/dcmjs-org/dcmjs/compare/v2.18.9...v2.19.0) (2025-02-03) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.18.9](https://github.com/dcmjs-org/dcmjs/compare/v2.18.8...v2.18.9) (2025-02-03) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.18.8](https://github.com/dcmjs-org/dcmjs/compare/v2.18.7...v2.18.8) (2025-02-03) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.18.7](https://github.com/dcmjs-org/dcmjs/compare/v2.18.6...v2.18.7) (2025-02-03) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.18.6](https://github.com/dcmjs-org/dcmjs/compare/v2.18.5...v2.18.6) (2025-02-03) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.18.5](https://github.com/dcmjs-org/dcmjs/compare/v2.18.4...v2.18.5) (2025-02-03) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.18.4](https://github.com/dcmjs-org/dcmjs/compare/v2.18.3...v2.18.4) (2025-02-03) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.18.3](https://github.com/dcmjs-org/dcmjs/compare/v2.18.2...v2.18.3) (2025-01-31) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.18.2](https://github.com/dcmjs-org/dcmjs/compare/v2.18.1...v2.18.2) (2025-01-31) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.18.1](https://github.com/dcmjs-org/dcmjs/compare/v2.18.0...v2.18.1) (2025-01-31) - -**Note:** Version bump only for package @cornerstonejs/adapters - -# [2.18.0](https://github.com/dcmjs-org/dcmjs/compare/v2.17.6...v2.18.0) (2025-01-30) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.17.6](https://github.com/dcmjs-org/dcmjs/compare/v2.17.5...v2.17.6) (2025-01-29) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.17.5](https://github.com/dcmjs-org/dcmjs/compare/v2.17.4...v2.17.5) (2025-01-27) - -**Note:** Version bump only for package @cornerstonejs/adapters - -## [2.17.4](https://github.com/dcmjs-org/dcmjs/compare/v2.17.3...v2.17.4) (2025-01-24) +### Features -**Note:** Version bump only for package @cornerstonejs/adapters +- Add key image adapters for key image point mark ([#1754](https://github.com/cornerstonejs/cornerstone3D/issues/1754)) ([a1fd3f9](https://github.com/cornerstonejs/cornerstone3D/commit/a1fd3f9d0ea40d53cafd792d59bc1dbfc90663a5)) ## [2.17.3](https://github.com/dcmjs-org/dcmjs/compare/v2.17.2...v2.17.3) (2025-01-22) diff --git a/packages/adapters/babel.config.js b/packages/adapters/babel.config.js new file mode 100644 index 0000000000..fed6f05fec --- /dev/null +++ b/packages/adapters/babel.config.js @@ -0,0 +1 @@ +module.exports = require("../../babel.config.js"); diff --git a/packages/adapters/jest.config.js b/packages/adapters/jest.config.js new file mode 100644 index 0000000000..bd1f3dad96 --- /dev/null +++ b/packages/adapters/jest.config.js @@ -0,0 +1,11 @@ +/* eslint-disable */ +const base = require("../../jest.config.base.js"); +const path = require("path"); + +module.exports = { + ...base, + displayName: "adapters", + moduleNameMapper: { + "^@cornerstonejs/(.*)$": path.resolve(__dirname, "../$1/src") + } +}; diff --git a/packages/adapters/package.json b/packages/adapters/package.json index f51f4e8484..4da66ce6b9 100644 --- a/packages/adapters/package.json +++ b/packages/adapters/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/adapters", - "version": "2.19.16", + "version": "3.0.0-beta.6", "description": "Adapters for Cornerstone3D to/from formats including DICOM SR and others", "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", @@ -68,9 +68,9 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/dcmjs-org/dcmjs.git" + "url": "git+https://github.com/cornerstonejs/cornerstone3D.git" }, - "author": "@cornerstonejs (formerly Steve Pieper from dcmjs)", + "author": "@cornerstonejs adapters package", "license": "MIT", "bugs": { "url": "https://github.com/cornerstonejs/cornerstone3D/issues" @@ -84,7 +84,7 @@ "ndarray": "^1.0.19" }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.16", - "@cornerstonejs/tools": "^2.19.16" + "@cornerstonejs/core": "^3.0.0-beta.6", + "@cornerstonejs/tools": "^3.0.0-beta.6" } } diff --git a/packages/adapters/rollup.config.mjs b/packages/adapters/rollup.config.mjs index 30bac3ad56..ac7da0b622 100644 --- a/packages/adapters/rollup.config.mjs +++ b/packages/adapters/rollup.config.mjs @@ -49,7 +49,7 @@ export default [ }), babel({ exclude: "node_modules/**", - babelHelpers: "bundled", + babelHelpers: "runtime", extensions: [".js", ".ts"] }), json() diff --git a/packages/adapters/src/adapters/Cornerstone/ArrowAnnotate.js b/packages/adapters/src/adapters/Cornerstone/ArrowAnnotate.js index bf37e4edd3..46d7a026bc 100644 --- a/packages/adapters/src/adapters/Cornerstone/ArrowAnnotate.js +++ b/packages/adapters/src/adapters/Cornerstone/ArrowAnnotate.js @@ -61,7 +61,8 @@ class ArrowAnnotate { static getTID300RepresentationArguments(tool) { const points = [tool.handles.start, tool.handles.end]; - let { finding, findingSites } = tool; + const { findingSites } = tool; + let { finding } = tool; const TID300RepresentationArguments = { points, diff --git a/packages/adapters/src/adapters/Cornerstone/MeasurementReport.js b/packages/adapters/src/adapters/Cornerstone/MeasurementReport.js index 929980af68..6c8a9aea6a 100644 --- a/packages/adapters/src/adapters/Cornerstone/MeasurementReport.js +++ b/packages/adapters/src/adapters/Cornerstone/MeasurementReport.js @@ -31,16 +31,11 @@ const codeValueMatch = (group, code, oldCode) => { ); }; -function getTID300ContentItem( - tool, - toolType, - ReferencedSOPSequence, - toolClass -) { - const args = toolClass.getTID300RepresentationArguments(tool); +function getTID300ContentItem(tool, ReferencedSOPSequence, adapterClass) { + const args = adapterClass.getTID300RepresentationArguments(tool); args.ReferencedSOPSequence = ReferencedSOPSequence; - const TID300Measurement = new toolClass.TID300Representation(args); + const TID300Measurement = new adapterClass.TID300Representation(args); return TID300Measurement; } @@ -61,12 +56,7 @@ function getMeasurementGroup(toolType, toolData, ReferencedSOPSequence) { // Loop through the array of tool instances // for this tool const Measurements = toolTypeData.data.map(tool => { - return getTID300ContentItem( - tool, - toolType, - ReferencedSOPSequence, - toolClass - ); + return getTID300ContentItem(tool, ReferencedSOPSequence, toolClass); }); return new TID1501MeasurementGroup(Measurements); diff --git a/packages/adapters/src/adapters/Cornerstone3D/Angle.ts b/packages/adapters/src/adapters/Cornerstone3D/Angle.ts index 4464d80a13..327e09599a 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Angle.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Angle.ts @@ -1,29 +1,14 @@ import { utilities } from "dcmjs"; -import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; import MeasurementReport from "./MeasurementReport"; +import BaseAdapter3D from "./BaseAdapter3D"; const { CobbAngle: TID300CobbAngle } = utilities.TID300; -const MEASUREMENT_TYPE = "Angle"; -const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${MEASUREMENT_TYPE}`; - -class Angle { - public static toolType = MEASUREMENT_TYPE; - public static utilityToolType = MEASUREMENT_TYPE; - public static TID300Representation = TID300CobbAngle; - public static isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - if (!TrackingIdentifier.includes(":")) { - return false; - } - - const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - - if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { - return false; - } - - return toolType === MEASUREMENT_TYPE; - }; +class Angle extends BaseAdapter3D { + static { + this.init("Angle", TID300CobbAngle); + this.registerLegacy(); + } // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. public static getMeasurementData( @@ -107,13 +92,11 @@ class Angle { point3, point4, rAngle: angle, - trackingIdentifierTextValue, + trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [] }; } } -MeasurementReport.registerTool(Angle); - export default Angle; diff --git a/packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.js b/packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.ts similarity index 77% rename from packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.js rename to packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.ts index 7514a181d0..736af13519 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.js +++ b/packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.ts @@ -1,21 +1,23 @@ import MeasurementReport from "./MeasurementReport"; import { utilities } from "dcmjs"; -import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; +import BaseAdapter3D from "./BaseAdapter3D"; import CodingScheme from "./CodingScheme"; const { Point: TID300Point } = utilities.TID300; +const { codeValues } = CodingScheme; -const ARROW_ANNOTATE = "ArrowAnnotate"; -const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${ARROW_ANNOTATE}`; - -const { codeValues, CodingSchemeDesignator } = CodingScheme; +class ArrowAnnotate extends BaseAdapter3D { + static { + this.init("ArrowAnnotate", TID300Point); + this.registerLegacy(); + } -class ArrowAnnotate { static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, imageToWorldCoords, - metadata + metadata, + _trackingIdentifier ) { const { defaultState, SCOORDGroup, ReferencedFrameNumber } = MeasurementReport.getSetupMeasurementData( @@ -85,8 +87,8 @@ class ArrowAnnotate { } static getTID300RepresentationArguments(tool, worldToImageCoords) { - const { data, metadata } = tool; - let { finding, findingSites } = tool; + const { data, metadata, findingSites } = tool; + let { finding } = tool; const { referencedImageId } = metadata; if (!referencedImageId) { @@ -122,42 +124,22 @@ class ArrowAnnotate { y: pointImage2[1] } ], - trackingIdentifierTextValue, - findingSites: findingSites || [] + trackingIdentifierTextValue: this.trackingIdentifierTextValue, + findingSites: findingSites || [], + finding }; // If freetext finding isn't present, add it from the tool text. if (!finding || finding.CodeValue !== codeValues.CORNERSTONEFREETEXT) { finding = { CodeValue: codeValues.CORNERSTONEFREETEXT, - CodingSchemeDesignator, + CodingSchemeDesignator: CodingScheme.CodingSchemeDesignator, CodeMeaning: data.text }; } - TID300RepresentationArguments.finding = finding; - return TID300RepresentationArguments; } } -ArrowAnnotate.toolType = ARROW_ANNOTATE; -ArrowAnnotate.utilityToolType = ARROW_ANNOTATE; -ArrowAnnotate.TID300Representation = TID300Point; -ArrowAnnotate.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - if (!TrackingIdentifier.includes(":")) { - return false; - } - - const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - - if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { - return false; - } - - return toolType === ARROW_ANNOTATE; -}; - -MeasurementReport.registerTool(ArrowAnnotate); - export default ArrowAnnotate; diff --git a/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts b/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts new file mode 100644 index 0000000000..40c33c28d0 --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts @@ -0,0 +1,178 @@ +import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; +import MeasurementReport, { + type AdapterOptions, + type MeasurementAdapter +} from "./MeasurementReport"; + +export type Point = { + x: number; + y: number; + z?: number; +}; + +export type TID300Arguments = { + points?: Point[]; + point1?: Point; + point2?: Point; + trackingIdentifierTextValue: string; + findingSites: []; + finding; + + [key: string]: unknown; +}; + +/** + * This is a basic definition of adapters to be inherited for other adapters. + */ +export default class BaseAdapter3D { + public static toolType: string; + public static TID300Representation; + public static trackingIdentifierTextValue: string; + public static trackingIdentifiers: Set; + + /** + * The parent type is another type which could be used to parse this instance, + * but for which this sub-class has a better representation. For example, + * key images are parseable as Probe instances, but are represented as a different tool + * Thus, the name for the key image is `Cornerstone3DTag:Probe:KeyImage` so that + * a prefix query testing just the Probe could parse this object and display it, + * but a better/full path key could also be done. + */ + public static parentType: string; + + public static init( + toolType: string, + representation, + options?: AdapterOptions + ) { + this.toolType = toolType; + if (BaseAdapter3D.toolType) { + throw new Error( + `Base adapter tool type set to ${this.toolType} while setting ${toolType}` + ); + } + this.parentType = options?.parentType; + this.trackingIdentifiers = new Set(); + + this.TID300Representation = representation; + if (this.parentType) { + this.trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${this.parentType}:${this.toolType}`; + const alternateTrackingIdentifier = `${CORNERSTONE_3D_TAG}:${this.toolType}`; + this.trackingIdentifiers.add(alternateTrackingIdentifier); + } else { + this.trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${toolType}`; + } + this.trackingIdentifiers.add(this.trackingIdentifierTextValue); + MeasurementReport.registerTool(this); + } + + /** + * registerLegacy registers the given tool adapter using the legacy/old + * cornerstone 1.x adapter names so that the adapter class will be used to load + * those older adapters. + */ + public static registerLegacy() { + this.trackingIdentifiers.add( + `cornerstoneTools@^4.0.0:${this.toolType}` + ); + } + + /** Registers a new copy of the given type that has the prefix path the + * same as that of adapter, but adds the toolType to this path to create + * a new tool instance of the given type. This preserves compatibility + * with parsing in other versions, without need to replace the original parent + * type. + */ + public static registerSubType( + adapter: MeasurementAdapter, + toolType: string, + replace? + ) { + const subAdapter = Object.create(adapter); + subAdapter.init(toolType, adapter.TID300Representation, { + parentType: adapter.parentType || adapter.toolType, + replace + }); + return subAdapter; + } + + /** + * @returns true if the tool is of the given tool type based on the tracking identifier + */ + public static isValidCornerstoneTrackingIdentifier( + trackingIdentifier: string + ) { + if (this.trackingIdentifiers.has(trackingIdentifier)) { + return true; + } + if (!trackingIdentifier.includes(":")) { + return false; + } + + return trackingIdentifier.startsWith(this.trackingIdentifierTextValue); + } + + /** + * Returns annotation data for CS3D to use based on the underlying + * DICOM SR annotation data. + */ + public static getMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + _imageToWorldCoords, + metadata, + trackingIdentifier?: string + ) { + const { defaultState: state, ReferencedFrameNumber } = + MeasurementReport.getSetupMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + metadata, + this.toolType + ); + + state.annotation.data = { + cachedStats: {}, + frameNumber: ReferencedFrameNumber, + seriesLevel: trackingIdentifier?.indexOf(":Series") > 0 + }; + + return state; + } + + public static getTID300RepresentationArguments( + tool, + worldToImageCoords + ): TID300Arguments { + const { data, metadata } = tool; + const { finding, findingSites } = tool; + const { referencedImageId } = metadata; + + if (!referencedImageId) { + throw new Error( + "Probe.getTID300RepresentationArguments: referencedImageId is not defined" + ); + } + + const { + handles: { points = [] } + } = data; + + const pointsImage = points.map(point => { + const pointImage = worldToImageCoords(referencedImageId, point); + return { + x: pointImage[0], + y: pointImage[1] + }; + }); + + const tidArguments = { + points: pointsImage, + trackingIdentifierTextValue: this.trackingIdentifierTextValue, + findingSites: findingSites || [], + finding + }; + + return tidArguments; + } +} diff --git a/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts b/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts index 5253bfeffa..a19bfe9f57 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts @@ -2,31 +2,18 @@ import { utilities } from "dcmjs"; import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; import MeasurementReport from "./MeasurementReport"; import { toArray } from "../helpers"; +import BaseAdapter3D from "./BaseAdapter3D"; const { Bidirectional: TID300Bidirectional } = utilities.TID300; -const BIDIRECTIONAL = "Bidirectional"; const LONG_AXIS = "Long Axis"; const SHORT_AXIS = "Short Axis"; -const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${BIDIRECTIONAL}`; - -class Bidirectional { - public static toolType = BIDIRECTIONAL; - public static utilityToolType = BIDIRECTIONAL; - public static TID300Representation = TID300Bidirectional; - public static isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - if (!TrackingIdentifier.includes(":")) { - return false; - } - - const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - - if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { - return false; - } - return toolType === BIDIRECTIONAL; - }; +class Bidirectional extends BaseAdapter3D { + static { + this.init("Bidirectional", TID300Bidirectional); + this.registerLegacy(); + } public static getMeasurementData( MeasurementGroup, @@ -184,13 +171,11 @@ class Bidirectional { }, longAxisLength: length, shortAxisLength: width, - trackingIdentifierTextValue, + trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding: finding, findingSites: findingSites || [] }; } } -MeasurementReport.registerTool(Bidirectional); - export default Bidirectional; diff --git a/packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts b/packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts index a48d388481..29ecf85fa9 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts @@ -1,19 +1,14 @@ import { utilities } from "dcmjs"; import MeasurementReport from "./MeasurementReport"; -import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; -import isValidCornerstoneTrackingIdentifier from "./isValidCornerstoneTrackingIdentifier"; +import BaseAdapter3D from "./BaseAdapter3D"; const { Circle: TID300Circle } = utilities.TID300; -const CIRCLEROI = "CircleROI"; - -class CircleROI { - static trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${CIRCLEROI}`; - static toolType = CIRCLEROI; - static utilityToolType = CIRCLEROI; - static TID300Representation = TID300Circle; - static isValidCornerstoneTrackingIdentifier = - isValidCornerstoneTrackingIdentifier; +class CircleROI extends BaseAdapter3D { + static { + this.init("CircleROI", TID300Circle); + this.registerLegacy(); + } /** Gets the measurement data for cornerstone, given DICOM SR measurement data. */ static getMeasurementData( @@ -113,6 +108,4 @@ class CircleROI { } } -MeasurementReport.registerTool(CircleROI); - export default CircleROI; diff --git a/packages/adapters/src/adapters/Cornerstone3D/CobbAngle.ts b/packages/adapters/src/adapters/Cornerstone3D/CobbAngle.ts index 2e13b64978..6f40cdbf22 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/CobbAngle.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/CobbAngle.ts @@ -1,29 +1,15 @@ import { utilities } from "dcmjs"; -import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; import MeasurementReport from "./MeasurementReport"; +import BaseAdapter3D from "./BaseAdapter3D"; const { CobbAngle: TID300CobbAngle } = utilities.TID300; -const MEASUREMENT_TYPE = "CobbAngle"; -const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${MEASUREMENT_TYPE}`; - -class CobbAngle { - public static toolType = MEASUREMENT_TYPE; - public static utilityToolType = MEASUREMENT_TYPE; - public static TID300Representation = TID300CobbAngle; - public static isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - if (!TrackingIdentifier.includes(":")) { - return false; - } - - const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - - if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { - return false; - } - - return toolType === MEASUREMENT_TYPE; - }; +class CobbAngle extends BaseAdapter3D { + static { + this.init("CobbAngle", TID300CobbAngle); + // Register using the Cornerstone 1.x name so this tool is used to load it + this.registerLegacy(); + } // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. public static getMeasurementData( @@ -112,13 +98,11 @@ class CobbAngle { point3, point4, rAngle: angle, - trackingIdentifierTextValue, + trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [] }; } } -MeasurementReport.registerTool(CobbAngle); - export default CobbAngle; diff --git a/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts b/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts index 1a910201cf..7628023ce8 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts @@ -1,23 +1,18 @@ import { vec3 } from "gl-matrix"; import { utilities } from "dcmjs"; -import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; import MeasurementReport from "./MeasurementReport"; -import isValidCornerstoneTrackingIdentifier from "./isValidCornerstoneTrackingIdentifier"; +import BaseAdapter3D from "./BaseAdapter3D"; type Point3 = [number, number, number]; const { Ellipse: TID300Ellipse } = utilities.TID300; -const ELLIPTICALROI = "EllipticalROI"; const EPSILON = 1e-4; -class EllipticalROI { - static trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${ELLIPTICALROI}`; - static toolType = ELLIPTICALROI; - static utilityToolType = ELLIPTICALROI; - static TID300Representation = TID300Ellipse; - static isValidCornerstoneTrackingIdentifier = - isValidCornerstoneTrackingIdentifier; +class EllipticalROI extends BaseAdapter3D { + static { + this.init("EllipticalROI", TID300Ellipse); + } static getMeasurementData( MeasurementGroup, @@ -199,6 +194,4 @@ class EllipticalROI { } } -MeasurementReport.registerTool(EllipticalROI); - export default EllipticalROI; diff --git a/packages/adapters/src/adapters/Cornerstone3D/KeyImage.ts b/packages/adapters/src/adapters/Cornerstone3D/KeyImage.ts new file mode 100644 index 0000000000..57e59b20d4 --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/KeyImage.ts @@ -0,0 +1,59 @@ +import { utilities } from "dcmjs"; +import Probe from "./Probe"; +const { Point: TID300Point } = utilities.TID300; + +export default class KeyImage extends Probe { + static { + this.init("KeyImage", TID300Point, { parentType: Probe.toolType }); + } + static trackingSeriesIdentifier = `${this.trackingIdentifierTextValue}:Series`; + static trackingPointIdentifier = `${this.trackingIdentifierTextValue}:Point`; + static trackingSeriesPointIdentifier = `${this.trackingIdentifierTextValue}:SeriesPoint`; + + static getMeasurementData( + measurementGroup, + sopInstanceUIDToImageIdMap, + imageToWorldCoords, + metadata, + trackingIdentifier + ) { + const baseData = super.getMeasurementData( + measurementGroup, + sopInstanceUIDToImageIdMap, + imageToWorldCoords, + metadata, + trackingIdentifier + ); + const { data } = baseData.annotation; + data.isPoint = trackingIdentifier.indexOf("Point") !== -1; + + return baseData; + } + + public static getTID300RepresentationArguments(tool, worldToImageCoords) { + const tid300Arguments = super.getTID300RepresentationArguments( + tool, + worldToImageCoords + ); + const { data } = tool; + if (data.isPoint) { + if (data.seriesLevel) { + tid300Arguments.trackingIdentifierTextValue = + this.trackingSeriesPointIdentifier; + } else { + tid300Arguments.trackingIdentifierTextValue = + this.trackingPointIdentifier; + } + } + if (data.seriesLevel) { + tid300Arguments.trackingIdentifierTextValue = + this.trackingSeriesIdentifier; + } + if (!tid300Arguments.points.length) { + tid300Arguments.points.push({ x: 0, y: 0 }); + } + return tid300Arguments; + } +} + +export { KeyImage }; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Length.js b/packages/adapters/src/adapters/Cornerstone3D/Length.ts similarity index 79% rename from packages/adapters/src/adapters/Cornerstone3D/Length.js rename to packages/adapters/src/adapters/Cornerstone3D/Length.ts index 4d409ed264..733265409d 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Length.js +++ b/packages/adapters/src/adapters/Cornerstone3D/Length.ts @@ -1,13 +1,18 @@ import { utilities } from "dcmjs"; -import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; import MeasurementReport from "./MeasurementReport"; +import BaseAdapter3D from "./BaseAdapter3D"; const { Length: TID300Length } = utilities.TID300; const LENGTH = "Length"; -const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${LENGTH}`; -class Length { +export default class Length extends BaseAdapter3D { + static { + this.init(LENGTH, TID300Length); + // Register using the Cornerstone 1.x name so this tool is used to load it + this.registerLegacy(); + } + // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. static getMeasurementData( MeasurementGroup, @@ -20,7 +25,7 @@ class Length { MeasurementGroup, sopInstanceUIDToImageIdMap, metadata, - Length.toolType + this.toolType ); const referencedImageId = @@ -84,30 +89,9 @@ class Length { point1, point2, distance, - trackingIdentifierTextValue, + trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [] }; } } - -Length.toolType = LENGTH; -Length.utilityToolType = LENGTH; -Length.TID300Representation = TID300Length; -Length.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - if (!TrackingIdentifier.includes(":")) { - return false; - } - - const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - - if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { - return false; - } - - return toolType === LENGTH; -}; - -MeasurementReport.registerTool(Length); - -export default Length; diff --git a/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.ts b/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.ts index d7e5fd28c4..2067df144f 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.ts @@ -18,77 +18,136 @@ const FINDING = { CodingSchemeDesignator: "DCM", CodeValue: "121071" }; const FINDING_SITE = { CodingSchemeDesignator: "SCT", CodeValue: "363698007" }; const FINDING_SITE_OLD = { CodingSchemeDesignator: "SRT", CodeValue: "G-C0E3" }; -const codeValueMatch = (group, code, oldCode?) => { - const { ConceptNameCodeSequence } = group; - if (!ConceptNameCodeSequence) { - return; - } - const { CodingSchemeDesignator, CodeValue } = ConceptNameCodeSequence; - return ( - (CodingSchemeDesignator == code.CodingSchemeDesignator && - CodeValue == code.CodeValue) || - (oldCode && - CodingSchemeDesignator == oldCode.CodingSchemeDesignator && - CodeValue == oldCode.CodeValue) - ); +export type AdapterOptions = { + /** + * The parent type is another type which could be used to parse this instance, + * but for which this sub-class has a better representation. For example, + * key images are parseable as Probe instances, but are represented as a different tool + * Thus, the name for the key image is `Cornerstone3DTag:Probe:KeyImage` so that + * a prefix query testing just the Probe could parse this object and display it, + * but a better/full path key could also be done. + */ + parentType?: string; + + /** + * If set, then replace this + */ + replace?: boolean | ((original: MeasurementAdapter) => void); }; -function getTID300ContentItem( - tool, - toolType, - ReferencedSOPSequence, - toolClass, - worldToImageCoords -) { - const args = toolClass.getTID300RepresentationArguments( - tool, - worldToImageCoords +/** + * A measurement adapter parses/creates data for DICOM SR measurements + */ +export interface MeasurementAdapter { + toolType: string; + TID300Representation; + trackingIdentifierTextValue: string; + trackingIdentifiers: Set; + + /** + * The parent type is the base type of the adapter that is used for the + * identifier, being compatible with older versions to read that subtype. + */ + parentType: string; + + /** + * Applies the options and registers this tool + */ + init(toolType: string, representation, options?: AdapterOptions); + + getMeasurementData( + measurementGroup, + sopInstanceUIDToImageIdMap, + imageToWorldCoords, + metadata, + trackingIdentifier: string ); - args.ReferencedSOPSequence = ReferencedSOPSequence; - const TID300Measurement = new toolClass.TID300Representation(args); + isValidCornerstoneTrackingIdentifier(trackingIdentifier: string): boolean; - return TID300Measurement; + getTID300RepresentationArguments( + tool, + worldToImageCoords + ): Record; } -function getMeasurementGroup( - toolType, - toolData, - ReferencedSOPSequence, - worldToImageCoords -) { - const toolTypeData = toolData[toolType]; - const toolClass = - MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_TOOL_TYPE[toolType]; - if ( - !toolTypeData || - !toolTypeData.data || - !toolTypeData.data.length || - !toolClass - ) { - return; - } +export default class MeasurementReport { + public static CORNERSTONE_3D_TAG = CORNERSTONE_3D_TAG; + + /** Maps tool type to the adapter name used to serialize this item to SR */ + public static measurementAdapterByToolType = new Map< + string, + MeasurementAdapter + >(); - // Loop through the array of tool instances - // for this tool - const Measurements = toolTypeData.data.map(tool => { - return getTID300ContentItem( + /** Maps tracking identifier to tool class to deserialize from SR into a tool instance */ + public static measurementAdapterByTrackingIdentifier = new Map< + string, + MeasurementAdapter + >(); + + public static getTID300ContentItem( + tool, + ReferencedSOPSequence, + toolClass, + worldToImageCoords + ) { + const args = toolClass.getTID300RepresentationArguments( tool, - toolType, - ReferencedSOPSequence, - toolClass, worldToImageCoords ); - }); + args.ReferencedSOPSequence = ReferencedSOPSequence; - return new TID1501MeasurementGroup(Measurements); -} + const TID300Measurement = new toolClass.TID300Representation(args); -export default class MeasurementReport { - public static CORNERSTONE_3D_TAG = CORNERSTONE_3D_TAG; - public static MEASUREMENT_BY_TOOLTYPE = {}; - public static CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE = {}; - public static CORNERSTONE_TOOL_CLASSES_BY_TOOL_TYPE = {}; + return TID300Measurement; + } + + public static codeValueMatch = (group, code, oldCode?) => { + const { ConceptNameCodeSequence } = group; + if (!ConceptNameCodeSequence) { + return; + } + const { CodingSchemeDesignator, CodeValue } = ConceptNameCodeSequence; + return ( + (CodingSchemeDesignator == code.CodingSchemeDesignator && + CodeValue == code.CodeValue) || + (oldCode && + CodingSchemeDesignator == oldCode.CodingSchemeDesignator && + CodeValue == oldCode.CodeValue) + ); + }; + + public static getMeasurementGroup( + toolType, + toolData, + ReferencedSOPSequence, + worldToImageCoords + ) { + const toolTypeData = toolData[toolType]; + const toolClass = this.measurementAdapterByToolType.get(toolType); + if ( + !toolTypeData || + !toolTypeData.data || + !toolTypeData.data.length || + !toolClass + ) { + return; + } + + // Loop through the array of tool instances + // for this tool + const Measurements = toolTypeData.data.map(tool => { + return this.getTID300ContentItem( + tool, + ReferencedSOPSequence, + toolClass, + worldToImageCoords + ); + }); + + return new TID1501MeasurementGroup(Measurements); + } static getCornerstoneLabelFromDefaultState(defaultState) { const { findingSites = [], finding } = defaultState; @@ -156,7 +215,7 @@ export default class MeasurementReport { return derivationSourceDataset; }; - static getSetupMeasurementData( + public static getSetupMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, metadata, @@ -166,11 +225,11 @@ export default class MeasurementReport { const contentSequenceArr = toArray(ContentSequence); const findingGroup = contentSequenceArr.find(group => - codeValueMatch(group, FINDING) + this.codeValueMatch(group, FINDING) ); const findingSiteGroups = contentSequenceArr.filter(group => - codeValueMatch(group, FINDING_SITE, FINDING_SITE_OLD) + this.codeValueMatch(group, FINDING_SITE, FINDING_SITE_OLD) ) || []; const NUMGroup = contentSequenceArr.find( group => group.ValueType === "NUM" @@ -300,7 +359,7 @@ export default class MeasurementReport { const measurementGroups = []; toolTypes.forEach(toolType => { - const group = getMeasurementGroup( + const group = this.getMeasurementGroup( toolType, toolData, ReferencedSOPSequence, @@ -370,69 +429,58 @@ export default class MeasurementReport { // For each of the supported measurement types, compute the measurement data const measurementData = {}; - const cornerstoneToolClasses = - MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE; - - const registeredToolClasses = []; - - Object.keys(cornerstoneToolClasses).forEach(key => { - registeredToolClasses.push(cornerstoneToolClasses[key]); - measurementData[key] = []; - }); - measurementGroups.forEach(measurementGroup => { try { const measurementGroupContentSequence = toArray( measurementGroup.ContentSequence ); - const TrackingIdentifierGroup = + const trackingIdentifierGroup = measurementGroupContentSequence.find( contentItem => contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER ); - const TrackingIdentifierValue = - TrackingIdentifierGroup.TextValue; + const { TextValue: trackingIdentifierValue } = + trackingIdentifierGroup; - const TrackingUniqueIdentifierGroup = + const trackingUniqueIdentifierGroup = measurementGroupContentSequence.find( contentItem => contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_UNIQUE_IDENTIFIER ); - const TrackingUniqueIdentifierValue = - TrackingUniqueIdentifierGroup?.UID; + const trackingUniqueIdentifierValue = + trackingUniqueIdentifierGroup?.UID; - const toolClass = + const toolAdapter = hooks?.getToolClass?.( measurementGroup, dataset, - registeredToolClasses + this.measurementAdapterByToolType ) || - registeredToolClasses.find(tc => - tc.isValidCornerstoneTrackingIdentifier( - TrackingIdentifierValue - ) + this.getAdapterForTrackingIdentifier( + trackingIdentifierValue ); - if (toolClass) { - const measurement = toolClass.getMeasurementData( + if (toolAdapter) { + const measurement = toolAdapter.getMeasurementData( measurementGroup, sopInstanceUIDToImageIdMap, imageToWorldCoords, - metadata + metadata, + trackingIdentifierValue ); measurement.TrackingUniqueIdentifier = - TrackingUniqueIdentifierValue; + trackingUniqueIdentifierValue; - console.log(`=== ${toolClass.toolType} ===`); + console.log(`=== ${toolAdapter.toolType} ===`); console.log(measurement); - - measurementData[toolClass.toolType].push(measurement); + measurementData[toolAdapter.toolType] ||= []; + measurementData[toolAdapter.toolType].push(measurement); } } catch (e) { console.warn( @@ -450,16 +498,66 @@ export default class MeasurementReport { /** * Register a new tool type. - * @param toolClass to perform I/O to DICOM for this tool + * @param toolAdapter to perform I/O to DICOM for this tool */ - public static registerTool(toolClass) { - MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE[ - toolClass.utilityToolType - ] = toolClass; - MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_TOOL_TYPE[ - toolClass.toolType - ] = toolClass; - MeasurementReport.MEASUREMENT_BY_TOOLTYPE[toolClass.toolType] = - toolClass.utilityToolType; + public static registerTool( + toolAdapter: MeasurementAdapter, + replace: boolean | ((original) => void) = false + ) { + const registerName = toolAdapter.toolType; + if (this.measurementAdapterByToolType.has(registerName)) { + if (!replace) { + throw new Error( + `The registered tool name ${registerName} already exists in adapters, use a different toolType or use replace` + ); + } + if (typeof replace === "function") { + // Call the function so it can call parent output + replace(this.measurementAdapterByToolType.get(registerName)); + } + } + this.measurementAdapterByToolType.set( + toolAdapter.toolType, + toolAdapter + ); + this.measurementAdapterByTrackingIdentifier.set( + toolAdapter.trackingIdentifierTextValue, + toolAdapter + ); + } + + public static registerTrackingIdentifier( + toolClass, + ...trackingIdentifiers: string[] + ) { + for (const identifier of trackingIdentifiers) { + this.measurementAdapterByTrackingIdentifier.set( + identifier, + toolClass + ); + } + } + + public static getAdapterForTrackingIdentifier(trackingIdentifier: string) { + const adapter = + this.measurementAdapterByTrackingIdentifier.get(trackingIdentifier); + if (adapter) { + return adapter; + } + for (const adapterTest of [ + ...this.measurementAdapterByToolType.values() + ]) { + if ( + adapterTest.isValidCornerstoneTrackingIdentifier( + trackingIdentifier + ) + ) { + this.measurementAdapterByTrackingIdentifier.set( + trackingIdentifier, + adapterTest + ); + return adapterTest; + } + } } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts b/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts index 25abb55488..4ea174d9bd 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts @@ -1,31 +1,16 @@ import MeasurementReport from "./MeasurementReport"; import { utilities } from "dcmjs"; -import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; import { vec3 } from "gl-matrix"; +import BaseAdapter3D from "./BaseAdapter3D"; const { Polyline: TID300Polyline } = utilities.TID300; -const PLANARFREEHANDROI = "PlanarFreehandROI"; -const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${PLANARFREEHANDROI}`; -const closedContourThreshold = 1e-5; - -class PlanarFreehandROI { - public static toolType = PLANARFREEHANDROI; - public static utilityToolType = PLANARFREEHANDROI; - public static TID300Representation = TID300Polyline; - public static isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - if (!TrackingIdentifier.includes(":")) { - return false; - } - - const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - - if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { - return false; - } +class PlanarFreehandROI extends BaseAdapter3D { + public static closedContourThreshold = 1e-5; - return toolType === PLANARFREEHANDROI; - }; + static { + this.init("PlanarFreehandROI", TID300Polyline); + } static getMeasurementData( MeasurementGroup, @@ -64,7 +49,7 @@ class PlanarFreehandROI { let isOpenContour = true; // If the contour is closed, this should have been encoded as exactly the same point, so check for a very small difference. - if (distanceBetweenFirstAndLastPoint < closedContourThreshold) { + if (distanceBetweenFirstAndLastPoint < this.closedContourThreshold) { worldCoords.pop(); // Remove the last element which is duplicated. isOpenContour = false; @@ -140,13 +125,11 @@ class PlanarFreehandROI { max, stdDev, /** Other */ - trackingIdentifierTextValue, + trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [] }; } } -MeasurementReport.registerTool(PlanarFreehandROI); - export default PlanarFreehandROI; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Probe.js b/packages/adapters/src/adapters/Cornerstone3D/Probe.js deleted file mode 100644 index 553ae3797c..0000000000 --- a/packages/adapters/src/adapters/Cornerstone3D/Probe.js +++ /dev/null @@ -1,106 +0,0 @@ -import { utilities } from "dcmjs"; -import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; -import MeasurementReport from "./MeasurementReport"; - -const { Point: TID300Point } = utilities.TID300; - -const PROBE = "Probe"; -const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${PROBE}`; - -class Probe { - static getMeasurementData( - MeasurementGroup, - sopInstanceUIDToImageIdMap, - imageToWorldCoords, - metadata - ) { - const { defaultState, SCOORDGroup, ReferencedFrameNumber } = - MeasurementReport.getSetupMeasurementData( - MeasurementGroup, - sopInstanceUIDToImageIdMap, - metadata, - Probe.toolType - ); - - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; - - const { GraphicData } = SCOORDGroup; - - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - worldCoords.push(point); - } - - const state = defaultState; - - state.annotation.data = { - handles: { - points: worldCoords, - activeHandleIndex: null, - textBox: { - hasMoved: false - } - }, - frameNumber: ReferencedFrameNumber - }; - - return state; - } - - static getTID300RepresentationArguments(tool, worldToImageCoords) { - const { data, metadata } = tool; - let { finding, findingSites } = tool; - const { referencedImageId } = metadata; - - if (!referencedImageId) { - throw new Error( - "Probe.getTID300RepresentationArguments: referencedImageId is not defined" - ); - } - - const { points } = data.handles; - - const pointsImage = points.map(point => { - const pointImage = worldToImageCoords(referencedImageId, point); - return { - x: pointImage[0], - y: pointImage[1] - }; - }); - - const TID300RepresentationArguments = { - points: pointsImage, - trackingIdentifierTextValue, - findingSites: findingSites || [], - finding - }; - - return TID300RepresentationArguments; - } -} - -Probe.toolType = PROBE; -Probe.utilityToolType = PROBE; -Probe.TID300Representation = TID300Point; -Probe.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - if (!TrackingIdentifier.includes(":")) { - return false; - } - - const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - - if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { - return false; - } - - return toolType === PROBE; -}; - -MeasurementReport.registerTool(Probe); - -export default Probe; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Probe.ts b/packages/adapters/src/adapters/Cornerstone3D/Probe.ts new file mode 100644 index 0000000000..e881dbf21d --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/Probe.ts @@ -0,0 +1,65 @@ +import { utilities } from "dcmjs"; +import MeasurementReport from "./MeasurementReport"; +import BaseAdapter3D from "./BaseAdapter3D"; + +const { Point: TID300Point } = utilities.TID300; + +class Probe extends BaseAdapter3D { + static { + this.init("Probe", TID300Point); + this.registerLegacy(); + } + + static getMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + imageToWorldCoords, + metadata, + trackingIdentifier + ) { + const state = super.getMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + imageToWorldCoords, + metadata, + trackingIdentifier + ); + + const { defaultState, SCOORDGroup } = + MeasurementReport.getSetupMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + metadata, + Probe.toolType + ); + + const referencedImageId = + defaultState.annotation.metadata.referencedImageId; + + const { GraphicData } = SCOORDGroup; + + const worldCoords = []; + for (let i = 0; i < GraphicData.length; i += 2) { + const point = imageToWorldCoords(referencedImageId, [ + GraphicData[i], + GraphicData[i + 1] + ]); + worldCoords.push(point); + } + + state.annotation.data = { + ...state.annotation.data, + handles: { + points: worldCoords, + activeHandleIndex: null, + textBox: { + hasMoved: false + } + } + }; + + return state; + } +} + +export default Probe; diff --git a/packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts b/packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts index e26e247918..f659a62e47 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts @@ -1,31 +1,15 @@ import { utilities } from "dcmjs"; -import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; import MeasurementReport from "./MeasurementReport"; +import BaseAdapter3D from "./BaseAdapter3D"; const { Polyline: TID300Polyline } = utilities.TID300; -const TOOLTYPE = "RectangleROI"; -const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${TOOLTYPE}`; - -class RectangleROI { - public static toolType = TOOLTYPE; - public static utilityToolType = TOOLTYPE; - public static TID300Representation = TID300Polyline; - - public static isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - if (!TrackingIdentifier.includes(":")) { - return false; - } - - const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - - if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { - return false; - } - - return toolType === TOOLTYPE; - }; - +class RectangleROI extends BaseAdapter3D { + static { + this.init("RectangleROI", TID300Polyline); + // Register using the Cornerstone 1.x name so this tool is used to load it + this.registerLegacy(); + } public static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, @@ -109,13 +93,11 @@ class RectangleROI { ], area, perimeter, - trackingIdentifierTextValue, + trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [] }; } } -MeasurementReport.registerTool(RectangleROI); - export default RectangleROI; diff --git a/packages/adapters/src/adapters/Cornerstone3D/UltrasoundDirectional.ts b/packages/adapters/src/adapters/Cornerstone3D/UltrasoundDirectional.ts index d502c8063e..affef4a1d2 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/UltrasoundDirectional.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/UltrasoundDirectional.ts @@ -1,30 +1,13 @@ import { utilities } from "dcmjs"; -import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; import MeasurementReport from "./MeasurementReport"; +import BaseAdapter3D from "./BaseAdapter3D"; const { Length: TID300Length } = utilities.TID300; -const ULTRASOUND_DIRECTIONAL = "UltrasoundDirectionalTool"; -const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${ULTRASOUND_DIRECTIONAL}`; - -class UltrasoundDirectional { - public static toolType = ULTRASOUND_DIRECTIONAL; - public static utilityToolType = ULTRASOUND_DIRECTIONAL; - public static TID300Representation = TID300Length; - public static isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - if (!TrackingIdentifier.includes(":")) { - return false; - } - - const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - - if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { - return false; - } - - return toolType === ULTRASOUND_DIRECTIONAL; - }; - +class UltrasoundDirectional extends BaseAdapter3D { + static { + this.init("UltrasoundDirectionalTool", TID300Length); + } // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. static getMeasurementData( MeasurementGroup, @@ -91,13 +74,11 @@ class UltrasoundDirectional { return { point1, point2, - trackingIdentifierTextValue, + trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [] }; } } -MeasurementReport.registerTool(UltrasoundDirectional); - export default UltrasoundDirectional; diff --git a/packages/adapters/src/adapters/Cornerstone3D/index.ts b/packages/adapters/src/adapters/Cornerstone3D/index.ts index ef0e057a1c..a2a6489489 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/index.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/index.ts @@ -13,12 +13,15 @@ import Length from "./Length"; import PlanarFreehandROI from "./PlanarFreehandROI"; import Probe from "./Probe"; import UltrasoundDirectional from "./UltrasoundDirectional"; +import BaseAdapter3D from "./BaseAdapter3D"; import * as Segmentation from "./Segmentation"; import * as ParametricMap from "./ParametricMap"; import * as RTSS from "./RTStruct"; +import KeyImage from "./KeyImage"; const Cornerstone3DSR = { + BaseAdapter3D, Bidirectional, CobbAngle, Angle, @@ -30,6 +33,7 @@ const Cornerstone3DSR = { Probe, PlanarFreehandROI, UltrasoundDirectional, + KeyImage, MeasurementReport, CodeScheme, CORNERSTONE_3D_TAG diff --git a/packages/adapters/test/KeyImage.jest.js b/packages/adapters/test/KeyImage.jest.js new file mode 100644 index 0000000000..1396fc6753 --- /dev/null +++ b/packages/adapters/test/KeyImage.jest.js @@ -0,0 +1,28 @@ +import { describe, it, expect, beforeEach } from "@jest/globals"; +import { Cornerstone3DSR } from "../src/adapters/Cornerstone3D"; + +const { KeyImage, Probe } = Cornerstone3DSR; + +describe("KeyImage", () => { + beforeEach(() => { + // Setup adapters + }); + + it("Must define tool type", () => { + expect(KeyImage.toolType).toBe("KeyImage"); + // Even after registering a sub-type of Probe, the Probe type should be correct + expect(Probe.toolType).toBe("Probe"); + }); + + it("Must define tracking identifiers", () => { + expect(KeyImage.trackingIdentifierTextValue).toBe( + "Cornerstone3DTools@^0.1.0:Probe:KeyImage" + ); + // Even after registering a sub-type of Probe, the Probe tracking identifier should be unchanged + expect(Probe.trackingIdentifierTextValue).toBe( + "Cornerstone3DTools@^0.1.0:Probe" + ); + }); +}); + +// diff --git a/packages/adapters/test/Probe.jest.js b/packages/adapters/test/Probe.jest.js new file mode 100644 index 0000000000..c2c4f99968 --- /dev/null +++ b/packages/adapters/test/Probe.jest.js @@ -0,0 +1,16 @@ +import { describe, it, expect, beforeEach } from "@jest/globals"; +import { Cornerstone3DSR } from "../src/adapters/Cornerstone3D"; + +const { Probe } = Cornerstone3DSR; + +describe("Probe", () => { + beforeEach(() => { + // Setup adapters + }); + + it("Must define tool type", () => { + expect(Probe.toolType).toBe("Probe"); + }); +}); + +// diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index a2a6103def..9d830f4bb9 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -3,14 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [2.19.16](https://github.com/dcmjs-org/dcmjs/compare/v2.19.15...v2.19.16) (2025-02-26) +# [3.0.0-beta.6](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2025-02-27) **Note:** Version bump only for package @cornerstonejs/ai -## [2.19.15](https://github.com/dcmjs-org/dcmjs/compare/v2.19.14...v2.19.15) (2025-02-26) +# [3.0.0-beta.5](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2025-02-25) **Note:** Version bump only for package @cornerstonejs/ai +# [3.0.0-beta.4](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/ai + +# [3.0.0-beta.3](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/ai + +# [3.0.0-beta.2](https://github.com/dcmjs-org/dcmjs/compare/v2.19.14...v3.0.0-beta.2) (2025-02-24) + +### Bug Fixes + +- publish beta for 3.0 ([8bf65df](https://github.com/dcmjs-org/dcmjs/commit/8bf65df9bec5f52459de1c49c4834b316f680f1b)) + ## [2.19.14](https://github.com/dcmjs-org/dcmjs/compare/v2.19.13...v2.19.14) (2025-02-24) **Note:** Version bump only for package @cornerstonejs/ai diff --git a/packages/ai/package.json b/packages/ai/package.json index fe225aa857..3367358f19 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/ai", - "version": "2.19.16", + "version": "3.0.0-beta.6", "description": "AI and ML Interfaces for Cornerstone3D", "files": [ "dist" @@ -21,7 +21,8 @@ }, "scripts": { "test": "jest --testTimeout 60000", - "clean": "rimraf dist", + "clean": "rm -rf node_modules/.cache/storybook && shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", "build": "yarn run build:esm", "build:esm": "tsc --project ./tsconfig.json", "build:esm:watch": "tsc --project ./tsconfig.json --watch", @@ -30,11 +31,14 @@ "build:update-api": "yarn run build:esm && api-extractor run --local", "start": "tsc --project ./tsconfig.json --watch", "format": "prettier --write 'src/**/*.js' 'test/**/*.js'", - "lint": "eslint --fix ." + "lint": "eslint --fix .", + "format-check": "npx eslint ./src --quiet", + "api-check": "api-extractor --debug run ", + "prepublishOnly": "yarn clean && yarn build" }, "repository": { "type": "git", - "url": "git+https://github.com/dcmjs-org/dcmjs.git" + "url": "git+https://github.com/cornerstonejs/cornerstone3D.git" }, "author": "@cornerstonejs", "license": "MIT", @@ -53,7 +57,7 @@ "onnxruntime-web": "1.17.1" }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.16", - "@cornerstonejs/tools": "^2.19.16" + "@cornerstonejs/core": "^3.0.0-beta.6", + "@cornerstonejs/tools": "^3.0.0-beta.6" } } diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 8db3e8817d..922faa1d3e 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -3,14 +3,30 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [2.19.16](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.15...v2.19.16) (2025-02-26) +# [3.0.0-beta.6](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2025-02-27) + +### Features + +- Add key image adapters for key image point mark ([#1754](https://github.com/cornerstonejs/cornerstone3D/issues/1754)) ([a1fd3f9](https://github.com/cornerstonejs/cornerstone3D/commit/a1fd3f9d0ea40d53cafd792d59bc1dbfc90663a5)) + +# [3.0.0-beta.5](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2025-02-25) **Note:** Version bump only for package @cornerstonejs/core -## [2.19.15](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.14...v2.19.15) (2025-02-26) +# [3.0.0-beta.4](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2025-02-25) **Note:** Version bump only for package @cornerstonejs/core +# [3.0.0-beta.3](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/core + +# [3.0.0-beta.2](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.14...v3.0.0-beta.2) (2025-02-24) + +### Bug Fixes + +- publish beta for 3.0 ([8bf65df](https://github.com/cornerstonejs/cornerstone3D/commit/8bf65df9bec5f52459de1c49c4834b316f680f1b)) + ## [2.19.14](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.13...v2.19.14) (2025-02-24) **Note:** Version bump only for package @cornerstonejs/core diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js index 10f5cc4456..be56fc7a06 100644 --- a/packages/core/jest.config.js +++ b/packages/core/jest.config.js @@ -5,6 +5,7 @@ const path = require('path'); module.exports = { ...base, displayName: 'core', + setupFiles: ['jest-canvas-mock'], moduleNameMapper: { '^@cornerstonejs/(.*)$': path.resolve(__dirname, '../$1/src'), }, diff --git a/packages/core/package.json b/packages/core/package.json index 117c445cc2..95f90bee2d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/core", - "version": "2.19.16", + "version": "3.0.0-beta.6", "description": "Cornerstone3D Core", "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", diff --git a/packages/core/src/cache/cache.ts b/packages/core/src/cache/cache.ts index 5c2db60a51..21576891ca 100644 --- a/packages/core/src/cache/cache.ts +++ b/packages/core/src/cache/cache.ts @@ -16,6 +16,7 @@ import imageIdToURI from '../utilities/imageIdToURI'; import eventTarget from '../eventTarget'; import Events from '../enums/Events'; import { ImageQualityStatus } from '../enums'; +import fnv1aHash from '../utilities/fnv1aHash'; const ONE_GB = 1073741824; @@ -33,6 +34,8 @@ class Cache { private readonly _imageCache = new Map(); // used to store volume data (3d) private readonly _volumeCache = new Map(); + // used to store the reverse lookup from imageIds to volumeId + private readonly _imageIdsToVolumeIdCache = new Map(); // Todo: contour for now, but will be used for surface, etc. private readonly _geometryCache = new Map(); @@ -40,6 +43,36 @@ class Cache { private _maxCacheSize = 3 * ONE_GB; private _geometryCacheSize = 0; + /** + * Generates a deterministic volume ID from a list of image IDs + * @param imageIds - Array of image IDs + * @returns A deterministic volume ID + */ + public generateVolumeId(imageIds: string[]): string { + const imageURIs = imageIds.map(imageIdToURI).sort(); + + let combinedHash = 0x811c9dc5; + for (const id of imageURIs) { + const idHash = fnv1aHash(id); + for (let i = 0; i < idHash.length; i++) { + combinedHash ^= idHash.charCodeAt(i); + combinedHash += + (combinedHash << 1) + + (combinedHash << 4) + + (combinedHash << 7) + + (combinedHash << 8) + + (combinedHash << 24); + } + } + return `volume-${(combinedHash >>> 0).toString(36)}`; + } + + public getImageIdsForVolumeId(volumeId: string): string[] { + return Array.from(this._imageIdsToVolumeIdCache.entries()) + .filter(([_, id]) => id === volumeId) + .map(([key]) => key); + } + /** * Set the maximum cache Size * diff --git a/packages/core/src/enums/Events.ts b/packages/core/src/enums/Events.ts index 1f7aadd32e..d6ba3dacbb 100644 --- a/packages/core/src/enums/Events.ts +++ b/packages/core/src/enums/Events.ts @@ -239,13 +239,8 @@ enum Events { /** * Triggers when the webworker has made progress * You should use it with a workerType to indicate the type of worker that is making progress - * Checkout the polySEG convertors in the cornerstone tools - * to lean how to use it */ WEB_WORKER_PROGRESS = 'CORNERSTONE_WEB_WORKER_PROGRESS', - // IMAGE_CACHE_FULL = 'CORNERSTONE_IMAGE_CACHE_FULL', - // PRE_RENDER = 'CORNERSTONE_PRE_RENDER', - // ELEMENT_RESIZED = 'CORNERSTONE_ELEMENT_RESIZED', /** * Triggers on the HTML element when viewport modifies its colormap diff --git a/packages/core/src/utilities/VoxelManager.ts b/packages/core/src/utilities/VoxelManager.ts index e805e5b4c1..a6a6b0f145 100644 --- a/packages/core/src/utilities/VoxelManager.ts +++ b/packages/core/src/utilities/VoxelManager.ts @@ -1,4 +1,3 @@ -import { vec3 } from 'gl-matrix'; import cache from '../cache/cache'; import type { BoundsIJK, @@ -893,7 +892,20 @@ export default class VoxelManager { const sliceData = new SliceDataConstructor(sliceSize); // @ts-ignore sliceData.set(scalarData.subarray(sliceStart, sliceEnd)); - imageVoxelManager.scalarData = sliceData; + + // Instead of directly assigning scalarData, use TypedArray's set method + // previously here we were using imageVoxelManager.scalarData = sliceData + // which had some weird side effects + if (imageVoxelManager.scalarData) { + imageVoxelManager.scalarData.set(sliceData); + // Ensure the voxel manager knows about the changes + imageVoxelManager.modifiedSlices.add(sliceIndex); + } else { + // Fallback to individual updates if scalarData is not directly accessible + for (let i = 0; i < sliceSize; i++) { + imageVoxelManager.setAtIndex(i, sliceData[i]); + } + } // Update min/max values for this slice for (let i = 0; i < sliceData.length; i++) { @@ -939,7 +951,7 @@ export default class VoxelManager { public static createScalarVolumeVoxelManager({ dimensions, scalarData, - numberOfComponents = 1, + numberOfComponents, }: { dimensions: Point3; scalarData; diff --git a/packages/core/src/utilities/calculateSpacingBetweenImageIds.ts b/packages/core/src/utilities/calculateSpacingBetweenImageIds.ts new file mode 100644 index 0000000000..de154ea969 --- /dev/null +++ b/packages/core/src/utilities/calculateSpacingBetweenImageIds.ts @@ -0,0 +1,150 @@ +import { vec3 } from 'gl-matrix'; +import * as metaData from '../metaData'; +import { getConfiguration } from '../init'; + +/** + * Calculates the spacing between images in a series based on their positions + * + * @param imageIds - array of imageIds + * @returns The calculated spacing value between images + */ +export default function calculateSpacingBetweenImageIds( + imageIds: string[] +): number { + const { + imagePositionPatient: referenceImagePositionPatient, + imageOrientationPatient, + } = metaData.get('imagePlaneModule', imageIds[0]); + + // Calculate scan axis normal from image orientation + const rowCosineVec = vec3.fromValues( + imageOrientationPatient[0], + imageOrientationPatient[1], + imageOrientationPatient[2] + ); + const colCosineVec = vec3.fromValues( + imageOrientationPatient[3], + imageOrientationPatient[4], + imageOrientationPatient[5] + ); + + const scanAxisNormal = vec3.create(); + vec3.cross(scanAxisNormal, rowCosineVec, colCosineVec); + + // Convert referenceImagePositionPatient to vec3 + const refIppVec = vec3.fromValues( + referenceImagePositionPatient[0], + referenceImagePositionPatient[1], + referenceImagePositionPatient[2] + ); + + // Check if we are using wadouri scheme + const usingWadoUri = imageIds[0].split(':')[0] === 'wadouri'; + let spacing: number; + + function getDistance(imageId: string) { + const { imagePositionPatient } = metaData.get('imagePlaneModule', imageId); + const positionVector = vec3.create(); + + // Convert imagePositionPatient to vec3 + const ippVec = vec3.fromValues( + imagePositionPatient[0], + imagePositionPatient[1], + imagePositionPatient[2] + ); + + vec3.sub(positionVector, refIppVec, ippVec); + return vec3.dot(positionVector, scanAxisNormal); + } + + if (!usingWadoUri) { + const distanceImagePairs = imageIds.map((imageId) => { + const distance = getDistance(imageId); + return { + distance, + imageId, + }; + }); + + distanceImagePairs.sort((a, b) => b.distance - a.distance); + const numImages = distanceImagePairs.length; + + // Calculated average spacing. + // We would need to resample if these are not similar. + // It should be up to the host app to do this if it needed to. + spacing = + Math.abs( + distanceImagePairs[numImages - 1].distance - + distanceImagePairs[0].distance + ) / + (numImages - 1); + } else { + // Using wadouri, so we have only prefetched the first, middle, and last + // images for metadata. Assume initial imageId array order is pre-sorted, + // but check orientation. + const prefetchedImageIds = [ + imageIds[0], + imageIds[Math.floor(imageIds.length / 2)], + ]; + + const firstImageDistance = getDistance(prefetchedImageIds[0]); + const middleImageDistance = getDistance(prefetchedImageIds[1]); + + const metadataForMiddleImage = metaData.get( + 'imagePlaneModule', + prefetchedImageIds[1] + ); + + if (!metadataForMiddleImage) { + throw new Error('Incomplete metadata required for volume construction.'); + } + + const positionVector = vec3.create(); + + // Convert metadataForMiddleImage.imagePositionPatient to vec3 + const middleIppVec = vec3.fromValues( + metadataForMiddleImage.imagePositionPatient[0], + metadataForMiddleImage.imagePositionPatient[1], + metadataForMiddleImage.imagePositionPatient[2] + ); + + vec3.sub(positionVector, refIppVec, middleIppVec); + const distanceBetweenFirstAndMiddleImages = vec3.dot( + positionVector, + scanAxisNormal + ); + spacing = + Math.abs(distanceBetweenFirstAndMiddleImages) / + Math.floor(imageIds.length / 2); + } + + const { sliceThickness, spacingBetweenSlices } = metaData.get( + 'imagePlaneModule', + imageIds[0] + ); + + const { strictZSpacingForVolumeViewport } = getConfiguration().rendering; + + // We implemented these lines for multiframe dicom files that does not have + // position for each frame, leading to incorrect calculation of spacing = 0 + // If possible, we use the sliceThickness, but we warn about this dicom file + // weirdness. If sliceThickness is not available, we set to 1 just to render + if (spacing === 0 && !strictZSpacingForVolumeViewport) { + if (spacingBetweenSlices) { + console.debug('Could not calculate spacing. Using spacingBetweenSlices'); + spacing = spacingBetweenSlices; + } else if (sliceThickness) { + console.debug( + 'Could not calculate spacing and no spacingBetweenSlices. Using sliceThickness' + ); + spacing = sliceThickness; + } else { + console.debug( + 'Could not calculate spacing. The VolumeViewport visualization is compromised. Setting spacing to 1 to render' + ); + spacing = 1; + } + } + + return spacing; +} diff --git a/packages/core/src/utilities/fnv1aHash.ts b/packages/core/src/utilities/fnv1aHash.ts new file mode 100644 index 0000000000..713787a143 --- /dev/null +++ b/packages/core/src/utilities/fnv1aHash.ts @@ -0,0 +1,14 @@ +/** + * Generates a hash for a string using FNV-1a algorithm + * @param str - string to hash + * @returns the hashed string in base 36 + */ +export default function fnv1aHash(str: string): string { + let hash = 0x811c9dc5; + for (let i = 0; i < str.length; i++) { + hash ^= str.charCodeAt(i); + hash += + (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); + } + return (hash >>> 0).toString(36); +} diff --git a/packages/core/src/utilities/index.ts b/packages/core/src/utilities/index.ts index ca345f5322..86db4f8cd9 100644 --- a/packages/core/src/utilities/index.ts +++ b/packages/core/src/utilities/index.ts @@ -80,6 +80,7 @@ import * as color from './color'; import { deepEqual } from './deepEqual'; import type { IViewport } from '../types/IViewport'; import FrameRange from './FrameRange'; +import fnv1aHash from './fnv1aHash'; // solving the circular dependency issue import { _getViewportModality } from './getViewportModality'; @@ -94,7 +95,7 @@ import scroll from './scroll'; import clip from './clip'; import createSubVolume from './createSubVolume'; import getVolumeDirectionVectors from './getVolumeDirectionVectors'; - +import calculateSpacingBetweenImageIds from './calculateSpacingBetweenImageIds'; const getViewportModality = (viewport: IViewport, volumeId?: string) => _getViewportModality(viewport, volumeId, cache.getVolume); @@ -108,6 +109,7 @@ export { scaleRgbTransferFunction, triggerEvent, imageIdToURI, + fnv1aHash, calibratedPixelSpacingMetadataProvider, clamp, uuidv4, @@ -193,4 +195,5 @@ export { transformWorldToIndexContinuous, createSubVolume, getVolumeDirectionVectors, + calculateSpacingBetweenImageIds, }; diff --git a/packages/core/src/utilities/pointInShapeCallback.ts b/packages/core/src/utilities/pointInShapeCallback.ts index 3ef5200d65..7851b2bda2 100644 --- a/packages/core/src/utilities/pointInShapeCallback.ts +++ b/packages/core/src/utilities/pointInShapeCallback.ts @@ -73,10 +73,17 @@ export function pointInShapeCallback( if ((imageData as CPUImageData).getScalarData) { scalarData = (imageData as CPUImageData).getScalarData(); } else { - scalarData = (imageData as vtkImageData) - .getPointData() - .getScalars() - .getData(); + const scalars = (imageData as vtkImageData).getPointData().getScalars(); + + if (scalars) { + scalarData = scalars.getData(); + } else { + // @ts-ignore + const { voxelManager } = imageData.get('voxelManager') || {}; + if (voxelManager) { + scalarData = voxelManager.getCompleteScalarDataArray(); + } + } } const dimensions = imageData.getDimensions(); diff --git a/packages/core/src/utilities/sortImageIdsAndGetSpacing.ts b/packages/core/src/utilities/sortImageIdsAndGetSpacing.ts index ef5fba217c..216ac1c784 100644 --- a/packages/core/src/utilities/sortImageIdsAndGetSpacing.ts +++ b/packages/core/src/utilities/sortImageIdsAndGetSpacing.ts @@ -1,6 +1,6 @@ import { vec3 } from 'gl-matrix'; import * as metaData from '../metaData'; -import { getConfiguration } from '../init'; +import calculateSpacingBetweenImageIds from './calculateSpacingBetweenImageIds'; import type { Point3 } from '../types'; interface SortedImageIdsItem { @@ -15,7 +15,7 @@ interface SortedImageIdsItem { * @param imageIds - array of imageIds * @param scanAxisNormal - [x, y, z] array or gl-matrix vec3 * - * @returns The sortedImageIds, zSpacing, and origin of the first image in the series. + * @returns The sortedImageIds, spacing, and origin of the first image in the series. */ export default function sortImageIdsAndGetSpacing( imageIds: string[], @@ -42,20 +42,12 @@ export default function sortImageIdsAndGetSpacing( vec3.cross(scanAxisNormal, rowCosineVec, colCosineVec); } - const refIppVec = vec3.create(); - // Check if we are using wadouri scheme const usingWadoUri = imageIds[0].split(':')[0] === 'wadouri'; - vec3.set( - refIppVec, - referenceImagePositionPatient[0], - referenceImagePositionPatient[1], - referenceImagePositionPatient[2] - ); + const zSpacing = calculateSpacingBetweenImageIds(imageIds); let sortedImageIds: string[]; - let zSpacing: number; function getDistance(imageId: string) { const { imagePositionPatient } = metaData.get('imagePlaneModule', imageId); @@ -89,19 +81,7 @@ export default function sortImageIdsAndGetSpacing( }); distanceImagePairs.sort((a, b) => b.distance - a.distance); - sortedImageIds = distanceImagePairs.map((a) => a.imageId); - const numImages = distanceImagePairs.length; - - // Calculated average spacing. - // We would need to resample if these are not similar. - // It should be up to the host app to do this if it needed to. - zSpacing = - Math.abs( - distanceImagePairs[numImages - 1].distance - - distanceImagePairs[0].distance - ) / - (numImages - 1); } else { // Using wadouri, so we have only prefetched the first, middle, and last // images for metadata. Assume initial imageId array order is pre-sorted, @@ -116,62 +96,13 @@ export default function sortImageIdsAndGetSpacing( if (firstImageDistance - middleImageDistance < 0) { sortedImageIds.reverse(); } - - // Calculate average spacing between the first and middle prefetched images, - // otherwise fall back to DICOM `spacingBetweenSlices` - const metadataForMiddleImage = metaData.get( - 'imagePlaneModule', - prefetchedImageIds[1] - ); - - if (!metadataForMiddleImage) { - throw new Error('Incomplete metadata required for volume construction.'); - } - - const positionVector = vec3.create(); - - vec3.sub( - positionVector, - referenceImagePositionPatient, - metadataForMiddleImage.imagePositionPatient - ); - const distanceBetweenFirstAndMiddleImages = vec3.dot( - positionVector, - scanAxisNormal - ); - zSpacing = - Math.abs(distanceBetweenFirstAndMiddleImages) / - Math.floor(imageIds.length / 2); } - const { - imagePositionPatient: origin, - sliceThickness, - spacingBetweenSlices, - } = metaData.get('imagePlaneModule', sortedImageIds[0]); - - const { strictZSpacingForVolumeViewport } = getConfiguration().rendering; - - // We implemented these lines for multiframe dicom files that does not have - // position for each frame, leading to incorrect calculation of zSpacing = 0 - // If possible, we use the sliceThickness, but we warn about this dicom file - // weirdness. If sliceThickness is not available, we set to 1 just to render - if (zSpacing === 0 && !strictZSpacingForVolumeViewport) { - if (spacingBetweenSlices) { - console.log('Could not calculate zSpacing. Using spacingBetweenSlices'); - zSpacing = spacingBetweenSlices; - } else if (sliceThickness) { - console.log( - 'Could not calculate zSpacing and no spacingBetweenSlices. Using sliceThickness' - ); - zSpacing = sliceThickness; - } else { - console.log( - 'Could not calculate zSpacing. The VolumeViewport visualization is compromised. Setting zSpacing to 1 to render' - ); - zSpacing = 1; - } - } + const { imagePositionPatient: origin } = metaData.get( + 'imagePlaneModule', + sortedImageIds[0] + ); + const result: SortedImageIdsItem = { zSpacing, origin, diff --git a/packages/core/src/webWorkerManager/webWorkerManager.js b/packages/core/src/webWorkerManager/webWorkerManager.js index 79c69864bc..eb1286274b 100644 --- a/packages/core/src/webWorkerManager/webWorkerManager.js +++ b/packages/core/src/webWorkerManager/webWorkerManager.js @@ -52,6 +52,7 @@ class CentralizedWorkerManager { autoTerminateOnIdle: autoTerminateOnIdle.enabled, idleCheckIntervalId: null, idleTimeThreshold: autoTerminateOnIdle.idleTimeThreshold, + options: options, }; workerProperties.loadCounters = Array(maxWorkerInstances).fill(0); @@ -156,6 +157,8 @@ class CentralizedWorkerManager { workerProperties.processing = true; + // augment args with options + args = { ...args, ...workerProperties.options }; const results = await api[methodName](args, ...finalCallbacks); workerProperties.processing = false; diff --git a/packages/core/test/stackViewport_node_render.jest.js b/packages/core/test/stackViewport_node_render.jest.js new file mode 100644 index 0000000000..3175697d65 --- /dev/null +++ b/packages/core/test/stackViewport_node_render.jest.js @@ -0,0 +1,117 @@ +import * as cornerstone3D from '../src/index'; +import { + fakeImageLoader, + fakeMetaDataProvider, +} from '../../../utils/test/testUtilsImageLoader'; + +import { describe, it, expect } from '@jest/globals'; +import { render } from 'react-dom'; + +const { + utilities, + setUseCPURendering, + RenderingEngine, + Enums, + imageLoader, + metaData, + init, +} = cornerstone3D; +const { ViewportType, Events } = Enums; + +const renderingEngineId = utilities.uuidv4(); +const viewportId = 'VIEWPORT'; + +function encodeImageIdInfo(info) { + return `fakeImageLoader:${encodeURIComponent(JSON.stringify(info))}`; +} + +const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 20, + barWidth: 5, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, +}; + +const imageId = encodeImageIdInfo(imageInfo); + +function setSize(element, width, height) { + Object.defineProperty(element, 'offsetWidth', { value: width }); + Object.defineProperty(element, 'offsetHeight', { value: height }); + Object.defineProperty(element, 'clientWidth', { value: width }); + Object.defineProperty(element, 'clientHeight', { value: height }); + Object.defineProperty(element, 'getBoundingClientRect', { + value: () => ({ width, height }), + }); +} + +function initCore() { + init({}); + imageLoader.registerImageLoader('fakeImageLoader', fakeImageLoader); + + metaData.addProvider(utilities.calibratedPixelSpacingMetadataProvider.get); + metaData.addProvider(utilities.genericMetadataProvider.get); + metaData.addProvider(fakeMetaDataProvider, 10000); + + document.body.innerHTML = `
+
+ +
+
`; + const renderingEngine = new RenderingEngine(renderingEngineId); + const element = document.getElementById('elementId'); + setSize(element, 100, 100); + const viewportElement = document.getElementById('viewport-element'); + setSize(viewportElement, 100, 100); + const canvasElement = document.getElementById('cornerstone-canvas'); + setSize(canvasElement, 100, 100); + return { renderingEngine, element }; +} + +describe('stackViewport_node_render', function () { + let viewport, element, renderingEngine; + + beforeEach(() => { + setUseCPURendering(true); + window.devicePixelRatio = 1; + + const initData = initCore(); + element = initData.element; + renderingEngine = initData.renderingEngine; + + const viewportInput = { + viewportId, + type: ViewportType.STACK, + element, + defaultOptions: { + background: [0.2, 0, 0.2], + }, + }; + + renderingEngine.enableElement(viewportInput); + viewport = renderingEngine.getViewport(viewportId); + }); + + it('Basic Viewport Creation', () => { + expect(viewport).not.toBeUndefined(); + }); + + it('Should render simple stack', () => { + let promise = new Promise((resolve) => { + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = viewport.getCanvas(); + const image = canvas.toDataURL('image/png'); + // console.error('Rendered image', image); + expect(image).toBeTruthy(); + resolve(image); + }); + }); + viewport.setStack([imageId], 0); + viewport.render(); + return promise; + }); +}); diff --git a/packages/dicomImageLoader/CHANGELOG.md b/packages/dicomImageLoader/CHANGELOG.md index bcb7df1113..6fea89d1ae 100644 --- a/packages/dicomImageLoader/CHANGELOG.md +++ b/packages/dicomImageLoader/CHANGELOG.md @@ -3,14 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [2.19.16](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.15...v2.19.16) (2025-02-26) +# [3.0.0-beta.6](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2025-02-27) **Note:** Version bump only for package @cornerstonejs/dicom-image-loader -## [2.19.15](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.14...v2.19.15) (2025-02-26) +# [3.0.0-beta.5](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2025-02-25) **Note:** Version bump only for package @cornerstonejs/dicom-image-loader +# [3.0.0-beta.4](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +# [3.0.0-beta.3](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +# [3.0.0-beta.2](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.14...v3.0.0-beta.2) (2025-02-24) + +### Bug Fixes + +- publish beta for 3.0 ([8bf65df](https://github.com/cornerstonejs/cornerstone3D/commit/8bf65df9bec5f52459de1c49c4834b316f680f1b)) + ## [2.19.14](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.13...v2.19.14) (2025-02-24) **Note:** Version bump only for package @cornerstonejs/dicom-image-loader diff --git a/packages/dicomImageLoader/package.json b/packages/dicomImageLoader/package.json index a96a6f23de..94b36c01c8 100644 --- a/packages/dicomImageLoader/package.json +++ b/packages/dicomImageLoader/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/dicom-image-loader", - "version": "2.19.16", + "version": "3.0.0-beta.6", "description": "Cornerstone Image Loader for DICOM WADO-URI and WADO-RS and Local file", "keywords": [ "DICOM", @@ -116,7 +116,7 @@ "uuid": "^9.0.0" }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.16", + "@cornerstonejs/core": "^3.0.0-beta.6", "dicom-parser": "^1.8.9" }, "lint-staged": { diff --git a/packages/docs/docs/getting-started/vue-angular-react-etc.md b/packages/docs/docs/getting-started/vue-angular-react-etc.md deleted file mode 100644 index e6d235fed9..0000000000 --- a/packages/docs/docs/getting-started/vue-angular-react-etc.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -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. - -Follow the links below to see how to use cornerstone3D with your favorite framework. - -- [Cornerstone3D with React](https://github.com/cornerstonejs/vite-react-cornerstone3d) -- [Cornerstone3D with Vue](https://github.com/cornerstonejs/vue-cornerstone3d) -- [Cornerstone3D with Angular](https://github.com/cornerstonejs/angular-cornerstone3d) - - [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'; - -export default defineConfig({ - plugins: [viteCommonjs()], - optimizeDeps: { - exclude: ['@cornerstonejs/dicom-image-loader'], - include: ['dicom-parser'], - }, -}); -``` - -## Troubleshooting - -### 1. Polyseg & Labelmap interpolation - -By default, we don't include the `@icr/polyseg-wasm`, `itk-wasm`, and `@itk-wasm/morphological-contour-interpolation` libraries in our bundle to keep the size pretty small. If you need these features, you'll need to install them separately and import them into your project. You can do this by running - -```bash -yarn install @icr/polyseg-wasm itk-wasm @itk-wasm/morphological-contour-interpolation -``` - -### 1. 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"], - } -}, -``` - -:::note -You might need to add `external: ["itk-wasm", "@itk-wasm/morphological-contour-interpolation"],` to the rollupOptions as well -::: - -### 2. Path Resolution Issues with @cornerstonejs/core - -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. - -If you previously used - -`noParse: [/(codec)/],` - -to avoid parsing codecs in your webpack module, remove that line. The cornerstone3D library now includes the codecs as an ES module. - -Also since we are using wasm, you will need to add the following to your webpack configuration in the `module.rules` section: - -```javascript -{ - test: /\.wasm/, - type: 'asset/resource', -}, -``` - -## Svelte + Vite - -Similar to the configuration above, use the CommonJS plugin converting commonjs to esm. Otherwise, it will be pending at `await viewport.setStack(stack);`, the image will not be rendered. - -```javascript -import { defineConfig } from 'vite'; -import { svelte } from '@sveltejs/vite-plugin-svelte'; -import { viteCommonjs } from '@originjs/vite-plugin-commonjs'; - -export default defineConfig({ - plugins: [svelte(), viteCommonjs()], - optimizeDeps: { - exclude: ['@cornerstonejs/dicom-image-loader'], - include: ['dicom-parser'], - }, -}); -``` - -:::note Tip -If you are using `sveltekit`, and config like `plugins: [ sveltekit(), viteCommonjs() ]`, `viteCommonjs()` may not work. -Try replace `sveltekit` with `vite-plugin-svelte` and it will work. -::: diff --git a/packages/docs/docs/getting-started/vue-angular-react-vite.md b/packages/docs/docs/getting-started/vue-angular-react-vite.md new file mode 100644 index 0000000000..9e9c61e509 --- /dev/null +++ b/packages/docs/docs/getting-started/vue-angular-react-vite.md @@ -0,0 +1,244 @@ +--- +id: vue-angular-react-etc +title: 'React, Vue, Angular, etc.' +--- + +Here are some examples of how to use cornerstone3D with React, Vue, Angular, vite-based frameworks, etc. +We have made it easy to use cornerstone3D with your favorite framework. + +Follow the links below to see how to use cornerstone3D with your favorite framework. + +- [Cornerstone3D with vite-based React](https://github.com/cornerstonejs/vite-react-cornerstone3d) +- [Cornerstone3D with vite-based Vue](https://github.com/cornerstonejs/vite-vue-cornerstone3d) +- [Cornerstone3D with Angular](https://github.com/cornerstonejs/angular-cornerstone3d) + - [Community maintained project](https://github.com/yanqzsu/ng-cornerstone) +- [Cornerstone3D with Next.js](https://github.com/cornerstonejs/nextjs-cornerstone3d) + +## Vite + +### Basic Setup + +The following is an example of a Vite configuration for a vite-based project that works with cornerstone3D. + +```javascript +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { viteCommonjs } from '@originjs/vite-plugin-commonjs'; + +export default defineConfig({ + plugins: [ + react(), + // for dicom-parser + viteCommonjs(), + ], + // seems like only required in dev mode + optimizeDeps: { + exclude: ['@cornerstonejs/dicom-image-loader'], + include: ['dicom-parser'], + }, + worker: { + format: 'es', + }, +}); +``` + +:::note +This configuration is for basic usage of cornerstone3D tools, no polySeg and no labelmap interpolation +::: + +### Advanced Setup + +#### PolySeg + +If you need to use polyseg to convert between segmentation representations, you can add the following as a dependency and initialize the cornerstoneTools with the following configuration: + +```bash +yarn add @cornerstonejs/polymorphic-segmentation +``` + +```js +import * as polySeg from '@cornerstonejs/polymorphic-segmentation'; +import { init } from '@cornerstonejs/tools'; + +initialize({ + addons: { + polySeg, + }, +}); +``` + +Next, you'll need to edit the Vite configuration to include the following. Keep in mind that we're including the WASM files in the build and excluding them from dependency optimization. There is an ongoing issue in vite with `import.meta.url` ([check their GitHub issue](https://github.com/vitejs/vite/issues/8427)), which force us to exclude the wasm files from optimization of dependencies. + +```js +export default defineConfig({ + assetsInclude: ['**/*.wasm'], + plugins: [ + react(), + // for dicom-parser + viteCommonjs(), + ], + // seems like only required in dev mode + optimizeDeps: { + exclude: [ + '@cornerstonejs/dicom-image-loader', + '@cornerstonejs/polymorphic-segmentation', + ], + include: ['dicom-parser'], + }, + worker: { + format: 'es', + }, +}); +``` + +#### Labelmap Interpolation + +you need to add the following to your vite config: + +```bash +yarn add @cornerstonejs/labelmap-interpolation +``` + +and then you need to edit the vite config to include the following: + +```js +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { viteCommonjs } from '@originjs/vite-plugin-commonjs'; + +export default defineConfig({ + assetsInclude: ['**/*.wasm'], + plugins: [ + react(), + // for dicom-parser + viteCommonjs(), + ], + // seems like only required in dev mode + optimizeDeps: { + exclude: [ + '@cornerstonejs/dicom-image-loader', + '@cornerstonejs/polymorphic-segmentation', + '@cornerstonejs/labelmap-interpolation', + ], + include: ['dicom-parser'], + }, + worker: { + format: 'es', + }, +}); +``` + +## Webpack + +### Basic Setup + +It should work out of the box with no configuration, so the following `nextjs.config.js` is the only thing you need to add. + +```js +/** @type {import('next').NextConfig} */ +const nextConfig = { + webpack: (config) => { + // resolve fs for one of the dependencies + config.resolve.fallback = { + fs: false, + }; + + return config; + }, +}; + +export default nextConfig; +``` + +### Advanced Setup (PolySeg & Labelmap Interpolation) + +You might need to add + +```js + +``` + +## Troubleshooting + +### 1. Rollup Options + +By default, we don't include the `@icr/polyseg-wasm`, `itk-wasm`, and `@itk-wasm/morphological-contour-interpolation` libraries in our bundle to keep the size pretty small. +Rollup **might** complain about these libraries, so you can add the following to the rollupOptions: + +```js +worker: { + format: "es", + rollupOptions: { + external: ["@icr/polyseg-wasm"], + }, + }, +``` + +### 2. Path Resolution Issues with @cornerstonejs/core + +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. + +If you previously used + +`noParse: [/(codec)/],` + +to avoid parsing codecs in your webpack module, remove that line. The cornerstone3D library now includes the codecs as an ES module. + +Also since we are using wasm, you will need to add the following to your webpack configuration in the `module.rules` section: + +```javascript +{ + test: /\.wasm/, + type: 'asset/resource', +}, +``` + +### 5. Svelte + Vite + +Similar to the configuration above, use the CommonJS plugin converting commonjs to esm. Otherwise, it will be pending at `await viewport.setStack(stack);`, the image will not be rendered. + +```javascript +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { viteCommonjs } from '@originjs/vite-plugin-commonjs'; + +export default defineConfig({ + plugins: [svelte(), viteCommonjs()], + optimizeDeps: { + exclude: ['@cornerstonejs/dicom-image-loader'], + include: ['dicom-parser'], + }, +}); +``` + +:::note Tip +If you are using `sveltekit`, and config like `plugins: [ sveltekit(), viteCommonjs() ]`, `viteCommonjs()` may not work. +Try replace `sveltekit` with `vite-plugin-svelte` and it will work. diff --git a/packages/docs/docs/migration-guides/2x/1-general.md b/packages/docs/docs/migration-guides/2x/1-general.md index 6d85d265e9..7ccbe7a9e5 100644 --- a/packages/docs/docs/migration-guides/2x/1-general.md +++ b/packages/docs/docs/migration-guides/2x/1-general.md @@ -28,7 +28,7 @@ Watch this video guide for a [visual walkthrough](https://www.youtube.com/embed/ We have worked hard to enhance the developer experience when using Cornerstone3D with various frameworks like React, Vue, Angular, Vite, and Webpack. -For more information, please refer to the [frameworks](../../getting-started/vue-angular-react-etc.md) page. +For more information, please refer to the [frameworks](../../getting-started/vue-angular-react-vite.md) page. You need to modify your Vite and Webpack configurations to correctly import the Cornerstone3D library. Check each framework's repository for more details. @@ -39,7 +39,6 @@ We have streamlined the process of loading volumes without sacrificing speed by You can remove `Cross-Origin-Opener-Policy` and `Cross-Origin-Embedder-Policy` from your custom headers if you don't need them in other aspects of your app. - ## Typescript Version We have upgraded the typescript version from 4.6 to 5.5 in the 2.0 version of the cornerstone3D. @@ -76,10 +75,9 @@ Both Node.js and modern browsers now support ECMAScript Modules (ESM) by default :::note Tip -If you must use CJS, for example, if you are using `dicom-image-loader` and `dicom-parser`, you need to use `vite-plugin-commonjs` to convert CommonJS to ESM. For more information, please refer to the [Frameworks](../../getting-started/vue-angular-react-etc.md) page. +If you must use CJS, for example, if you are using `dicom-image-loader` and `dicom-parser`, you need to use `vite-plugin-commonjs` to convert CommonJS to ESM. For more information, please refer to the [Frameworks](../../getting-started/vue-angular-react-vite.md) page. ::: - ## Package Exports The Cornerstone libraries now utilize the `exports` field in their `package.json` files. This allows for more precise control over how modules are imported and ensures compatibility with different build systems. diff --git a/packages/docs/docs/migration-guides/3x/1-polyseg.md b/packages/docs/docs/migration-guides/3x/1-polyseg.md new file mode 100644 index 0000000000..c8c174c416 --- /dev/null +++ b/packages/docs/docs/migration-guides/3x/1-polyseg.md @@ -0,0 +1,91 @@ +--- +id: general +title: 'General' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Externalized PolySeg + +PolySeg has been moved from the `cornerstoneTools` package and is now a standalone package called @cornerstonejs/polymorphic-segmentation. + +## Usage + +Now, it's not included in the `cornerstoneTools` package anymore. If you need to enable polymorphic conversions, you'll have to install it and initialize `cornerstoneTools` with it. + +```js +import * as polyseg from '@cornerstonejs/polymorphic-segmentation'; +import { init } from '@cornerstonejs/tools'; + +init({ + addons: { + polyseg, + }, +}); +``` + +:::note +This change was made because we weren't shipping the cornerstone tools with our `polyseg-wasm` dependencies. There were a few issues with bundlers complaining about the static assets included. Now, those who don't want to use it are fine, and those who do will need to install it and initialize `cornerstoneTools` themselves. +::: + +## Exports + +We weren't exposing any functions from the `tools` directory. If you need something, import it from `@cornerstonejs/polymorphic-segmentation`. +It exports the following: + +```js +import { + canComputeRequestedRepresentation, + // computes + computeContourData, + computeLabelmapData, + computeSurfaceData, + // updates + updateSurfaceData, + // init + init, +} from '@cornerstonejs/polymorphic-segmentation'; +``` + +### computeAndAddContourRepresentation, computeAndAddLabelmapRepresentation, computeAndAddSurfaceRepresentation + +have been removed from the `tools` directory. If you happen to need them (unlikely), you'll have to build them yourself. + +```js +import { utilities } from '@cornerstonejs/tools'; +import { computeLabelmapData } from '@cornerstonejs/polymorphic-segmentation'; + +const { computeAndAddRepresentation } = utilities.segmentation; + +// for labelmap +const labelmapData = await computeAndAddRepresentation( + segmentationId, + Representations.Labelmap, + () => computeLabelmapData(segmentationId, { viewport }), + () => null +); + +// for surface +import { + computeSurfaceData, + updateSurfaceData, +} from '@cornerstonejs/polymorphic-segmentation'; + +const SurfaceData = await computeAndAddRepresentation( + segmentationId, + Representations.Surface, + () => computeSurfaceData(segmentationId, { viewport }), + () => updateSurfaceData(segmentationId, { viewport }) +); + +// same for contour +import { computeContourData } from '@cornerstonejs/polymorphic-segmentation'; + +const contourData = await computeAndAddRepresentation( + segmentationId, + Representations.Contour, + () => computeContourData(segmentationId, { viewport }), + () => undefined +); +``` diff --git a/packages/docs/docs/migration-guides/3x/2-threshold-tools.md b/packages/docs/docs/migration-guides/3x/2-threshold-tools.md new file mode 100644 index 0000000000..866d90714c --- /dev/null +++ b/packages/docs/docs/migration-guides/3x/2-threshold-tools.md @@ -0,0 +1,99 @@ +--- +id: threshold-tools +title: 'Labelmap Thresholding Tools' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Key Changes: + +* The nested `strategySpecificConfiguration` object has been removed completely +* Configuration properties have been moved to the root level of the configuration object +* Threshold configuration has been restructured: + * `threshold` array is now a `range` property inside a `threshold` object + * Additional threshold properties (`isDynamic`, `dynamicRadius`) are part of the same object +* `setBrushThresholdForToolGroup()` function signature has changed to accept a structured threshold object +* Strategy-specific properties like `useCenterSegmentIndex` have been moved to the root configuration level +* `activeStrategy` is now a standalone property in tool operations data, no longer inside a nested configuration + +## Migration Steps: + +### 1. Replace strategySpecificConfiguration with direct properties + +**Before:** +```diff +- configuration: { +- activeStrategy: 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL', +- strategySpecificConfiguration: { +- THRESHOLD: { +- threshold: [-150, -70], +- // other threshold properties +- }, +- useCenterSegmentIndex: true, +- }, +- } +``` + +**After:** +```diff ++ configuration: { ++ activeStrategy: 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL', ++ threshold: { ++ range: [-150, -70], ++ isDynamic: false, ++ // other threshold properties directly here ++ }, ++ useCenterSegmentIndex: true, ++ } +``` + +### 2. Update threshold configuration structure + +**Before:** +```diff +- strategySpecificConfiguration: { +- THRESHOLD: { +- threshold: [-150, -70], // Previous threshold array format +- isDynamic: false, +- dynamicRadius: 5 +- } +- } +``` + +**After:** +```diff ++ threshold: { ++ range: [-150, -70], // New 'range' property replaces 'threshold' ++ isDynamic: false, ++ dynamicRadius: 5 ++ } +``` + +### 3. Update setBrushThresholdForToolGroup calls + +**Before:** +```diff +- segmentationUtils.setBrushThresholdForToolGroup( +- toolGroupId, +- thresholdArgs.threshold, +- thresholdArgs +- ); +``` + +**After:** +```diff ++ segmentationUtils.setBrushThresholdForToolGroup( ++ toolGroupId, ++ fullThresholdArgs ++ ); +``` + +Note that `thresholdArgs` should now be an object with the structure: +```javascript +{ + range: [min, max], // Previously 'threshold' + isDynamic: boolean, + dynamicRadius: number +} +``` diff --git a/packages/docs/docs/migration-guides/3x/3-labelmap-interpolation.md b/packages/docs/docs/migration-guides/3x/3-labelmap-interpolation.md new file mode 100644 index 0000000000..a37f8992c3 --- /dev/null +++ b/packages/docs/docs/migration-guides/3x/3-labelmap-interpolation.md @@ -0,0 +1,71 @@ +--- +id: general +title: 'General' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Not a composition but a utility + +Previously, interpolation was a brush composition, restricting its use to tools inheriting from a brush. However, interpolation should really be a utility anyone can use, even without a tool. + +Before, you had to use this workaround for interpolation: + +```js +addButtonToToolbar({ + title: 'Run Overlapping Interpolation', + onClick: () => { + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + const activeName = toolGroup.getActivePrimaryMouseButtonTool(); + const brush = toolGroup.getToolInstance(activeName); + brush.interpolate?.(element1, { extendedConfig: false }); + }, +}); +``` + +Now it's as simple as this: + +```js +import * as labelmapInterpolation from '@cornerstonejs/labelmap-interpolation'; + +labelmapInterpolation.interpolate({ + segmentationId, + segmentIndex, +}); +``` + +:::note +We once again had to implement a workaround for `itk-wasm` as a dynamic dependency to prevent bundler problems in cornerstone3D 2.0. However, this caused numerous issues. Now, it's a separate, standalone utility package that doesn't need to be bundled with cornerstone3D. +::: + +## Migration + +Remove the `labelmap` interpolation from your custom tools composition. + +Before: + +```javascript +const RECTANGLE_STRATEGY = new BrushStrategy( + 'Rectangle', + compositions.regionFill, + compositions.setValue, + initializeRectangle, + compositions.determineSegmentIndex, + compositions.preview, + compositions.labelmapInterpolation +); +``` + +After: + +```javascript +const RECTANGLE_STRATEGY = new BrushStrategy( + 'Rectangle', + compositions.regionFill, + compositions.setValue, + initializeRectangle, + compositions.determineSegmentIndex, + compositions.preview +); +``` diff --git a/packages/docs/docs/migration-guides/3x/4-get-statistics.md b/packages/docs/docs/migration-guides/3x/4-get-statistics.md new file mode 100644 index 0000000000..f3aed9181c --- /dev/null +++ b/packages/docs/docs/migration-guides/3x/4-get-statistics.md @@ -0,0 +1,40 @@ +--- +id: statistics +title: 'Segmentation Statistics API' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + +## Key Changes: + +* Statistics calculation has been moved from brush tool methods to a dedicated utility function +* Statistics are now calculated asynchronously using web workers +* The function signature for getting statistics has changed completely +* Progress events are now emitted during statistics calculation + +## Migration Steps: + +### 1. Replace tool-based statistics methods with the standalone utility + +**Before:** +```diff +- const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); +- const activeName = toolGroup.getActivePrimaryMouseButtonTool(); +- const brush = toolGroup.getToolInstance(activeName); +- const stats = brush.getStatistics(viewport.element, { indices }); +``` + +**After:** +```diff ++ const stats = await segmentationUtils.getStatistics({ ++ segmentationId, ++ segmentIndices: indices, ++ viewportId: viewport.id, ++ }); +``` + +:::note +ViewportId is needed since some statistics calculations are performed regarding the base image in the viewport. +::: diff --git a/packages/docs/docs/migration-guides/3x/5-adapters.md b/packages/docs/docs/migration-guides/3x/5-adapters.md new file mode 100644 index 0000000000..4160751e23 --- /dev/null +++ b/packages/docs/docs/migration-guides/3x/5-adapters.md @@ -0,0 +1,64 @@ +--- +id: adapters +title: 'Adapters API' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + +## Key Changes: + +* MeasurementsReport has two maps instead of objects for setting the + adapter classes, mapping the tool type to adapter class and the + tracking id to adapter class. +* A new register additional tracking id method exists to allow adding + custom adapter methods. +* Adapter implementations now have a base class to handle some of the + definition. This allows calling into the base class to handle some of the + definition such as the is tracking handling. +* The MeasurementsReport class is now extensible to create a new class with + completely different default handling. To do this, the two map attributes + need to be redeclared, and the new instance registered for the handlers. +* There is now an init method to create tracking identifiers and register a new + handler. +* The annotation changed event no longer requires the viewport id/rendering id + * This change is done so that measurements can be updated when not visible + +## Migration Steps: + +### 1. Replace MeasurementsReports.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE + +**Before:** +```diff +- const toolClass = MeasurementReports.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE[toolType]; +``` + +**After:** +```diff +- const toolClass = MeasurementReports.measurementAdapterByToolType.get(toolType); +``` + +### 2. Replace Tool instance adapter registration which is identical to existing registration + +**Before:** +```diff +- class MyNewToolAdapter { ... identical to eg Probe Adapter } +``` + +**After:** +```diff +- const MyNewToolAdapter = Probe.initCopy('MyNewTool'); +``` + +### 3. Replace old tool registration with registerTrackingIdentifier + +**Before:** +```diff +- class OldToolAdapter { ... identical to eg Length v1.0 except has :v1.0 at end of tracking identifier } +``` + +**After:** +```diff +- MeasurementReport.registerTrackingIdentifier(Length, `${Length.trackingIdentifierTextValue}:v1.0`); +``` diff --git a/packages/docs/package.json b/packages/docs/package.json index 5b29821946..e012097c6c 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "docs", - "version": "2.1.10", + "version": "3.0.0-beta.5", "private": true, "repository": "https://github.com/cornerstonejs/cornerstone3D", "scripts": { @@ -33,11 +33,11 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@cornerstonejs/adapters": "^2.19.16", - "@cornerstonejs/core": "^2.19.16", - "@cornerstonejs/dicom-image-loader": "^2.19.16", - "@cornerstonejs/nifti-volume-loader": "^2.19.16", - "@cornerstonejs/tools": "^2.19.16", + "@cornerstonejs/adapters": "^3.0.0-beta.6", + "@cornerstonejs/core": "^3.0.0-beta.6", + "@cornerstonejs/dicom-image-loader": "^3.0.0-beta.6", + "@cornerstonejs/nifti-volume-loader": "^3.0.0-beta.6", + "@cornerstonejs/tools": "^3.0.0-beta.6", "@docusaurus/core": "3.6.3", "@docusaurus/faster": "3.6.3", "@docusaurus/module-type-aliases": "3.6.3", diff --git a/packages/labelmap-interpolation/CHANGELOG.md b/packages/labelmap-interpolation/CHANGELOG.md new file mode 100644 index 0000000000..9fd8d13205 --- /dev/null +++ b/packages/labelmap-interpolation/CHANGELOG.md @@ -0,0 +1,22 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.0.0-beta.6](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2025-02-27) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +# [3.0.0-beta.5](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +# [3.0.0-beta.4](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +# [3.0.0-beta.3](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +# Change Log diff --git a/packages/labelmap-interpolation/README.md b/packages/labelmap-interpolation/README.md new file mode 100644 index 0000000000..568ed58f98 --- /dev/null +++ b/packages/labelmap-interpolation/README.md @@ -0,0 +1,60 @@ +# Cornerstone Segmentation Labelmap Interpolation + +This package provides a utility for interpolating labelmaps in 3D medical imaging. It leverages the power of `itk-wasm` and `@itk-wasm/morphological-contour-interpolation` to perform morphological contour interpolation between segmented slices. + +## Overview + +When creating segmentations in 3D medical imaging, it's often time-consuming to manually segment every slice. This package allows you to segment only a subset of slices and then automatically interpolate the segmentation between those slices, significantly reducing the time required for complete 3D segmentation. + +## Installation + +```bash +npm install @cornerstonejs/labelmap-interpolation +``` + +## Usage + +### Basic Usage + +```typescript +import { interpolate } from '@cornerstonejs/labelmap-interpolation'; + +// Run interpolation on a specific segment +interpolate({ + segmentationId: 'MY_SEGMENTATION_ID', + segmentIndex: 1, // The segment index to interpolate +}); +``` + +### With Configuration Options + +```typescript +import { interpolate } from '@cornerstonejs/labelmap-interpolation'; + +// Run interpolation with custom configuration +interpolate({ + segmentationId: 'MY_SEGMENTATION_ID', + segmentIndex: 1, + configuration: { + axis: 2, // Axis along which to perform interpolation (0=X, 1=Y, 2=Z) + noHeuristicAlignment: false, // Whether to disable heuristic alignment + noUseDistanceTransform: false, // Whether to disable distance transform + useCustomSlicePositions: false, // Whether to use custom slice positions + preview: false // Whether to preview the interpolation result + } +}); +``` + +## How It Works + +The interpolation process works by: + +1. Taking a segmentation volume with segments on non-adjacent slices +2. Using morphological contour interpolation to fill in the missing slices +3. Updating the segmentation volume with the interpolated data + +The package uses web workers to perform the interpolation in a background thread, preventing UI freezes during computation. + +## License + +MIT diff --git a/packages/labelmap-interpolation/api-extractor.json b/packages/labelmap-interpolation/api-extractor.json new file mode 100644 index 0000000000..4ddb5f44d3 --- /dev/null +++ b/packages/labelmap-interpolation/api-extractor.json @@ -0,0 +1,9 @@ +{ + "extends": "../../api-extractor.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/esm/index.d.ts", + "apiReport": { + "reportFileName": ".api.md", + "reportFolder": "../../common/reviews/api" + } +} diff --git a/packages/labelmap-interpolation/examples/labelmapInterpolation/index.ts b/packages/labelmap-interpolation/examples/labelmapInterpolation/index.ts new file mode 100644 index 0000000000..24307a33ce --- /dev/null +++ b/packages/labelmap-interpolation/examples/labelmapInterpolation/index.ts @@ -0,0 +1,300 @@ +import { + RenderingEngine, + Enums, + imageLoader, + eventTarget, +} from '@cornerstonejs/core'; +import * as cornerstoneTools from '@cornerstonejs/tools'; +import { + createImageIdsAndCacheMetaData, + initDemo, + setTitleAndDescription, + addBrushSizeSlider, +} from '../../../../utils/demo/helpers'; + +// This is for debugging purposes +console.debug( + 'Click on index.ts to open source code for this example --------->' +); + +const { + ToolGroupManager, + ZoomTool, + StackScrollTool, + Enums: csToolsEnums, + RectangleScissorsTool, + CircleScissorsTool, + BrushTool, + PaintFillTool, + PanTool, + segmentation, + utilities: cstUtils, +} = cornerstoneTools; + +const { MouseBindings, KeyboardBindings, Events } = csToolsEnums; +const { ViewportType } = Enums; +const { segmentation: segmentationUtils, roundNumber } = cstUtils; + +// Define a unique id for the volume +let renderingEngine; +const renderingEngineId = 'myRenderingEngine'; +const viewportId = 'STACK_VIEWPORT'; +const toolGroupId = 'TOOL_GROUP_ID'; + +// ======== Set up page ======== // +setTitleAndDescription( + 'Stack Segmentation Statistics', + 'Here we demonstrate how to calculate statistics for a stack segmentation.' +); + +const size = '500px'; +const content = document.getElementById('content'); + +const statsGrid = document.createElement('div'); +statsGrid.style.display = 'flex'; +statsGrid.style.flexDirection = 'row'; +statsGrid.style.fontSize = 'smaller'; + +const statsIds = ['segment1', 'segment2', 'segmentCombined']; +const statsStyle = { + width: '20em', + height: '10em', +}; + +for (const statsId of statsIds) { + const statsDiv = document.createElement('div'); + statsDiv.id = statsId; + statsDiv.innerText = statsId; + Object.assign(statsDiv.style, statsStyle); + statsGrid.appendChild(statsDiv); +} + +content.appendChild(statsGrid); + +const viewportGrid = document.createElement('div'); +viewportGrid.style.display = 'flex'; +viewportGrid.style.flexDirection = 'row'; + +const element = document.createElement('div'); +element.style.width = size; +element.style.height = size; +element.oncontextmenu = () => false; + +viewportGrid.appendChild(element); +content.appendChild(viewportGrid); + +const instructions = document.createElement('p'); +instructions.innerText = ` + Left Click: Use selected Segmentation Tool. + Middle Click: Pan + Right Click: Zoom + Mouse wheel: Scroll Stack + `; + +content.append(instructions); + +// ============================= // + +function displayStat(stat) { + if (!stat) { + return; + } + return `${stat.label || stat.name}: ${roundNumber(stat.value)} ${ + stat.unit ? stat.unit : '' + }`; +} + +async function calculateStatistics(id, indices) { + const viewport = renderingEngine.getViewport(viewportId); + const stats = await segmentationUtils.getStatistics({ + segmentationId: 'SEGMENTATION_ID', + segmentIndices: indices, + viewportId: viewport.id, + }); + + if (!stats) { + return; + } + const items = [`Statistics on ${indices.join(', ')}`]; + stats.count.label = 'Voxels'; + + items.push( + displayStat(stats.volume), + displayStat(stats.count), + displayStat(stats.mean), + displayStat(stats.max), + displayStat(stats.min), + displayStat(stats.peakValue) + ); + const statsDiv = document.getElementById(id); + statsDiv.innerHTML = items.map((span) => `${span}
\n`).join('\n'); +} + +let timeoutId; + +function segmentationModifiedCallback(evt) { + const { detail } = evt; + if (!detail || !detail.segmentIndex || detail.segmentIndex === 255) { + return; + } + + const statsId = detail.segmentIndex === 1 ? statsIds[0] : statsIds[1]; + + const debounced = () => { + calculateStatistics(statsId, [detail.segmentIndex]); + // Also update combined stats + calculateStatistics(statsIds[2], [1, 2]); + }; + + if (timeoutId) { + window.clearTimeout(timeoutId); + } + + timeoutId = window.setTimeout(debounced, 1000); +} + +// ============================= // + +function setupTools() { + // Add tools to Cornerstone3D + cornerstoneTools.addTool(PanTool); + cornerstoneTools.addTool(ZoomTool); + cornerstoneTools.addTool(StackScrollTool); + cornerstoneTools.addTool(RectangleScissorsTool); + cornerstoneTools.addTool(CircleScissorsTool); + cornerstoneTools.addTool(PaintFillTool); + cornerstoneTools.addTool(BrushTool); + + // Define a tool group + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + + // Add tools to the group + toolGroup.addTool(PanTool.toolName); + toolGroup.addTool(ZoomTool.toolName); + toolGroup.addTool(StackScrollTool.toolName); + toolGroup.addTool(RectangleScissorsTool.toolName); + toolGroup.addTool(CircleScissorsTool.toolName); + toolGroup.addTool(PaintFillTool.toolName); + toolGroup.addTool(BrushTool.toolName); + + // Set tool modes + toolGroup.setToolActive(BrushTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + + toolGroup.setToolActive(PanTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Auxiliary, + }, + { + mouseButton: MouseBindings.Primary, + modifierKey: KeyboardBindings.Ctrl, + }, + ], + }); + + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Secondary, + }, + { + mouseButton: MouseBindings.Primary, + modifierKey: KeyboardBindings.Shift, + }, + ], + }); + + toolGroup.setToolActive(StackScrollTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Wheel }], + }); + + return toolGroup; +} + +// ============================= // + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + const toolGroup = setupTools(); + + // Get Cornerstone imageIds and fetch metadata into RAM + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }); + + // Create a stack of images + const imageIdsArray = imageIds.slice(0, 10); + // Create segmentation images for the stack + const segImages = await imageLoader.createAndCacheDerivedLabelmapImages( + imageIdsArray + ); + + // Instantiate a rendering engine + renderingEngine = new RenderingEngine(renderingEngineId); + + // Create the viewport + const viewportInput = { + viewportId, + type: ViewportType.STACK, + element, + }; + + renderingEngine.setViewports([viewportInput]); + + // Set the stack of images + const viewport = renderingEngine.getViewport(viewportId); + await viewport.setStack(imageIdsArray, 0); + + // Add the viewport to the toolgroup + toolGroup.addViewport(viewportId, renderingEngineId); + + // Add segmentation + segmentation.addSegmentations([ + { + segmentationId: 'SEGMENTATION_ID', + representation: { + type: csToolsEnums.SegmentationRepresentations.Labelmap, + data: { + imageIds: segImages.map((it) => it.imageId), + }, + }, + }, + ]); + + // Add the segmentation representation to the viewport + await segmentation.addSegmentationRepresentations(viewportId, [ + { + segmentationId: 'SEGMENTATION_ID', + type: csToolsEnums.SegmentationRepresentations.Labelmap, + }, + ]); + + // Add brush size slider + addBrushSizeSlider({ + toolGroupId, + }); + + cornerstoneTools.utilities.stackContextPrefetch.enable(element); + + // Add segmentation modified callback + eventTarget.addEventListener( + Events.SEGMENTATION_DATA_MODIFIED, + segmentationModifiedCallback + ); + + // Render the image + renderingEngine.render(); +} + +run(); diff --git a/packages/labelmap-interpolation/package.json b/packages/labelmap-interpolation/package.json new file mode 100644 index 0000000000..4fa7590b95 --- /dev/null +++ b/packages/labelmap-interpolation/package.json @@ -0,0 +1,58 @@ +{ + "name": "@cornerstonejs/labelmap-interpolation", + "version": "3.0.0-beta.6", + "description": "Labelmap Interpolation utility for Cornerstone3D", + "files": [ + "dist" + ], + "module": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", + "directories": { + "build": "dist" + }, + "exports": { + ".": { + "import": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts" + } + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "jest --testTimeout 60000", + "clean": "rimraf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "build": "yarn run build:esm", + "build:esm": "tsc --project ./tsconfig.json", + "build:esm:watch": "tsc --project ./tsconfig.json --watch", + "dev": "tsc --project ./tsconfig.json --watch", + "build:all": "yarn run build:esm", + "start": "tsc --project ./tsconfig.json --watch", + "format": "prettier --write 'src/**/*.js' 'test/**/*.js'", + "lint": "eslint --fix .", + "format-check": "npx eslint ./src --quiet", + "api-check": "api-extractor --debug run ", + "build:update-api": "yarn run build:esm && api-extractor run --local", + "prepublishOnly": "yarn clean && yarn build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/cornerstonejs/cornerstone3D.git" + }, + "author": "@cornerstonejs", + "license": "MIT", + "bugs": { + "url": "https://github.com/cornerstonejs/cornerstone3D/issues" + }, + "homepage": "https://github.com/cornerstonejs/cornerstone3D/blob/main/packages/labelmap-interpolation/README.md", + "dependencies": { + "@itk-wasm/morphological-contour-interpolation": "1.1.0", + "itk-wasm": "1.0.0-b.165" + }, + "peerDependencies": { + "@cornerstonejs/core": "^3.0.0-beta.6", + "@cornerstonejs/tools": "^3.0.0-beta.6", + "@kitware/vtk.js": "^32.9.0" + } +} diff --git a/packages/labelmap-interpolation/src/index.ts b/packages/labelmap-interpolation/src/index.ts new file mode 100644 index 0000000000..d90f681bfa --- /dev/null +++ b/packages/labelmap-interpolation/src/index.ts @@ -0,0 +1,3 @@ +import interpolate from './utilities/interpolateLabelmap'; + +export { interpolate }; diff --git a/packages/labelmap-interpolation/src/registerWorker.ts b/packages/labelmap-interpolation/src/registerWorker.ts new file mode 100644 index 0000000000..f63326bdb5 --- /dev/null +++ b/packages/labelmap-interpolation/src/registerWorker.ts @@ -0,0 +1,34 @@ +import { getWebWorkerManager } from '@cornerstonejs/core'; +let registered = false; + +export function registerInterpolationWorker() { + if (registered) { + return; + } + + registered = true; + + const workerFn = () => { + // @ts-ignore + return new Worker( + // @ts-ignore + new URL('./workers/interpolationWorker.js', import.meta.url), + { + name: 'interpolation', + type: 'module', + } + ); + }; + + const workerManager = getWebWorkerManager(); + + const options = { + maxWorkerInstances: 1, + autoTerminateOnIdle: { + enabled: true, + idleTimeThreshold: 2000, + }, + }; + + workerManager.registerWorker('interpolation', workerFn, options); +} diff --git a/packages/labelmap-interpolation/src/utilities/interpolateLabelmap.ts b/packages/labelmap-interpolation/src/utilities/interpolateLabelmap.ts new file mode 100644 index 0000000000..fe12459894 --- /dev/null +++ b/packages/labelmap-interpolation/src/utilities/interpolateLabelmap.ts @@ -0,0 +1,99 @@ +import { + getWebWorkerManager, + eventTarget, + Enums, + triggerEvent, +} from '@cornerstonejs/core'; +import { + segmentation, + Enums as csToolsEnums, + utilities, +} from '@cornerstonejs/tools'; +import { registerInterpolationWorker } from '../registerWorker'; + +type MorphologicalContourInterpolationOptions = { + label?: number; + axis?: number; + noHeuristicAlignment?: boolean; + noUseDistanceTransform?: boolean; + useCustomSlicePositions?: boolean; +}; + +const { triggerSegmentationEvents } = segmentation; +const { getOrCreateSegmentationVolume } = utilities.segmentation; + +const { triggerSegmentationDataModified } = triggerSegmentationEvents; +const { WorkerTypes } = csToolsEnums; + +const workerManager = getWebWorkerManager(); + +const triggerWorkerProgress = (eventTarget, progress) => { + triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, { + progress, + type: WorkerTypes.INTERPOLATE_LABELMAP, + }); +}; + +async function interpolateLabelmap({ + segmentationId, + segmentIndex, + configuration = { preview: false }, +}: { + segmentationId: string; + segmentIndex: number; + configuration?: MorphologicalContourInterpolationOptions & { + preview: boolean; + }; +}) { + registerInterpolationWorker(); + + triggerWorkerProgress(eventTarget, 0); + + const segVolume = getOrCreateSegmentationVolume(segmentationId); + + const { + voxelManager: segmentationVoxelManager, + imageData: segmentationImageData, + } = segVolume; + + const segmentationInfo = { + scalarData: segmentationVoxelManager.getCompleteScalarDataArray(), + dimensions: segmentationImageData.getDimensions(), + spacing: segmentationImageData.getSpacing(), + origin: segmentationImageData.getOrigin(), + direction: segmentationImageData.getDirection(), + }; + + try { + const { data: outputScalarData } = await workerManager.executeTask( + 'interpolation', + 'interpolateLabelmap', + { + segmentationInfo, + configuration: { + ...configuration, + label: segmentIndex, + }, + } + ); + + // Update the segmentation with the modified data + segmentationVoxelManager.setCompleteScalarDataArray(outputScalarData); + + triggerSegmentationDataModified( + segmentationId, + segmentationVoxelManager.getArrayOfModifiedSlices(), + segmentIndex + ); + + triggerWorkerProgress(eventTarget, 100); + } catch (error) { + console.warn( + 'Warning: Failed to perform morphological contour interpolation', + error + ); + triggerWorkerProgress(eventTarget, 100); + } +} + +export default interpolateLabelmap; diff --git a/packages/labelmap-interpolation/src/workers/interpolationWorker.js b/packages/labelmap-interpolation/src/workers/interpolationWorker.js new file mode 100644 index 0000000000..fba8d144d7 --- /dev/null +++ b/packages/labelmap-interpolation/src/workers/interpolationWorker.js @@ -0,0 +1,174 @@ +import { expose } from 'comlink'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; +import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; + +/** + * Dynamically imports ITK WASM modules needed for labelmap interpolation + * @param moduleId - The module ID to import ('itk-wasm' or '@itk-wasm/morphological-contour-interpolation') + * @returns Promise that resolves to the imported module + */ +async function peerImport(moduleId) { + try { + switch (moduleId) { + case 'itk-wasm': + return import('itk-wasm'); + case '@itk-wasm/morphological-contour-interpolation': + return import('@itk-wasm/morphological-contour-interpolation'); + default: + throw new Error(`Unknown module ID: ${moduleId}`); + } + } catch (error) { + console.warn(`Error importing ${moduleId}:`, error); + return null; + } +} + +const computeWorker = { + getITKImage: async (args) => { + const { imageData, options } = args; + + const { imageName, scalarData } = options; + + let Image, ImageType, IntTypes, FloatTypes, PixelTypes; + + try { + const itkModule = await peerImport('itk-wasm'); + if (!itkModule) { + throw new Error('Module not found'); + } + ({ Image, ImageType, IntTypes, FloatTypes, PixelTypes } = itkModule); + } catch (error) { + console.warn( + "Warning: 'itk-wasm' module not found. Please install it separately." + ); + return null; + } + + const dataTypesMap = { + Int8: IntTypes.Int8, + UInt8: IntTypes.UInt8, + Int16: IntTypes.Int16, + UInt16: IntTypes.UInt16, + Int32: IntTypes.Int32, + UInt32: IntTypes.UInt32, + Int64: IntTypes.Int64, + UInt64: IntTypes.UInt64, + Float32: FloatTypes.Float32, + Float64: FloatTypes.Float64, + }; + + const { numberOfComponents } = imageData.get('numberOfComponents'); + + const dimensions = imageData.getDimensions(); + const origin = imageData.getOrigin(); + const spacing = imageData.getSpacing(); + const directionArray = imageData.getDirection(); + const direction = new Float64Array(directionArray); + const dataType = scalarData.constructor.name + .replace(/^Ui/, 'UI') + .replace(/Array$/, ''); + const metadata = undefined; + + const imageType = new ImageType( + dimensions.length, + dataTypesMap[dataType], + PixelTypes.Scalar, + numberOfComponents + ); + + const image = new Image(imageType); + image.name = imageName; + image.origin = origin; + image.spacing = spacing; + image.direction = direction; + image.size = dimensions; + image.metadata = metadata; + image.data = scalarData; + + return image; + }, + interpolateLabelmap: async (args) => { + const { segmentationInfo, configuration } = args; + const { scalarData, dimensions, spacing, origin, direction } = + segmentationInfo; + + let itkModule; + try { + itkModule = await peerImport( + '@itk-wasm/morphological-contour-interpolation' + ); + if (!itkModule) { + throw new Error('Module not found'); + } + } catch (error) { + console.warn( + "Warning: '@itk-wasm/morphological-contour-interpolation' module not found. Please install it separately." + ); + return { data: scalarData }; + } + + const imageData = vtkImageData.newInstance(); + imageData.setDimensions(dimensions); + imageData.setOrigin(origin); + imageData.setDirection(direction || [1, 0, 0, 0, 1, 0, 0, 0, 1]); + imageData.setSpacing(spacing); + + const scalarArray = vtkDataArray.newInstance({ + name: 'Pixels', + numberOfComponents: 1, + values: scalarData, + }); + + imageData.getPointData().setScalars(scalarArray); + imageData.modified(); + + try { + const inputImage = await computeWorker.getITKImage({ + imageData, + options: { + imageName: 'interpolation', + scalarData: scalarData, + }, + }); + + if (!inputImage) { + throw new Error('Failed to get ITK image'); + } + + const { outputImage } = await itkModule.morphologicalContourInterpolation( + inputImage, + { + ...configuration, + // since we already have a web worker, we don't need to create another one + webWorker: false, + } + ); + + const outputScalarData = outputImage.data; + const modifiedScalarData = new Uint16Array(scalarData.length); + + // Copy the original data first + modifiedScalarData.set(scalarData); + + // Only update values that are different + for (let i = 0; i < outputScalarData.length; i++) { + const newValue = outputScalarData[i]; + const originalValue = scalarData[i]; + + if (newValue !== originalValue) { + modifiedScalarData[i] = newValue; + } + } + + return { data: modifiedScalarData }; + } catch (error) { + console.error(error); + console.warn( + 'Warning: Failed to perform morphological contour interpolation' + ); + return { data: scalarData }; + } + }, +}; + +expose(computeWorker); diff --git a/packages/labelmap-interpolation/tsconfig.json b/packages/labelmap-interpolation/tsconfig.json new file mode 100644 index 0000000000..bc915f1e65 --- /dev/null +++ b/packages/labelmap-interpolation/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist/esm", + "rootDir": "./src" + }, + "include": ["./src/**/*"] +} diff --git a/packages/nifti-volume-loader/CHANGELOG.md b/packages/nifti-volume-loader/CHANGELOG.md index 1a6f8599a7..d3254e72a5 100644 --- a/packages/nifti-volume-loader/CHANGELOG.md +++ b/packages/nifti-volume-loader/CHANGELOG.md @@ -3,14 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [2.19.16](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.15...v2.19.16) (2025-02-26) +# [3.0.0-beta.6](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2025-02-27) **Note:** Version bump only for package @cornerstonejs/nifti-volume-loader -## [2.19.15](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.14...v2.19.15) (2025-02-26) +# [3.0.0-beta.5](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2025-02-25) **Note:** Version bump only for package @cornerstonejs/nifti-volume-loader +# [3.0.0-beta.4](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +# [3.0.0-beta.3](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +# [3.0.0-beta.2](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.14...v3.0.0-beta.2) (2025-02-24) + +### Bug Fixes + +- publish beta for 3.0 ([8bf65df](https://github.com/cornerstonejs/cornerstone3D/commit/8bf65df9bec5f52459de1c49c4834b316f680f1b)) + ## [2.19.14](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.13...v2.19.14) (2025-02-24) **Note:** Version bump only for package @cornerstonejs/nifti-volume-loader diff --git a/packages/nifti-volume-loader/package.json b/packages/nifti-volume-loader/package.json index f436fc81f4..c47c59b7c5 100644 --- a/packages/nifti-volume-loader/package.json +++ b/packages/nifti-volume-loader/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/nifti-volume-loader", - "version": "2.19.16", + "version": "3.0.0-beta.6", "description": "Nifti Image Loader for Cornerstone3D", "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", @@ -61,7 +61,7 @@ "nifti-reader-js": "^0.6.8" }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.16" + "@cornerstonejs/core": "^3.0.0-beta.6" }, "contributors": [ { diff --git a/packages/polymorphic-segmentation/CHANGELOG.md b/packages/polymorphic-segmentation/CHANGELOG.md new file mode 100644 index 0000000000..f92e20c60b --- /dev/null +++ b/packages/polymorphic-segmentation/CHANGELOG.md @@ -0,0 +1,22 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.0.0-beta.6](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2025-02-27) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +# [3.0.0-beta.5](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +# [3.0.0-beta.4](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +# [3.0.0-beta.3](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +# Change Log diff --git a/packages/polymorphic-segmentation/README.md b/packages/polymorphic-segmentation/README.md new file mode 100644 index 0000000000..bdd71bfc3a --- /dev/null +++ b/packages/polymorphic-segmentation/README.md @@ -0,0 +1,40 @@ +# Cornerstone Segmentation Polymorphic Segmentation + +A powerful and flexible segmentation addon for Cornerstone3D that provides polymorphic segmentation capabilities. + +## Overview + +The Polymorphic Segmentation package extends Cornerstone3D's segmentation capabilities with advanced features for creating, manipulating, and visualizing segmentations across different modalities and use cases. + +## Installation + +```bash +npm install @cornerstonejs/polymorphic-segmentation +``` + +## Initialization + +To use the Polymorphic Segmentation package with Cornerstone Tools, you need to initialize it as an addon during the Cornerstone Tools initialization process. + +### Basic Initialization + +```js +import { init } from '@cornerstonejs/tools'; +import * as polySeg from '@cornerstonejs/polymorphic-segmentation'; + +// Initialize Cornerstone Tools with the Polymorphic Segmentation addon +await init({ + addons: { + polySeg, + }, +}); +``` + +:::note +If you don't initialize polySeg as an addon, you will not be able to use the polymorphic segmentation features. But the +rest of the Cornerstone3D components will work as expected. +::: + +## License + +MIT diff --git a/packages/polymorphic-segmentation/api-extractor.json b/packages/polymorphic-segmentation/api-extractor.json new file mode 100644 index 0000000000..4ddb5f44d3 --- /dev/null +++ b/packages/polymorphic-segmentation/api-extractor.json @@ -0,0 +1,9 @@ +{ + "extends": "../../api-extractor.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/esm/index.d.ts", + "apiReport": { + "reportFileName": ".api.md", + "reportFolder": "../../common/reviews/api" + } +} diff --git a/packages/tools/examples/PolySegWasmContourToStackLabelmap/index.ts b/packages/polymorphic-segmentation/examples/PolySegWasmContourToStackLabelmap/index.ts similarity index 100% rename from packages/tools/examples/PolySegWasmContourToStackLabelmap/index.ts rename to packages/polymorphic-segmentation/examples/PolySegWasmContourToStackLabelmap/index.ts diff --git a/packages/tools/examples/PolySegWasmContourToSurface/index.ts b/packages/polymorphic-segmentation/examples/PolySegWasmContourToSurface/index.ts similarity index 100% rename from packages/tools/examples/PolySegWasmContourToSurface/index.ts rename to packages/polymorphic-segmentation/examples/PolySegWasmContourToSurface/index.ts diff --git a/packages/tools/examples/PolySegWasmContourToVolumeLabelmap/index.ts b/packages/polymorphic-segmentation/examples/PolySegWasmContourToVolumeLabelmap/index.ts similarity index 100% rename from packages/tools/examples/PolySegWasmContourToVolumeLabelmap/index.ts rename to packages/polymorphic-segmentation/examples/PolySegWasmContourToVolumeLabelmap/index.ts diff --git a/packages/tools/examples/PolySegWasmStackLabelmapToSurface/index.ts b/packages/polymorphic-segmentation/examples/PolySegWasmStackLabelmapToSurface/index.ts similarity index 100% rename from packages/tools/examples/PolySegWasmStackLabelmapToSurface/index.ts rename to packages/polymorphic-segmentation/examples/PolySegWasmStackLabelmapToSurface/index.ts diff --git a/packages/tools/examples/PolySegWasmSurfaceToContour/index.ts b/packages/polymorphic-segmentation/examples/PolySegWasmSurfaceToContour/index.ts similarity index 100% rename from packages/tools/examples/PolySegWasmSurfaceToContour/index.ts rename to packages/polymorphic-segmentation/examples/PolySegWasmSurfaceToContour/index.ts diff --git a/packages/tools/examples/PolySegWasmSurfaceToStackLabelmap/index.ts b/packages/polymorphic-segmentation/examples/PolySegWasmSurfaceToStackLabelmap/index.ts similarity index 98% rename from packages/tools/examples/PolySegWasmSurfaceToStackLabelmap/index.ts rename to packages/polymorphic-segmentation/examples/PolySegWasmSurfaceToStackLabelmap/index.ts index 844dae4752..ccd731f73c 100644 --- a/packages/tools/examples/PolySegWasmSurfaceToStackLabelmap/index.ts +++ b/packages/polymorphic-segmentation/examples/PolySegWasmSurfaceToStackLabelmap/index.ts @@ -178,7 +178,7 @@ async function run() { const surfaces = await downloadSurfacesData(); - const geometriesInfo = createAndCacheGeometriesFromSurfaces(surfaces); + const geometriesInfo = await createAndCacheGeometriesFromSurfaces(surfaces); // Add the segmentations to state segmentation.addSegmentations([ diff --git a/packages/tools/examples/PolySegWasmSurfaceToVolumeLabelmap/index.ts b/packages/polymorphic-segmentation/examples/PolySegWasmSurfaceToVolumeLabelmap/index.ts similarity index 100% rename from packages/tools/examples/PolySegWasmSurfaceToVolumeLabelmap/index.ts rename to packages/polymorphic-segmentation/examples/PolySegWasmSurfaceToVolumeLabelmap/index.ts diff --git a/packages/tools/examples/PolySegWasmVolumeLabelmapToContour/index.ts b/packages/polymorphic-segmentation/examples/PolySegWasmVolumeLabelmapToContour/index.ts similarity index 100% rename from packages/tools/examples/PolySegWasmVolumeLabelmapToContour/index.ts rename to packages/polymorphic-segmentation/examples/PolySegWasmVolumeLabelmapToContour/index.ts diff --git a/packages/tools/examples/PolySegWasmVolumeLabelmapToSurface/index.ts b/packages/polymorphic-segmentation/examples/PolySegWasmVolumeLabelmapToSurface/index.ts similarity index 100% rename from packages/tools/examples/PolySegWasmVolumeLabelmapToSurface/index.ts rename to packages/polymorphic-segmentation/examples/PolySegWasmVolumeLabelmapToSurface/index.ts diff --git a/packages/polymorphic-segmentation/package.json b/packages/polymorphic-segmentation/package.json new file mode 100644 index 0000000000..686f750cde --- /dev/null +++ b/packages/polymorphic-segmentation/package.json @@ -0,0 +1,56 @@ +{ + "name": "@cornerstonejs/polymorphic-segmentation", + "version": "3.0.0-beta.6", + "description": "Polymorphic Segmentation utility for Cornerstone3D", + "files": [ + "dist" + ], + "module": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", + "directories": { + "build": "dist" + }, + "exports": { + ".": { + "import": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts" + } + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "jest --testTimeout 60000", + "clean": "rimraf dist", + "build": "yarn run build:esm", + "build:esm": "tsc --project ./tsconfig.json", + "build:esm:watch": "tsc --project ./tsconfig.json --watch", + "dev": "tsc --project ./tsconfig.json --watch", + "build:all": "yarn run build:esm", + "start": "tsc --project ./tsconfig.json --watch", + "format": "prettier --write 'src/**/*.js' 'test/**/*.js'", + "lint": "eslint --fix .", + "format-check": "npx eslint ./src --quiet", + "api-check": "api-extractor --debug run ", + "build:update-api": "yarn run build:esm && api-extractor run --local", + "prepublishOnly": "yarn clean && yarn build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/cornerstonejs/cornerstone3D.git" + }, + "author": "@cornerstonejs", + "license": "MIT", + "bugs": { + "url": "https://github.com/cornerstonejs/cornerstone3D/issues" + }, + "homepage": "https://github.com/cornerstonejs/cornerstone3D/blob/main/packages/polymorphic-segmentation/README.md", + "dependencies": { + "@icr/polyseg-wasm": "0.4.0" + }, + "peerDependencies": { + "@cornerstonejs/core": "^3.0.0-beta.6", + "@cornerstonejs/tools": "^3.0.0-beta.6", + "@kitware/vtk.js": "^32.9.0" + } +} diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Contour/contourComputationStrategies.ts b/packages/polymorphic-segmentation/src/Contour/contourComputationStrategies.ts similarity index 90% rename from packages/tools/src/stateManagement/segmentation/polySeg/Contour/contourComputationStrategies.ts rename to packages/polymorphic-segmentation/src/Contour/contourComputationStrategies.ts index 390fa04fb9..24357952c7 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Contour/contourComputationStrategies.ts +++ b/packages/polymorphic-segmentation/src/Contour/contourComputationStrategies.ts @@ -1,15 +1,18 @@ -import { cache } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; -import { getUniqueSegmentIndices } from '../../../../utilities/segmentation/getUniqueSegmentIndices'; -import type { PolySegConversionOptions } from '../../../../types'; -import { computeSurfaceFromLabelmapSegmentation } from '../Surface/surfaceComputationStrategies'; -import type { SurfaceClipResult } from '../../helpers/clipAndCacheSurfacesForViewport'; -import { clipAndCacheSurfacesForViewport } from '../../helpers/clipAndCacheSurfacesForViewport'; +import { cache } from '@cornerstonejs/core'; +import { Enums, segmentation, utilities } from '@cornerstonejs/tools'; import { extractContourData } from './utils/extractContourData'; + +import type { PolySegConversionOptions } from '../types'; +import { computeSurfaceFromLabelmapSegmentation } from '../Surface/surfaceComputationStrategies'; +import type { SurfaceClipResult } from '../utilities/clipAndCacheSurfacesForViewport'; +import { clipAndCacheSurfacesForViewport } from '../utilities/clipAndCacheSurfacesForViewport'; import { createAndAddContourSegmentationsFromClippedSurfaces } from './utils/createAndAddContourSegmentationsFromClippedSurfaces'; -import { getSegmentation } from '../../getSegmentation'; -import { segmentationStyle } from '../../SegmentationStyle'; -import { SegmentationRepresentations } from '../../../../enums'; + +const { getUniqueSegmentIndices } = utilities.segmentation; +const { getSegmentation } = segmentation.state; +const { segmentationStyle } = segmentation; +const { SegmentationRepresentations } = Enums; // the map between segment index and the intersection points and lines export type RawContourData = Map; diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Contour/utils/createAndAddContourSegmentationsFromClippedSurfaces.ts b/packages/polymorphic-segmentation/src/Contour/utils/createAndAddContourSegmentationsFromClippedSurfaces.ts similarity index 93% rename from packages/tools/src/stateManagement/segmentation/polySeg/Contour/utils/createAndAddContourSegmentationsFromClippedSurfaces.ts rename to packages/polymorphic-segmentation/src/Contour/utils/createAndAddContourSegmentationsFromClippedSurfaces.ts index 3c4f626bb1..ee082d427b 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Contour/utils/createAndAddContourSegmentationsFromClippedSurfaces.ts +++ b/packages/polymorphic-segmentation/src/Contour/utils/createAndAddContourSegmentationsFromClippedSurfaces.ts @@ -1,8 +1,11 @@ -import PlanarFreehandContourSegmentationTool from '../../../../../tools/annotation/PlanarFreehandContourSegmentationTool'; -import { addAnnotation } from '../../../../annotation/annotationState'; -import type { RawContourData } from '../contourComputationStrategies'; import { utilities, type Types } from '@cornerstonejs/core'; +import { + PlanarFreehandContourSegmentationTool, + annotation, +} from '@cornerstonejs/tools'; +import type { RawContourData } from '../contourComputationStrategies'; +const { addAnnotation } = annotation.state; /** * Creates and adds contour segmentations from a clipped surface. * diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Contour/utils/extractContourData.ts b/packages/polymorphic-segmentation/src/Contour/utils/extractContourData.ts similarity index 90% rename from packages/tools/src/stateManagement/segmentation/polySeg/Contour/utils/extractContourData.ts rename to packages/polymorphic-segmentation/src/Contour/utils/extractContourData.ts index 05a2b0e8d4..7d41a49574 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Contour/utils/extractContourData.ts +++ b/packages/polymorphic-segmentation/src/Contour/utils/extractContourData.ts @@ -1,4 +1,4 @@ -import type { PolyDataClipCacheType } from '../../../helpers/clipAndCacheSurfacesForViewport'; +import type { PolyDataClipCacheType } from '../../utilities/clipAndCacheSurfacesForViewport'; import type { RawContourData } from '../contourComputationStrategies'; /** diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Contour/utils/updateContoursOnCameraModified.ts b/packages/polymorphic-segmentation/src/Contour/utils/updateContoursOnCameraModified.ts similarity index 94% rename from packages/tools/src/stateManagement/segmentation/polySeg/Contour/utils/updateContoursOnCameraModified.ts rename to packages/polymorphic-segmentation/src/Contour/utils/updateContoursOnCameraModified.ts index 20332071ae..cdbf1cd136 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Contour/utils/updateContoursOnCameraModified.ts +++ b/packages/polymorphic-segmentation/src/Contour/utils/updateContoursOnCameraModified.ts @@ -1,6 +1,6 @@ import { utilities, type Types, Enums } from '@cornerstonejs/core'; import { extractContourData } from './extractContourData'; -import { clipAndCacheSurfacesForViewport } from '../../../helpers/clipAndCacheSurfacesForViewport'; +import { clipAndCacheSurfacesForViewport } from '../../utilities'; import { createAndAddContourSegmentationsFromClippedSurfaces } from './createAndAddContourSegmentationsFromClippedSurfaces'; const currentViewportNormal = new Map(); diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/convertContourToLabelmap.ts b/packages/polymorphic-segmentation/src/Labelmap/convertContourToLabelmap.ts similarity index 91% rename from packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/convertContourToLabelmap.ts rename to packages/polymorphic-segmentation/src/Labelmap/convertContourToLabelmap.ts index a678389f36..23311897ce 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/convertContourToLabelmap.ts +++ b/packages/polymorphic-segmentation/src/Labelmap/convertContourToLabelmap.ts @@ -11,14 +11,12 @@ import { triggerEvent, eventTarget, } from '@cornerstonejs/core'; -import type { - Annotation, - ContourAnnotation, - ContourSegmentationData, - PolySegConversionOptions, -} from '../../../../types'; -import { getAnnotation } from '../../../annotation/annotationState'; -import { WorkerTypes } from '../../../../enums'; +import type { Types as ToolsTypes } from '@cornerstonejs/tools'; +import * as cornerstoneTools from '@cornerstonejs/tools'; +import type { PolySegConversionOptions } from '../types'; + +const { WorkerTypes } = cornerstoneTools.Enums; +const { getAnnotation } = cornerstoneTools.annotation.state; const workerManager = getWebWorkerManager(); @@ -30,7 +28,7 @@ const triggerWorkerProgress = (eventTarget, progress) => { }; export async function convertContourToVolumeLabelmap( - contourRepresentationData: ContourSegmentationData, + contourRepresentationData: ToolsTypes.ContourSegmentationData, options: PolySegConversionOptions = {} ) { const viewport = options.viewport as Types.IVolumeViewport; @@ -94,7 +92,7 @@ export async function convertContourToVolumeLabelmap( } export async function convertContourToStackLabelmap( - contourRepresentationData: ContourSegmentationData, + contourRepresentationData: ToolsTypes.ContourSegmentationData, options: PolySegConversionOptions = {} ) { if (!options.viewport) { @@ -236,7 +234,7 @@ export async function convertContourToStackLabelmap( } function _getAnnotationMapFromSegmentation( - contourRepresentationData: ContourSegmentationData, + contourRepresentationData: ToolsTypes.ContourSegmentationData, options: PolySegConversionOptions = {} ) { const annotationMap = contourRepresentationData.annotationUIDsMap; @@ -256,11 +254,12 @@ function _getAnnotationMapFromSegmentation( let uids = Array.from(annotationUIDsInSegment); uids = uids.filter( - (uid) => !(getAnnotation(uid) as Annotation).parentAnnotationUID + (uid) => + !(getAnnotation(uid) as ToolsTypes.Annotation).parentAnnotationUID ); const annotations = uids.map((uid) => { - const annotation = getAnnotation(uid) as ContourAnnotation; + const annotation = getAnnotation(uid) as ToolsTypes.ContourAnnotation; const hasChildAnnotations = annotation.childAnnotationUIDs?.length; return { @@ -271,7 +270,7 @@ function _getAnnotationMapFromSegmentation( annotation.childAnnotationUIDs.map((childUID) => { const childAnnotation = getAnnotation( childUID - ) as ContourAnnotation; + ) as ToolsTypes.ContourAnnotation; return childAnnotation.data.contour.polyline; }), }; diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/convertSurfaceToLabelmap.ts b/packages/polymorphic-segmentation/src/Labelmap/convertSurfaceToLabelmap.ts similarity index 88% rename from packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/convertSurfaceToLabelmap.ts rename to packages/polymorphic-segmentation/src/Labelmap/convertSurfaceToLabelmap.ts index 2ed19c6f97..08967fab8f 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/convertSurfaceToLabelmap.ts +++ b/packages/polymorphic-segmentation/src/Labelmap/convertSurfaceToLabelmap.ts @@ -6,8 +6,10 @@ import { getWebWorkerManager, triggerEvent, } from '@cornerstonejs/core'; -import type { SurfaceSegmentationData } from '../../../../types/SurfaceTypes'; -import { WorkerTypes } from '../../../../enums'; +import * as cornerstoneTools from '@cornerstonejs/tools'; +import type { Types as ToolsTypes } from '@cornerstonejs/tools'; + +const { WorkerTypes } = cornerstoneTools.Enums; const workerManager = getWebWorkerManager(); @@ -19,7 +21,7 @@ const triggerWorkerProgress = (eventTarget, progress) => { }; export async function convertSurfaceToVolumeLabelmap( - surfaceRepresentationData: SurfaceSegmentationData, + surfaceRepresentationData: ToolsTypes.SurfaceSegmentationData, segmentationVolume: Types.IImageVolume ) { const { geometryIds } = surfaceRepresentationData; diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/labelmapComputationStrategies.ts b/packages/polymorphic-segmentation/src/Labelmap/labelmapComputationStrategies.ts similarity index 85% rename from packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/labelmapComputationStrategies.ts rename to packages/polymorphic-segmentation/src/Labelmap/labelmapComputationStrategies.ts index a82fdc884a..d5f0c7be69 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/labelmapComputationStrategies.ts +++ b/packages/polymorphic-segmentation/src/Labelmap/labelmapComputationStrategies.ts @@ -1,22 +1,22 @@ -import { VolumeViewport, volumeLoader, imageLoader } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; -import { getUniqueSegmentIndices } from '../../../../utilities/segmentation/getUniqueSegmentIndices'; -import { getSegmentation } from '../../getSegmentation'; -import type { - LabelmapSegmentationDataStack, - LabelmapSegmentationDataVolume, -} from '../../../../types/LabelmapTypes'; +import { volumeLoader, imageLoader, VolumeViewport } from '@cornerstonejs/core'; +import { utilities } from '@cornerstonejs/tools'; +import * as cornerstoneTools from '@cornerstonejs/tools'; +import type { Types as ToolsTypes } from '@cornerstonejs/tools'; import { convertContourToStackLabelmap, convertContourToVolumeLabelmap, } from './convertContourToLabelmap'; import { convertSurfaceToVolumeLabelmap } from './convertSurfaceToLabelmap'; -import type { PolySegConversionOptions } from '../../../../types'; -import { computeStackLabelmapFromVolume } from '../../helpers/computeStackLabelmapFromVolume'; +import type { PolySegConversionOptions } from '../types'; + +const { computeStackLabelmapFromVolume, getUniqueSegmentIndices } = + utilities.segmentation; +const { getSegmentation } = cornerstoneTools.segmentation.state; export type RawLabelmapData = - | LabelmapSegmentationDataVolume - | LabelmapSegmentationDataStack; + | ToolsTypes.LabelmapSegmentationDataVolume + | ToolsTypes.LabelmapSegmentationDataStack; export async function computeLabelmapData( segmentationId: string, @@ -65,7 +65,10 @@ export async function computeLabelmapData( async function computeLabelmapFromContourSegmentation( segmentationId, options: PolySegConversionOptions = {} -): Promise { +): Promise< + | ToolsTypes.LabelmapSegmentationDataVolume + | ToolsTypes.LabelmapSegmentationDataStack +> { const isVolume = options.viewport ? options.viewport instanceof VolumeViewport : true; @@ -103,7 +106,10 @@ async function computeLabelmapFromContourSegmentation( async function computeLabelmapFromSurfaceSegmentation( segmentationId, options: PolySegConversionOptions = {} -): Promise { +): Promise< + | ToolsTypes.LabelmapSegmentationDataVolume + | ToolsTypes.LabelmapSegmentationDataStack +> { const { viewport } = options; const isVolume = viewport ? viewport instanceof VolumeViewport : true; @@ -161,7 +167,7 @@ async function computeLabelmapFromSurfaceSegmentation( // we need to convert the volume labelmap to a stack labelmap const stackData = (await computeStackLabelmapFromVolume({ volumeId: segmentationVolume.volumeId, - })) as LabelmapSegmentationDataStack; + })) as ToolsTypes.LabelmapSegmentationDataStack; return stackData; } diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Surface/convertContourToSurface.ts b/packages/polymorphic-segmentation/src/Surface/convertContourToSurface.ts similarity index 80% rename from packages/tools/src/stateManagement/segmentation/polySeg/Surface/convertContourToSurface.ts rename to packages/polymorphic-segmentation/src/Surface/convertContourToSurface.ts index 400de46526..6b2588035e 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Surface/convertContourToSurface.ts +++ b/packages/polymorphic-segmentation/src/Surface/convertContourToSurface.ts @@ -5,12 +5,11 @@ import { triggerEvent, getWebWorkerManager, } from '@cornerstonejs/core'; -import type { - ContourSegmentationAnnotation, - ContourSegmentationData, -} from '../../../../types'; -import { getAnnotation } from '../../../annotation/annotationState'; -import { WorkerTypes } from '../../../../enums'; +import type { Types as ToolsTypes } from '@cornerstonejs/tools'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + +const { WorkerTypes } = cornerstoneTools.Enums; +const { getAnnotation } = cornerstoneTools.annotation.state; const workerManager = getWebWorkerManager(); @@ -30,7 +29,7 @@ const triggerWorkerProgress = (eventTarget, progress, id) => { * @returns A promise that resolves to the surface data. */ export async function convertContourToSurface( - contourRepresentationData: ContourSegmentationData, + contourRepresentationData: ToolsTypes.ContourSegmentationData, segmentIndex: number ): Promise { const { annotationUIDsMap } = contourRepresentationData; @@ -42,8 +41,9 @@ export async function convertContourToSurface( for (const annotationUID of annotationUIDs) { const annotation = getAnnotation(annotationUID); - const { polyline } = (annotation as ContourSegmentationAnnotation).data - .contour; + const { polyline } = ( + annotation as ToolsTypes.ContourSegmentationAnnotation + ).data.contour; numPointsArray.push(polyline.length); polyline.forEach((polyline) => polylines.push(...polyline)); } diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Surface/convertLabelmapToSurface.ts b/packages/polymorphic-segmentation/src/Surface/convertLabelmapToSurface.ts similarity index 73% rename from packages/tools/src/stateManagement/segmentation/polySeg/Surface/convertLabelmapToSurface.ts rename to packages/polymorphic-segmentation/src/Surface/convertLabelmapToSurface.ts index 855ec9ad8a..c8bfb62983 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Surface/convertLabelmapToSurface.ts +++ b/packages/polymorphic-segmentation/src/Surface/convertLabelmapToSurface.ts @@ -6,13 +6,13 @@ import { triggerEvent, Enums, } from '@cornerstonejs/core'; -import type { - LabelmapSegmentationData, - LabelmapSegmentationDataStack, - LabelmapSegmentationDataVolume, -} from '../../../../types/LabelmapTypes'; -import { computeVolumeLabelmapFromStack } from '../../helpers/computeVolumeLabelmapFromStack'; -import { WorkerTypes } from '../../../../enums'; + +import * as cornerstoneTools from '@cornerstonejs/tools'; +import type { Types as ToolsTypes } from '@cornerstonejs/tools'; + +const { WorkerTypes } = cornerstoneTools.Enums; +const { computeVolumeLabelmapFromStack } = + cornerstoneTools.utilities.segmentation; const workerManager = getWebWorkerManager(); @@ -33,17 +33,21 @@ const triggerWorkerProgress = (eventTarget, progress, id) => { * @returns A promise that resolves to the surface data. */ export async function convertLabelmapToSurface( - labelmapRepresentationData: LabelmapSegmentationData, + labelmapRepresentationData: ToolsTypes.LabelmapSegmentationData, segmentIndex: number ): Promise { let volumeId; - if ((labelmapRepresentationData as LabelmapSegmentationDataVolume).volumeId) { - volumeId = (labelmapRepresentationData as LabelmapSegmentationDataVolume) - .volumeId; + if ( + (labelmapRepresentationData as ToolsTypes.LabelmapSegmentationDataVolume) + .volumeId + ) { + volumeId = ( + labelmapRepresentationData as ToolsTypes.LabelmapSegmentationDataVolume + ).volumeId; } else { const { imageIds } = - labelmapRepresentationData as LabelmapSegmentationDataStack; + labelmapRepresentationData as ToolsTypes.LabelmapSegmentationDataStack; ({ volumeId } = await computeVolumeLabelmapFromStack({ imageIds, diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Surface/createAndCacheSurfacesFromRaw.ts b/packages/polymorphic-segmentation/src/Surface/createAndCacheSurfacesFromRaw.ts similarity index 88% rename from packages/tools/src/stateManagement/segmentation/polySeg/Surface/createAndCacheSurfacesFromRaw.ts rename to packages/polymorphic-segmentation/src/Surface/createAndCacheSurfacesFromRaw.ts index 8d6b91c5a9..e13ce49d6a 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Surface/createAndCacheSurfacesFromRaw.ts +++ b/packages/polymorphic-segmentation/src/Surface/createAndCacheSurfacesFromRaw.ts @@ -1,9 +1,11 @@ import type { Types } from '@cornerstonejs/core'; import { Enums, geometryLoader } from '@cornerstonejs/core'; -import { getSegmentIndexColor } from '../../config/segmentationColor'; import type { RawSurfacesData } from './surfaceComputationStrategies'; -import type { PolySegConversionOptions } from '../../../../types'; -import { getSegmentation } from '../../getSegmentation'; +import type { PolySegConversionOptions } from '../types'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + +const { getSegmentation } = cornerstoneTools.segmentation.state; +const { getSegmentIndexColor } = cornerstoneTools.segmentation.config.color; /** * Creates and caches surfaces from raw surface data. diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Surface/surfaceComputationStrategies.ts b/packages/polymorphic-segmentation/src/Surface/surfaceComputationStrategies.ts similarity index 86% rename from packages/tools/src/stateManagement/segmentation/polySeg/Surface/surfaceComputationStrategies.ts rename to packages/polymorphic-segmentation/src/Surface/surfaceComputationStrategies.ts index b1f2d8d380..8e2a20a8ad 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Surface/surfaceComputationStrategies.ts +++ b/packages/polymorphic-segmentation/src/Surface/surfaceComputationStrategies.ts @@ -1,18 +1,14 @@ import type { Types } from '@cornerstonejs/core'; -import type { - ContourSegmentationData, - PolySegConversionOptions, -} from '../../../../types'; -import { getUniqueSegmentIndices } from '../../../../utilities/segmentation/getUniqueSegmentIndices'; -import { getSegmentation } from '../../getSegmentation'; +import type { PolySegConversionOptions } from '../types'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + import { convertContourToSurface } from './convertContourToSurface'; import { createAndCacheSurfacesFromRaw } from './createAndCacheSurfacesFromRaw'; -import type { - LabelmapSegmentationData, - LabelmapSegmentationDataStack, - LabelmapSegmentationDataVolume, -} from '../../../../types/LabelmapTypes'; import { convertLabelmapToSurface } from './convertLabelmapToSurface'; +import type { Types as ToolsTypes } from '@cornerstonejs/tools'; + +const { getUniqueSegmentIndices } = cornerstoneTools.utilities.segmentation; +const { getSegmentation } = cornerstoneTools.segmentation.state; export type RawSurfacesData = { segmentIndex: number; @@ -47,7 +43,9 @@ export async function computeSurfaceData( ...options, } ); - } else if (representationData.Labelmap as LabelmapSegmentationData) { + } else if ( + representationData.Labelmap as ToolsTypes.LabelmapSegmentationData + ) { // convert volume labelmap to surface rawSurfacesData = await computeSurfaceFromLabelmapSegmentation( segmentation.segmentationId, @@ -97,8 +95,8 @@ async function computeSurfaceFromLabelmapSegmentation( const promises = segmentIndices.map((index) => { const surface = convertLabelmapToSurface( labelmapRepresentationData as - | LabelmapSegmentationDataVolume - | LabelmapSegmentationDataStack, + | ToolsTypes.LabelmapSegmentationDataVolume + | ToolsTypes.LabelmapSegmentationDataStack, index ); @@ -143,7 +141,7 @@ async function computeSurfaceFromContourSegmentation( const promises = segmentIndices.map(async (index) => { const surface = await convertContourToSurface( - contourRepresentationData as ContourSegmentationData, + contourRepresentationData as ToolsTypes.ContourSegmentationData, index ); diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Surface/updateSurfaceData.ts b/packages/polymorphic-segmentation/src/Surface/updateSurfaceData.ts similarity index 84% rename from packages/tools/src/stateManagement/segmentation/polySeg/Surface/updateSurfaceData.ts rename to packages/polymorphic-segmentation/src/Surface/updateSurfaceData.ts index 86b75f03cd..5648331df3 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Surface/updateSurfaceData.ts +++ b/packages/polymorphic-segmentation/src/Surface/updateSurfaceData.ts @@ -1,14 +1,25 @@ import type { Types } from '@cornerstonejs/core'; import { cache } from '@cornerstonejs/core'; -import { getUniqueSegmentIndices } from '../../../../utilities/segmentation/getUniqueSegmentIndices'; -import { getViewportIdsWithSegmentation } from '../../getViewportIdsWithSegmentation'; -import { getSegmentation } from '../../getSegmentation'; -import { triggerSegmentationModified } from '../../triggerSegmentationEvents'; -import { getSegmentationRepresentation } from '../../getSegmentationRepresentation'; -import { SegmentationRepresentations } from '../../../../enums'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + import { computeSurfaceFromLabelmapSegmentation } from './surfaceComputationStrategies'; import { createAndCacheSurfacesFromRaw } from './createAndCacheSurfacesFromRaw'; +const { + utilities: { + segmentation: { getUniqueSegmentIndices }, + }, + segmentation: { + state: { + getViewportIdsWithSegmentation, + getSegmentation, + getSegmentationRepresentation, + }, + triggerSegmentationEvents: { triggerSegmentationModified }, + }, + Enums: { SegmentationRepresentations }, +} = cornerstoneTools; + export async function updateSurfaceData(segmentationId) { const surfacesObj = await computeSurfaceFromLabelmapSegmentation( segmentationId diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/canComputeRequestedRepresentation.ts b/packages/polymorphic-segmentation/src/canComputeRequestedRepresentation.ts similarity index 86% rename from packages/tools/src/stateManagement/segmentation/polySeg/canComputeRequestedRepresentation.ts rename to packages/polymorphic-segmentation/src/canComputeRequestedRepresentation.ts index be7ce91e03..1c34b83653 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/canComputeRequestedRepresentation.ts +++ b/packages/polymorphic-segmentation/src/canComputeRequestedRepresentation.ts @@ -1,14 +1,15 @@ -import { SegmentationRepresentations } from '../../../enums'; -import type { RepresentationsData } from '../../../types'; -import { getSegmentation } from '../getSegmentation'; -import { validate as validateLabelmap } from '../../../tools/displayTools/Labelmap/validateLabelmap'; +import type { Types as ToolsTypes } from '@cornerstonejs/tools'; +import { Enums, segmentation, utilities } from '@cornerstonejs/tools'; + +type RepresentationsData = ToolsTypes.RepresentationsData; + +const { SegmentationRepresentations } = Enums; +const { getSegmentation } = segmentation.state; +const { validateLabelmap } = utilities.segmentation; // Map of conversion paths between source and target representations // You should read it as "source" -> "targets" -const conversionPaths = new Map< - SegmentationRepresentations, - Set ->([ +const conversionPaths = new Map([ [ SegmentationRepresentations.Labelmap, new Set([ @@ -43,7 +44,7 @@ const conversionPaths = new Map< */ function canComputeRequestedRepresentation( segmentationId: string, - type: SegmentationRepresentations + type: typeof SegmentationRepresentations ): boolean { const { representationData } = getSegmentation(segmentationId); @@ -73,7 +74,7 @@ function getExistingRepresentationTypes( let validateFn; switch (representationType) { case SegmentationRepresentations.Labelmap: - validateFn = validateLabelmap; + validateFn = validateLabelmap.validate; break; // Todo: add validation for other representation types } diff --git a/packages/polymorphic-segmentation/src/index.ts b/packages/polymorphic-segmentation/src/index.ts new file mode 100644 index 0000000000..313f9c322a --- /dev/null +++ b/packages/polymorphic-segmentation/src/index.ts @@ -0,0 +1,25 @@ +import { computeContourData } from './Contour/contourComputationStrategies'; +import { computeLabelmapData } from './Labelmap/labelmapComputationStrategies'; +import { computeSurfaceData } from './Surface/surfaceComputationStrategies'; +import { canComputeRequestedRepresentation } from './canComputeRequestedRepresentation'; + +// updates +import { updateSurfaceData } from './Surface/updateSurfaceData'; +import { registerPolySegWorker } from './registerPolySegWorker'; + +function init() { + // register the worker if it hasn't been registered yet + registerPolySegWorker(); +} + +export { + canComputeRequestedRepresentation, + // computes + computeContourData, + computeLabelmapData, + computeSurfaceData, + // updates + updateSurfaceData, + // init + init, +}; diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/registerPolySegWorker.ts b/packages/polymorphic-segmentation/src/registerPolySegWorker.ts similarity index 81% rename from packages/tools/src/stateManagement/segmentation/polySeg/registerPolySegWorker.ts rename to packages/polymorphic-segmentation/src/registerPolySegWorker.ts index 400f6c954c..dfbdaaa89e 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/registerPolySegWorker.ts +++ b/packages/polymorphic-segmentation/src/registerPolySegWorker.ts @@ -13,7 +13,7 @@ export function registerPolySegWorker() { // @ts-ignore return new Worker( // @ts-ignore - new URL('../../../workers/polySegConverters', import.meta.url), + new URL('./workers/polySegConverters.js', import.meta.url), { name: 'polySeg', type: 'module', @@ -24,7 +24,7 @@ export function registerPolySegWorker() { const workerManager = getWebWorkerManager(); const options = { - maxWorkerInstances: 1, // Todo, make this configurable + maxWorkerInstances: 1, autoTerminateOnIdle: { enabled: true, idleTimeThreshold: 2000, diff --git a/packages/tools/src/types/PolySeg.ts b/packages/polymorphic-segmentation/src/types/PolySegConversionOptions.ts similarity index 100% rename from packages/tools/src/types/PolySeg.ts rename to packages/polymorphic-segmentation/src/types/PolySegConversionOptions.ts diff --git a/packages/polymorphic-segmentation/src/types/index.ts b/packages/polymorphic-segmentation/src/types/index.ts new file mode 100644 index 0000000000..99b89a7394 --- /dev/null +++ b/packages/polymorphic-segmentation/src/types/index.ts @@ -0,0 +1,3 @@ +import type { PolySegConversionOptions } from './PolySegConversionOptions'; + +export type { PolySegConversionOptions }; diff --git a/packages/tools/src/stateManagement/segmentation/helpers/clipAndCacheSurfacesForViewport.ts b/packages/polymorphic-segmentation/src/utilities/clipAndCacheSurfacesForViewport.ts similarity index 95% rename from packages/tools/src/stateManagement/segmentation/helpers/clipAndCacheSurfacesForViewport.ts rename to packages/polymorphic-segmentation/src/utilities/clipAndCacheSurfacesForViewport.ts index 40ca5ee187..265d5b7ec9 100644 --- a/packages/tools/src/stateManagement/segmentation/helpers/clipAndCacheSurfacesForViewport.ts +++ b/packages/polymorphic-segmentation/src/utilities/clipAndCacheSurfacesForViewport.ts @@ -6,12 +6,14 @@ import { triggerEvent, } from '@cornerstonejs/core'; -import { WorkerTypes } from '../../../enums'; -import { pointToString } from '../../../utilities/pointToString'; -import { registerPolySegWorker } from '../polySeg/registerPolySegWorker'; -import { getSurfaceActorEntry } from './getSegmentationActor'; +import { Enums as ToolsEnums, utilities } from '@cornerstonejs/tools'; +import { registerPolySegWorker } from '../registerPolySegWorker'; + const workerManager = getWebWorkerManager(); +const { WorkerTypes } = ToolsEnums; +const { pointToString } = utilities; + /** * Surfaces info for clipping */ diff --git a/packages/polymorphic-segmentation/src/utilities/index.ts b/packages/polymorphic-segmentation/src/utilities/index.ts new file mode 100644 index 0000000000..5823f559ac --- /dev/null +++ b/packages/polymorphic-segmentation/src/utilities/index.ts @@ -0,0 +1,3 @@ +import { clipAndCacheSurfacesForViewport } from './clipAndCacheSurfacesForViewport'; + +export { clipAndCacheSurfacesForViewport }; diff --git a/packages/tools/src/workers/polySegConverters.js b/packages/polymorphic-segmentation/src/workers/polySegConverters.js similarity index 97% rename from packages/tools/src/workers/polySegConverters.js rename to packages/polymorphic-segmentation/src/workers/polySegConverters.js index a2d2b5370c..fff909f58d 100644 --- a/packages/tools/src/workers/polySegConverters.js +++ b/packages/polymorphic-segmentation/src/workers/polySegConverters.js @@ -1,5 +1,6 @@ import { expose } from 'comlink'; import { utilities } from '@cornerstonejs/core'; +import { utilities as ToolsUtilities } from '@cornerstonejs/tools'; import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane'; @@ -7,14 +8,25 @@ import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData'; import vtkContourLoopExtraction from '@kitware/vtk.js/Filters/General/ContourLoopExtraction'; import vtkCutter from '@kitware/vtk.js/Filters/Core/Cutter'; -import { getBoundingBoxAroundShapeWorld } from '../utilities/boundingBox'; -import { - containsPoint, - getAABB, - projectTo2D, -} from '../utilities/math/polyline'; -import { isPlaneIntersectingAABB } from '../utilities/planar'; -import { checkStandardBasis, rotatePoints } from '../geometricSurfaceUtils'; +const { + math: { + polyline: { containsPoint, getAABB, projectTo2D }, + }, + geometricSurfaceUtils: { checkStandardBasis, rotatePoints }, + boundingBox: { getBoundingBoxAroundShapeWorld }, + planar: { isPlaneIntersectingAABB }, +} = ToolsUtilities; + +async function peerImport(moduleId) { + try { + if (moduleId === '@icr/polyseg-wasm') { + return import('@icr/polyseg-wasm'); + } + } catch (error) { + console.warn('Error importing module:', error); + return null; + } +} /** * Object containing methods for converting between different representations of @@ -43,7 +55,7 @@ const polySegConverters = { async initializePolySeg(progressCallback) { let ICRPolySeg; try { - ICRPolySeg = (await import('@icr/polyseg-wasm')).default; + ICRPolySeg = (await peerImport('@icr/polyseg-wasm')).default; } catch (error) { console.error(error); console.debug( diff --git a/packages/polymorphic-segmentation/tsconfig.json b/packages/polymorphic-segmentation/tsconfig.json new file mode 100644 index 0000000000..bc915f1e65 --- /dev/null +++ b/packages/polymorphic-segmentation/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist/esm", + "rootDir": "./src" + }, + "include": ["./src/**/*"] +} diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md index e07c3dd6cc..eb548a42ea 100644 --- a/packages/tools/CHANGELOG.md +++ b/packages/tools/CHANGELOG.md @@ -3,14 +3,30 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [2.19.16](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.15...v2.19.16) (2025-02-26) +# [3.0.0-beta.6](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2025-02-27) + +### Features + +- Add key image adapters for key image point mark ([#1754](https://github.com/cornerstonejs/cornerstone3D/issues/1754)) ([a1fd3f9](https://github.com/cornerstonejs/cornerstone3D/commit/a1fd3f9d0ea40d53cafd792d59bc1dbfc90663a5)) + +# [3.0.0-beta.5](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2025-02-25) **Note:** Version bump only for package @cornerstonejs/tools -## [2.19.15](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.14...v2.19.15) (2025-02-26) +# [3.0.0-beta.4](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2025-02-25) **Note:** Version bump only for package @cornerstonejs/tools +# [3.0.0-beta.3](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2025-02-25) + +**Note:** Version bump only for package @cornerstonejs/tools + +# [3.0.0-beta.2](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.14...v3.0.0-beta.2) (2025-02-24) + +### Bug Fixes + +- publish beta for 3.0 ([8bf65df](https://github.com/cornerstonejs/cornerstone3D/commit/8bf65df9bec5f52459de1c49c4834b316f680f1b)) + ## [2.19.14](https://github.com/cornerstonejs/cornerstone3D/compare/v2.19.13...v2.19.14) (2025-02-24) ### Bug Fixes diff --git a/packages/tools/examples/labelmapInterpolation/index.ts b/packages/tools/examples/labelmapInterpolation/index.ts deleted file mode 100644 index 1a477f0808..0000000000 --- a/packages/tools/examples/labelmapInterpolation/index.ts +++ /dev/null @@ -1,338 +0,0 @@ -import type { Types } from '@cornerstonejs/core'; -import { - RenderingEngine, - Enums, - setVolumesForViewports, - volumeLoader, -} from '@cornerstonejs/core'; -import { - initDemo, - createImageIdsAndCacheMetaData, - setTitleAndDescription, - addDropdownToToolbar, - addSliderToToolbar, - setCtTransferFunctionForVolumeActor, - addButtonToToolbar, - addManipulationBindings, -} from '../../../../utils/demo/helpers'; -import * as cornerstoneTools from '@cornerstonejs/tools'; - -// This is for debugging purposes -console.warn( - 'Click on index.ts to open source code for this example --------->' -); - -const { - ToolGroupManager, - Enums: csToolsEnums, - segmentation, - RectangleScissorsTool, - SphereScissorsTool, - CircleScissorsTool, - BrushTool, - PaintFillTool, - utilities: cstUtils, -} = cornerstoneTools; - -const { MouseBindings } = csToolsEnums; -const { ViewportType } = Enums; -const { segmentation: segmentationUtils } = cstUtils; - -// Define a unique id for the volume -const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix -const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use -const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id -const segmentationId = 'MY_SEGMENTATION_ID'; -const toolGroupId = 'MY_TOOLGROUP_ID'; - -// ======== Set up page ======== // -setTitleAndDescription( - 'Labelmap Interpolation', - 'Here we demonstrate interpolation between slices for labelmaps' -); - -const size = '500px'; -const content = document.getElementById('content'); -const viewportGrid = document.createElement('div'); - -viewportGrid.style.display = 'flex'; -viewportGrid.style.display = 'flex'; -viewportGrid.style.flexDirection = 'row'; - -const element1 = document.createElement('div'); -const element2 = document.createElement('div'); -const element3 = document.createElement('div'); -element1.style.width = size; -element1.style.height = size; -element2.style.width = size; -element2.style.height = size; -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); - -const instructions = document.createElement('p'); -instructions.innerText = ` - Use the labelmap tools in the normal way. Note preview is turned off for those - tools to simplify initial segment creation. -
Segments are interpolated BETWEEN slices, so you need to create two or more - segments of the same segment index on slices in a viewport separated by at least - one empty segment. - Use "Extended Interpolation" button to interpolate segments which don't - overlap (assuming the segments were drawn on the same slice). - Use "Overlapping Interpolation" button to interpolate overlapping segments - that is, the segment must - overlap if drawn on the same slice to interpolate between them. This is a good choice - for multiple segments. - Accept the interpolation by hitting enter, or use the "Reject Preview/Interpolation" button. - `; - -content.append(instructions); - -const interpolationTools = new Map(); - -const configuration = {}; - -interpolationTools.set('CircularBrush', { - baseTool: BrushTool.toolName, - configuration: { - ...configuration, - activeStrategy: 'FILL_INSIDE_CIRCLE', - }, -}); - -const optionsValues = [ - ...interpolationTools.keys(), - RectangleScissorsTool.toolName, - CircleScissorsTool.toolName, - SphereScissorsTool.toolName, - PaintFillTool.toolName, -]; - -// ============================= // -addDropdownToToolbar({ - options: { values: optionsValues, defaultValue: BrushTool.toolName }, - onSelectedValueChange: (nameAsStringOrNumber) => { - const name = String(nameAsStringOrNumber); - const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); - - // Set the currently active tool disabled - const toolName = toolGroup.getActivePrimaryMouseButtonTool(); - - if (toolName) { - toolGroup.setToolDisabled(toolName); - } - - toolGroup.setToolActive(name, { - bindings: [{ mouseButton: MouseBindings.Primary }], - }); - }, -}); - -addSliderToToolbar({ - title: 'Brush Size', - range: [5, 100], - defaultValue: 25, - onSelectedValueChange: (valueAsStringOrNumber) => { - const value = Number(valueAsStringOrNumber); - segmentationUtils.setBrushSizeForToolGroup(toolGroupId, value); - }, -}); - -// ============================= // -addDropdownToToolbar({ - options: { values: ['1', '2', '3'], defaultValue: '1' }, - labelText: 'Segment', - onSelectedValueChange: (segmentIndex) => { - segmentation.segmentIndex.setActiveSegmentIndex( - segmentationId, - Number(segmentIndex) - ); - }, -}); - -addButtonToToolbar({ - title: 'Run Extended Interpolation', - onClick: () => { - const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); - const activeName = toolGroup.getActivePrimaryMouseButtonTool(); - const brush = toolGroup.getToolInstance(activeName); - brush.interpolate?.(element1, { extendedConfig: true }); - }, -}); - -addButtonToToolbar({ - title: 'Run Overlapping Interpolation', - onClick: () => { - const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); - const activeName = toolGroup.getActivePrimaryMouseButtonTool(); - const brush = toolGroup.getToolInstance(activeName); - brush.interpolate?.(element1, { extendedConfig: false }); - }, -}); - -// ============================= // - -async function addSegmentationsToState() { - // Create a segmentation of the same resolution as the source data - await volumeLoader.createAndCacheDerivedLabelmapVolume(volumeId, { - volumeId: segmentationId, - }); - - // Add the segmentations to state - segmentation.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, - }, - }, - }, - ]); -} - -/** - * Runs the demo - */ -async function run() { - // Init Cornerstone and related libraries - await initDemo(); - - // Add tools to Cornerstone3D - cornerstoneTools.addTool(RectangleScissorsTool); - cornerstoneTools.addTool(CircleScissorsTool); - cornerstoneTools.addTool(SphereScissorsTool); - cornerstoneTools.addTool(PaintFillTool); - cornerstoneTools.addTool(BrushTool); - - // Define tool groups to add the segmentation display tool to - const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); - - // Manipulation Tools - addManipulationBindings(toolGroup); - - // Segmentation Tools - toolGroup.addTool(RectangleScissorsTool.toolName); - toolGroup.addTool(CircleScissorsTool.toolName); - toolGroup.addTool(SphereScissorsTool.toolName); - toolGroup.addTool(PaintFillTool.toolName); - toolGroup.addTool(BrushTool.toolName); - - for (const [toolName, config] of interpolationTools.entries()) { - if (config.baseTool) { - toolGroup.addToolInstance( - toolName, - config.baseTool, - config.configuration - ); - } else { - toolGroup.addTool(toolName, config.configuration); - } - if (config.passive) { - // This can be applied during add/remove contours - toolGroup.setToolPassive(toolName); - } - } - - toolGroup.setToolActive(interpolationTools.keys().next().value, { - bindings: [{ mouseButton: MouseBindings.Primary }], - }); - - // Get Cornerstone imageIds for the source data and fetch metadata into RAM - const imageIds = await createImageIdsAndCacheMetaData({ - StudyInstanceUID: - '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', - SeriesInstanceUID: - '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', - wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', - }); - - // Define a volume in memory - const volume = await volumeLoader.createAndCacheVolume(volumeId, { - imageIds, - }); - - // Add some segmentations based on the source data volume - await addSegmentationsToState(); - - // Instantiate a rendering engine - const renderingEngineId = 'myRenderingEngine'; - const renderingEngine = new RenderingEngine(renderingEngineId); - - // Create the viewports - const viewportId1 = 'CT_AXIAL'; - const viewportId2 = 'CT_SAGITTAL'; - const viewportId3 = 'CT_CORONAL'; - - const viewportInputArray = [ - { - viewportId: viewportId1, - type: ViewportType.ORTHOGRAPHIC, - element: element1, - defaultOptions: { - orientation: Enums.OrientationAxis.AXIAL, - background: [0, 0, 0], - }, - }, - { - viewportId: viewportId2, - type: ViewportType.ORTHOGRAPHIC, - element: element2, - defaultOptions: { - orientation: Enums.OrientationAxis.SAGITTAL, - background: [0, 0, 0], - }, - }, - { - viewportId: viewportId3, - type: ViewportType.ORTHOGRAPHIC, - element: element3, - defaultOptions: { - orientation: Enums.OrientationAxis.CORONAL, - background: [0, 0, 0], - }, - }, - ]; - - renderingEngine.setViewports(viewportInputArray); - - toolGroup.addViewport(viewportId1, renderingEngineId); - toolGroup.addViewport(viewportId2, renderingEngineId); - toolGroup.addViewport(viewportId3, renderingEngineId); - - // Set the volume to load - volume.load(); - - // Set volumes on the viewports - await setVolumesForViewports( - renderingEngine, - [{ volumeId, callback: setCtTransferFunctionForVolumeActor }], - [viewportId1, viewportId2, viewportId3] - ); - - // Add the segmentation representation to the toolgroup - const segMap = { - [viewportId1]: [{ segmentationId }], - [viewportId2]: [{ segmentationId }], - [viewportId3]: [{ segmentationId }], - }; - await segmentation.addLabelmapRepresentationToViewportMap(segMap); - - // Render the image - renderingEngine.render(); -} - -run(); diff --git a/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts b/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts index b96a751704..234cf1947c 100644 --- a/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts +++ b/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts @@ -109,7 +109,7 @@ const viewportId1 = viewportIds[0]; const viewportId2 = viewportIds[1]; const viewportId3 = viewportIds[2]; -const defaultThresholdOption = [...labelmapTools.thresholdOptions.keys()][2]; +const defaultThresholdOption = [...labelmapTools.thresholdOptions.keys()][5]; const thresholdArgs = labelmapTools.thresholdOptions.get( defaultThresholdOption ); @@ -119,9 +119,7 @@ interpolationTools.set('ThresholdSphereIsland', { configuration: { ...configuration, activeStrategy: 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL', - strategySpecificConfiguration: { - THRESHOLD: { ...thresholdArgs }, - }, + threshold: thresholdArgs, }, }); @@ -130,9 +128,7 @@ interpolationTools.set('ThresholdCircle', { configuration: { ...configuration, activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', - strategySpecificConfiguration: { - THRESHOLD: { ...thresholdArgs }, - }, + threshold: thresholdArgs, }, }); @@ -141,9 +137,7 @@ interpolationTools.set('ThresholdSphere', { configuration: { ...configuration, activeStrategy: 'THRESHOLD_INSIDE_SPHERE', - strategySpecificConfiguration: { - THRESHOLD: { ...thresholdArgs }, - }, + threshold: thresholdArgs, }, }); @@ -174,11 +168,7 @@ addDropdownToToolbar({ map: labelmapTools.thresholdOptions, }, onSelectedValueChange: (name, thresholdArgs) => { - segmentationUtils.setBrushThresholdForToolGroup( - toolGroupId, - thresholdArgs.threshold, - thresholdArgs - ); + segmentationUtils.setBrushThresholdForToolGroup(toolGroupId, thresholdArgs); }, }); diff --git a/packages/tools/examples/labelmapStatistics/index.ts b/packages/tools/examples/labelmapStatistics/index.ts index 082fc530df..5b8f9d9dae 100644 --- a/packages/tools/examples/labelmapStatistics/index.ts +++ b/packages/tools/examples/labelmapStatistics/index.ts @@ -1,58 +1,52 @@ -import type { Types } from '@cornerstonejs/core'; import { RenderingEngine, Enums, - setVolumesForViewports, - volumeLoader, + imageLoader, eventTarget, } from '@cornerstonejs/core'; +import * as cornerstoneTools from '@cornerstonejs/tools'; import { - initDemo, createImageIdsAndCacheMetaData, - setTitleAndDescription, + initDemo, addDropdownToToolbar, - addSliderToToolbar, - setCtTransferFunctionForVolumeActor, - getLocalUrl, - addManipulationBindings, + setTitleAndDescription, + addButtonToToolbar, + addBrushSizeSlider, } from '../../../../utils/demo/helpers'; -import * as cornerstoneTools from '@cornerstonejs/tools'; // This is for debugging purposes -console.warn( +console.debug( 'Click on index.ts to open source code for this example --------->' ); const { ToolGroupManager, + ZoomTool, + StackScrollTool, Enums: csToolsEnums, - segmentation, RectangleScissorsTool, - SphereScissorsTool, CircleScissorsTool, BrushTool, PaintFillTool, + PanTool, + segmentation, utilities: cstUtils, } = cornerstoneTools; -const { MouseBindings, Events } = csToolsEnums; +const { MouseBindings, KeyboardBindings, Events } = csToolsEnums; const { ViewportType } = Enums; const { segmentation: segmentationUtils, roundNumber } = cstUtils; // Define a unique id for the volume -const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix -const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use -const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id -const segmentationId = 'MY_SEGMENTATION_ID'; -const toolGroupId = 'MY_TOOLGROUP_ID'; -const viewports = []; - -const DEFAULT_BRUSH_SIZE = 20; +let renderingEngine; +const renderingEngineId = 'myRenderingEngine'; +const viewportId = 'STACK_VIEWPORT'; +const toolGroupId = 'TOOL_GROUP_ID'; // ======== Set up page ======== // setTitleAndDescription( - 'Labelmap Segmentation Statistics', - 'Here we demonstrate calculating labelmap statistics' + 'Stack Segmentation Statistics', + 'Here we demonstrate how to calculate statistics for a stack segmentation.' ); const size = '500px'; @@ -60,7 +54,6 @@ const content = document.getElementById('content'); const statsGrid = document.createElement('div'); statsGrid.style.display = 'flex'; -statsGrid.style.display = 'flex'; statsGrid.style.flexDirection = 'row'; statsGrid.style.fontSize = 'smaller'; @@ -81,179 +74,29 @@ for (const statsId of statsIds) { content.appendChild(statsGrid); const viewportGrid = document.createElement('div'); - -viewportGrid.style.display = 'flex'; viewportGrid.style.display = 'flex'; viewportGrid.style.flexDirection = 'row'; -const element1 = document.createElement('div'); -const element2 = document.createElement('div'); -const element3 = document.createElement('div'); -element1.style.width = size; -element1.style.height = size; -element2.style.width = size; -element2.style.height = size; -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); +const element = document.createElement('div'); +element.style.width = size; +element.style.height = size; +element.oncontextmenu = () => false; +viewportGrid.appendChild(element); content.appendChild(viewportGrid); const instructions = document.createElement('p'); instructions.innerText = ` - Hover - show preview of segmentation tool - Left drag to extend preview - Left Click (or enter) to accept preview - Reject preview by button (or esc) - Hover outside of region to reset to hovered over segment index - Shift Left - zoom, Ctrl Left - Pan, Alt Left - Stack Scroll + Left Click: Use selected Segmentation Tool. + Middle Click: Pan + Right Click: Zoom + Mouse wheel: Scroll Stack `; content.append(instructions); -const interpolationTools = new Map(); -const configuration = { - preview: { - enabled: true, - }, -}; - -const thresholdOptions = new Map(); -thresholdOptions.set('Dynamic Radius 0', { isDynamic: true, dynamicRadius: 0 }); -thresholdOptions.set('Dynamic Radius 1', { isDynamic: true, dynamicRadius: 1 }); -thresholdOptions.set('Dynamic Radius 3', { isDynamic: true, dynamicRadius: 3 }); -thresholdOptions.set('Dynamic Radius 5', { isDynamic: true, dynamicRadius: 5 }); -thresholdOptions.set('Use Existing Threshold', { - isDynamic: false, - dynamicRadius: 5, -}); -thresholdOptions.set('CT Fat: (-150, -70)', { - threshold: [-150, -70], - isDynamic: false, -}); -thresholdOptions.set('CT Bone: (200, 1000)', { - threshold: [200, 1000], - isDynamic: false, -}); -const defaultThresholdOption = [...thresholdOptions.keys()][2]; -const thresholdArgs = thresholdOptions.get(defaultThresholdOption); - -interpolationTools.set('ThresholdSphere', { - baseTool: BrushTool.toolName, - configuration: { - ...configuration, - activeStrategy: 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL', - strategySpecificConfiguration: { - useCenterSegmentIndex: true, - THRESHOLD: { ...thresholdArgs }, - }, - }, -}); - -interpolationTools.set('SphereBrush', { - baseTool: BrushTool.toolName, - configuration: { - ...configuration, - activeStrategy: 'FILL_INSIDE_SPHERE', - strategySpecificConfiguration: { - useCenterSegmentIndex: true, - }, - }, -}); - -const optionsValues = [...interpolationTools.keys()]; - // ============================= // -// Create a reference to the threshold dropdown element -const thresholdDropdownElement = null; - -addDropdownToToolbar({ - options: { values: optionsValues, defaultValue: BrushTool.toolName }, - onSelectedValueChange: (nameAsStringOrNumber) => { - const name = String(nameAsStringOrNumber); - const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); - - // Set the currently active tool disabled - const toolName = toolGroup.getActivePrimaryMouseButtonTool(); - - if (toolName) { - toolGroup.setToolDisabled(toolName); - } - - toolGroup.setToolActive(name, { - bindings: [{ mouseButton: MouseBindings.Primary }], - }); - - // Show/hide threshold dropdown based on selected tool - if (thresholdDropdownElement) { - thresholdDropdownElement.style.display = name - .toLowerCase() - .includes('threshold') - ? 'inline-block' - : 'none'; - } - }, -}); - -// Store reference to threshold dropdown element -thresholdDropdownElement = addDropdownToToolbar({ - options: { - values: Array.from(thresholdOptions.keys()), - defaultValue: defaultThresholdOption, - }, - onSelectedValueChange: (nameAsStringOrNumber) => { - const name = String(nameAsStringOrNumber); - const thresholdArgs = thresholdOptions.get(name); - - segmentationUtils.setBrushThresholdForToolGroup( - toolGroupId, - thresholdArgs.threshold, - thresholdArgs - ); - }, -}); - -// Initially hide threshold dropdown if first tool doesn't include 'threshold' -if (thresholdDropdownElement) { - const initialTool = optionsValues[0]; - thresholdDropdownElement.style.display = initialTool - .toLowerCase() - .includes('threshold') - ? 'inline-block' - : 'none'; -} - -addSliderToToolbar({ - title: 'Brush Size', - range: [5, 100], - defaultValue: DEFAULT_BRUSH_SIZE, - onSelectedValueChange: (valueAsStringOrNumber) => { - const value = Number(valueAsStringOrNumber); - segmentationUtils.setBrushSizeForToolGroup(toolGroupId, value); - }, -}); - -// ============================= // -addDropdownToToolbar({ - options: { values: ['1', '2'], defaultValue: '1' }, - labelText: 'Segment', - onSelectedValueChange: (segmentIndex) => { - segmentation.segmentIndex.setActiveSegmentIndex( - segmentationId, - Number(segmentIndex) - ); - }, -}); - function displayStat(stat) { if (!stat) { return; @@ -263,30 +106,26 @@ function displayStat(stat) { }`; } -function calculateStatistics(id, indices) { - const [viewport] = viewports; - const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); - const activeName = toolGroup.getActivePrimaryMouseButtonTool(); - const brush = toolGroup.getToolInstance(activeName); - const stats = brush.getStatistics(viewport.element, { indices }); +async function calculateStatistics(id, indices) { + const viewport = renderingEngine.getViewport(viewportId); + const stats = await segmentationUtils.getStatistics({ + segmentationId: 'SEGMENTATION_ID', + segmentIndices: indices, + }); if (!stats) { return; } const items = [`Statistics on ${indices.join(', ')}`]; stats.count.label = 'Voxels'; - // const lesionGlycolysis = { - // name: 'Lesion Glycolysis', - // value: stats.volume.value * stats.stdDev.value, - // unit: 'HU \xB7 mm \xb3', - // }; + items.push( displayStat(stats.volume), displayStat(stats.count), - // displayStat(lesionGlycolysis), displayStat(stats.mean), displayStat(stats.max), - displayStat(stats.min) + displayStat(stats.min), + displayStat(stats.peakValue) ); const statsDiv = document.getElementById(id); statsDiv.innerHTML = items.map((span) => `${span}
\n`).join('\n'); @@ -317,170 +156,161 @@ function segmentationModifiedCallback(evt) { // ============================= // -async function addSegmentationsToState() { - // Create a segmentation of the same resolution as the source data - volumeLoader.createAndCacheDerivedLabelmapVolume(volumeId, { - volumeId: segmentationId, - }); - - // Add the segmentations to state - segmentation.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, - }, - }, - }, - ]); - - eventTarget.addEventListener( - Events.SEGMENTATION_DATA_MODIFIED, - segmentationModifiedCallback - ); -} - -/** - * Runs the demo - */ -async function run() { - // Init Cornerstone and related libraries - await initDemo(); - +function setupTools() { // Add tools to Cornerstone3D + cornerstoneTools.addTool(PanTool); + cornerstoneTools.addTool(ZoomTool); + cornerstoneTools.addTool(StackScrollTool); cornerstoneTools.addTool(RectangleScissorsTool); cornerstoneTools.addTool(CircleScissorsTool); - cornerstoneTools.addTool(SphereScissorsTool); cornerstoneTools.addTool(PaintFillTool); cornerstoneTools.addTool(BrushTool); - // Define tool groups to add the segmentation display tool to + // Define a tool group const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); - // Manipulation Tools - addManipulationBindings(toolGroup); - - // Segmentation Tools + // Add tools to the group + toolGroup.addTool(PanTool.toolName); + toolGroup.addTool(ZoomTool.toolName); + toolGroup.addTool(StackScrollTool.toolName); toolGroup.addTool(RectangleScissorsTool.toolName); toolGroup.addTool(CircleScissorsTool.toolName); - toolGroup.addTool(SphereScissorsTool.toolName); toolGroup.addTool(PaintFillTool.toolName); toolGroup.addTool(BrushTool.toolName); - for (const [toolName, config] of interpolationTools.entries()) { - if (config.baseTool) { - toolGroup.addToolInstance( - toolName, - config.baseTool, - config.configuration - ); - } else { - toolGroup.addTool(toolName, config.configuration); - } - if (config.passive) { - // This can be applied during add/remove contours - toolGroup.setToolPassive(toolName); - } - } - - toolGroup.setToolActive(interpolationTools.keys().next().value, { + // Set tool modes + toolGroup.setToolActive(BrushTool.toolName, { bindings: [{ mouseButton: MouseBindings.Primary }], }); - // Get Cornerstone imageIds for the source data and fetch metadata into RAM + toolGroup.setToolActive(PanTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Auxiliary, + }, + { + mouseButton: MouseBindings.Primary, + modifierKey: KeyboardBindings.Ctrl, + }, + ], + }); + + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Secondary, + }, + { + mouseButton: MouseBindings.Primary, + modifierKey: KeyboardBindings.Shift, + }, + ], + }); + + toolGroup.setToolActive(StackScrollTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Wheel }], + }); + + return toolGroup; +} + +// ============================= // + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + const toolGroup = setupTools(); + + // Get Cornerstone imageIds and fetch metadata into RAM + // const imageIds = await createImageIdsAndCacheMetaData({ + // StudyInstanceUID: + // '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + // SeriesInstanceUID: + // '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + // wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + // }); + // Get Cornerstone imageIds and fetch metadata into RAM const imageIds = await createImageIdsAndCacheMetaData({ StudyInstanceUID: - '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + '1.3.6.1.4.1.14519.5.2.1.1188.2803.137585363493444318569098508293', SeriesInstanceUID: - '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', - wadoRsRoot: - getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + '1.3.6.1.4.1.14519.5.2.1.1188.2803.699272945123913604672897602509', + SOPInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.1188.2803.295285318555680716246271899544', + wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', }); - - // Define a volume in memory - const volume = await volumeLoader.createAndCacheVolume(volumeId, { - imageIds, + const imageIds2 = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: '1.2.840.113663.1500.1.248223208.1.1.20110323.105903.687', + SeriesInstanceUID: + '1.2.840.113663.1500.1.248223208.2.1.20110323.105903.687', + SOPInstanceUID: '1.2.840.113663.1500.1.248223208.3.10.20110323.110423.875', + wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', }); - // Add some segmentations based on the source data volume - await addSegmentationsToState(); + // Create a stack of images + const imageIdsArray = imageIds.slice(0, 10); + // Create segmentation images for the stack + const segImages = await imageLoader.createAndCacheDerivedLabelmapImages( + imageIdsArray + ); // Instantiate a rendering engine - const renderingEngineId = 'myRenderingEngine'; - const renderingEngine = new RenderingEngine(renderingEngineId); + renderingEngine = new RenderingEngine(renderingEngineId); + + // Create the viewport + const viewportInput = { + viewportId, + type: ViewportType.STACK, + element, + }; - // Create the viewports - const viewportId1 = 'CT_AXIAL'; - const viewportId2 = 'CT_SAGITTAL'; - const viewportId3 = 'CT_CORONAL'; + renderingEngine.setViewports([viewportInput]); - const viewportInputArray = [ - { - viewportId: viewportId1, - type: ViewportType.ORTHOGRAPHIC, - element: element1, - defaultOptions: { - orientation: Enums.OrientationAxis.AXIAL, - background: [0, 0, 0], - }, - }, + // Set the stack of images + const viewport = renderingEngine.getViewport(viewportId); + await viewport.setStack(imageIdsArray, 0); + + // Add the viewport to the toolgroup + toolGroup.addViewport(viewportId, renderingEngineId); + + // Add segmentation + segmentation.addSegmentations([ { - viewportId: viewportId2, - type: ViewportType.ORTHOGRAPHIC, - element: element2, - defaultOptions: { - orientation: Enums.OrientationAxis.SAGITTAL, - background: [0, 0, 0], + segmentationId: 'SEGMENTATION_ID', + representation: { + type: csToolsEnums.SegmentationRepresentations.Labelmap, + data: { + imageIds: segImages.map((it) => it.imageId), + }, }, }, + ]); + + // Add the segmentation representation to the viewport + await segmentation.addSegmentationRepresentations(viewportId, [ { - viewportId: viewportId3, - type: ViewportType.ORTHOGRAPHIC, - element: element3, - defaultOptions: { - orientation: Enums.OrientationAxis.CORONAL, - background: [0, 0, 0], - }, + segmentationId: 'SEGMENTATION_ID', + type: csToolsEnums.SegmentationRepresentations.Labelmap, }, - ]; - - renderingEngine.setViewports(viewportInputArray); - - toolGroup.addViewport(viewportId1, renderingEngineId); - toolGroup.addViewport(viewportId2, renderingEngineId); - toolGroup.addViewport(viewportId3, renderingEngineId); + ]); - viewports.push(...renderingEngine.getViewports()); + // Add brush size slider + addBrushSizeSlider({ + toolGroupId, + }); - // Set the volume to load - volume.load(); + cornerstoneTools.utilities.stackContextPrefetch.enable(element); - // Set volumes on the viewports - await setVolumesForViewports( - renderingEngine, - [{ volumeId, callback: setCtTransferFunctionForVolumeActor }], - [viewportId1, viewportId2, viewportId3] + // Add segmentation modified callback + eventTarget.addEventListener( + Events.SEGMENTATION_DATA_MODIFIED, + segmentationModifiedCallback ); - const segmentationRepresentation = [ - { - segmentationId, - }, - ]; - // Add the segmentation representation to the toolgroup - await segmentation.addLabelmapRepresentationToViewportMap({ - [viewportId1]: segmentationRepresentation, - [viewportId2]: segmentationRepresentation, - [viewportId3]: segmentationRepresentation, - }); - - segmentationUtils.setBrushSizeForToolGroup(toolGroupId, DEFAULT_BRUSH_SIZE); - // Render the image renderingEngine.render(); } diff --git a/packages/tools/examples/stackLabelmapSegmentation/index.ts b/packages/tools/examples/stackLabelmapSegmentation/index.ts index 739c3b7f58..1b89cbbdc8 100644 --- a/packages/tools/examples/stackLabelmapSegmentation/index.ts +++ b/packages/tools/examples/stackLabelmapSegmentation/index.ts @@ -84,7 +84,8 @@ const brushInstanceNames = { CircularBrush: 'CircularBrush', SphereBrush: 'SphereBrush', CircularEraser: 'CircularEraser', - ThresholdBrush: 'ThresholdBrush', + ThresholdBrushCircle: 'ThresholdBrushCircle', + ThresholdBrushSphere: 'ThresholdBrushSphere', DynamicThreshold: 'DynamicThreshold', }; @@ -92,7 +93,8 @@ const brushStrategies = { [brushInstanceNames.CircularBrush]: 'FILL_INSIDE_CIRCLE', [brushInstanceNames.SphereBrush]: 'FILL_INSIDE_SPHERE', [brushInstanceNames.CircularEraser]: 'ERASE_INSIDE_CIRCLE', - [brushInstanceNames.ThresholdBrush]: 'THRESHOLD_INSIDE_CIRCLE', + [brushInstanceNames.ThresholdBrushCircle]: 'THRESHOLD_INSIDE_CIRCLE', + [brushInstanceNames.ThresholdBrushSphere]: 'THRESHOLD_INSIDE_SPHERE', [brushInstanceNames.DynamicThreshold]: 'THRESHOLD_INSIDE_CIRCLE', }; @@ -100,7 +102,14 @@ const brushValues = [ brushInstanceNames.CircularBrush, brushInstanceNames.SphereBrush, brushInstanceNames.CircularEraser, - brushInstanceNames.ThresholdBrush, + brushInstanceNames.ThresholdBrushCircle, + brushInstanceNames.ThresholdBrushSphere, + brushInstanceNames.DynamicThreshold, +]; + +const thresholdBrushValues = [ + brushInstanceNames.ThresholdBrushCircle, + brushInstanceNames.ThresholdBrushSphere, brushInstanceNames.DynamicThreshold, ]; @@ -155,19 +164,24 @@ addDropdownToToolbar({ // Set the currently active tool disabled const toolName = toolGroup.getActivePrimaryMouseButtonTool(); - if (toolName) { toolGroup.setToolDisabled(toolName); } + // Show/hide threshold dropdown based on selected tool + const thresholdDropdown = document.getElementById('thresholdDropdown'); + if (thresholdDropdown) { + thresholdDropdown.style.display = thresholdBrushValues.includes(name) + ? 'block' + : 'none'; + } + if (brushValues.includes(name)) { toolGroup.setToolActive(name, { bindings: [{ mouseButton: MouseBindings.Primary }], }); } else { - const toolName = name; - - toolGroup.setToolActive(toolName, { + toolGroup.setToolActive(name, { bindings: [{ mouseButton: MouseBindings.Primary }], }); } @@ -194,7 +208,7 @@ addDropdownToToolbar({ segmentationUtils.setBrushThresholdForToolGroup(toolGroupId, threshold); }, -}); +}).style.display = 'none'; addButtonToToolbar({ title: 'Create New Segmentation on Current Image', @@ -244,7 +258,6 @@ addDropdownToToolbar({ options: { values: segmentationIds, defaultValue: '' }, onSelectedValueChange: (nameAsStringOrNumber) => { const name = String(nameAsStringOrNumber); - const index = segmentationIds.indexOf(name); segmentation.activeSegmentation.setActiveSegmentation(viewportId, name); // Update the dropdown @@ -298,25 +311,20 @@ function setupTools(toolGroupId) { activeStrategy: brushStrategies.CircularEraser, } ); + toolGroup.addToolInstance( - brushInstanceNames.ThresholdBrush, + brushInstanceNames.ThresholdBrushCircle, BrushTool.toolName, { - activeStrategy: brushStrategies.ThresholdBrush, + activeStrategy: brushStrategies.ThresholdBrushCircle, } ); + toolGroup.addToolInstance( - brushInstanceNames.DynamicThreshold, + brushInstanceNames.ThresholdBrushSphere, BrushTool.toolName, { - activeStrategy: brushStrategies.DynamicThreshold, - preview: { - enabled: true, - }, - strategySpecificConfiguration: { - useCenterSegmentIndex: true, - THRESHOLD: { isDynamic: true, dynamicRadius: 3 }, - }, + activeStrategy: brushStrategies.ThresholdBrushSphere, } ); diff --git a/packages/tools/examples/stackRange/index.ts b/packages/tools/examples/stackRange/index.ts index bb6e5dfe1b..02b7be9c2c 100644 --- a/packages/tools/examples/stackRange/index.ts +++ b/packages/tools/examples/stackRange/index.ts @@ -180,6 +180,17 @@ addButtonToToolbar({ }, }); +addButtonToToolbar({ + id: 'Toggle Point', + title: 'Toggle Point', + onClick() { + const annotation = getActiveAnnotation(); + if (annotation) { + KeyImageTool.setPoint(annotation); + } + }, +}); + function annotationModifiedListener(evt) { updateAnnotationDiv( evt.detail.annotation?.annotationUID || diff --git a/packages/tools/examples/videoSegmentation/index.ts b/packages/tools/examples/videoSegmentation/index.ts index 08839726fd..a8dceb0e19 100644 --- a/packages/tools/examples/videoSegmentation/index.ts +++ b/packages/tools/examples/videoSegmentation/index.ts @@ -165,9 +165,10 @@ function setupTools(toolGroupId) { preview: { enabled: true, }, - strategySpecificConfiguration: { - useCenterSegmentIndex: true, - THRESHOLD: { isDynamic: true, dynamicRadius: 3 }, + useCenterSegmentIndex: true, + threshold: { + isDynamic: true, + dynamicRadius: 3, }, } ); diff --git a/packages/tools/package.json b/packages/tools/package.json index f801d60a9a..9b5a8b39ec 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/tools", - "version": "2.19.16", + "version": "3.0.0-beta.6", "description": "Cornerstone3D Tools", "types": "./dist/esm/index.d.ts", "module": "./dist/esm/index.js", @@ -104,7 +104,7 @@ "canvas": "^2.11.2" }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.16", + "@cornerstonejs/core": "^3.0.0-beta.6", "@kitware/vtk.js": "32.9.0", "@types/d3-array": "^3.0.4", "@types/d3-interpolate": "^3.0.1", diff --git a/packages/tools/src/config.ts b/packages/tools/src/config.ts new file mode 100644 index 0000000000..778b2fbda5 --- /dev/null +++ b/packages/tools/src/config.ts @@ -0,0 +1,106 @@ +import type { ContourSegmentationData } from './types'; +import type { Types } from '@cornerstonejs/core'; +import type { LabelmapSegmentationData } from './types/LabelmapTypes'; +import type { SurfaceSegmentationData } from './types/SurfaceTypes'; +import type SegmentationRepresentations from './enums/SegmentationRepresentations'; + +/** + * Options for converting between segmentation representations using PolySeg + * set + */ +export type PolySegConversionOptions = { + /** Optional array of segment indices to convert */ + segmentIndices?: number[]; + /** ID of the segmentation to convert */ + segmentationId?: string; + /** Viewport to use for conversion */ + viewport?: Types.IStackViewport | Types.IVolumeViewport; +}; + +/** + * Interface for the PolySeg add-on that handles segmentation representation conversions + */ +type ComputeRepresentationFn = ( + segmentationId: string, + options: PolySegConversionOptions +) => Promise; + +type PolySegAddOn = { + /** Checks if a representation type can be computed for a segmentation */ + canComputeRequestedRepresentation: ( + segmentationId: string, + representationType: SegmentationRepresentations + ) => boolean; + + /** Initializes the PolySeg add-on */ + init: () => void; + + /** Computes different segmentation representation data */ + computeContourData: ComputeRepresentationFn; + computeLabelmapData: ComputeRepresentationFn; + computeSurfaceData: ComputeRepresentationFn; + + /** Updates different segmentation representation data */ + updateSurfaceData: ComputeRepresentationFn; +}; + +/** + * Available add-ons that can be configured + */ +type AddOns = { + polySeg: PolySegAddOn; +}; + +/** + * Configuration type containing add-ons + */ +export type Config = { + addons: AddOns; +}; + +let config = {} as Config; + +/** + * Gets the current configuration + */ +export function getConfig(): Config { + return config; +} + +/** + * Sets a new configuration + */ +export function setConfig(newConfig: Config): void { + config = newConfig; +} + +/** + * Gets configured add-ons + */ +export function getAddOns(): AddOns { + return config.addons; +} + +let polysegInitialized = false; + +/** + * Gets the PolySeg add-on, initializing it if needed + * @returns The PolySeg add-on instance or null if not configured + */ +export function getPolySeg() { + if (!config.addons?.polySeg) { + console.warn( + 'PolySeg add-on not configured. This will prevent automatic conversion between segmentation representations (labelmap, contour, surface). To enable these features, install @cornerstonejs/polymorphic-segmentation and register it during initialization: cornerstoneTools.init({ addons: { polySeg } }).' + ); + + return null; + } + + const polyseg = config.addons.polySeg; + if (!polysegInitialized) { + polyseg.init(); + polysegInitialized = true; + } + + return polyseg; +} diff --git a/packages/tools/src/enums/StrategyCallbacks.ts b/packages/tools/src/enums/StrategyCallbacks.ts index 36ad22d143..dc43217ec7 100644 --- a/packages/tools/src/enums/StrategyCallbacks.ts +++ b/packages/tools/src/enums/StrategyCallbacks.ts @@ -63,6 +63,12 @@ enum StrategyCallbacks { /** Compute statistics on this instance */ GetStatistics = 'getStatistics', + + /** Handle stack viewport sphere brush overrides */ + EnsureImageVolumeFor3DManipulation = 'ensureImageVolumeFor3DManipulation', + + /** Handle stack image reference for 3D manipulation */ + EnsureSegmentationVolumeFor3DManipulation = 'ensureSegmentationVolumeFor3DManipulation', } export default StrategyCallbacks; diff --git a/packages/tools/src/enums/WorkerTypes.ts b/packages/tools/src/enums/WorkerTypes.ts index bf1660f92a..9afc93ac09 100644 --- a/packages/tools/src/enums/WorkerTypes.ts +++ b/packages/tools/src/enums/WorkerTypes.ts @@ -11,6 +11,10 @@ enum ChangeTypes { POLYSEG_LABELMAP_TO_SURFACE = 'Converting Labelmap to Surface', SURFACE_CLIPPING = 'Clipping Surfaces', + + COMPUTE_STATISTICS = 'Computing Statistics', + + INTERPOLATE_LABELMAP = 'Interpolating Labelmap', } export default ChangeTypes; diff --git a/packages/tools/src/init.ts b/packages/tools/src/init.ts index ecd9426844..77f0de492f 100644 --- a/packages/tools/src/init.ts +++ b/packages/tools/src/init.ts @@ -16,6 +16,8 @@ import { annotationInterpolationEventDispatcher } from './eventDispatchers'; import * as ToolGroupManager from './store/ToolGroupManager'; import { defaultSegmentationStateManager } from './stateManagement/segmentation/SegmentationStateManager'; import segmentationRepresentationModifiedListener from './eventListeners/segmentation/segmentationRepresentationModifiedListener'; +import { setConfig } from './config'; +import type { Config } from './config'; let csToolsInitialized = false; @@ -25,11 +27,13 @@ let csToolsInitialized = false; * @param defaultConfiguration - A configuration object that will be used to * initialize the tool. */ -export function init(defaultConfiguration = {}): void { +export function init(defaultConfiguration = {} as Config): void { if (csToolsInitialized) { return; } + setConfig(defaultConfiguration); + _addCornerstoneEventListeners(); _addCornerstoneToolsEventListeners(); diff --git a/packages/tools/src/stateManagement/annotation/helpers/state.ts b/packages/tools/src/stateManagement/annotation/helpers/state.ts index ce96dd7119..4347c51821 100644 --- a/packages/tools/src/stateManagement/annotation/helpers/state.ts +++ b/packages/tools/src/stateManagement/annotation/helpers/state.ts @@ -95,14 +95,16 @@ function triggerAnnotationRemoved( /** * Triggers an annotation modified event. + * Note this no longer requires the element, which should be handled by testing + * to see which viewports this element is shown on. */ function triggerAnnotationModified( annotation: Annotation, - element: HTMLDivElement, + element?: HTMLDivElement, changeType = ChangeTypes.HandlesUpdated ): void { - const enabledElement = getEnabledElement(element); - const { viewportId, renderingEngineId } = enabledElement; + const enabledElement = element && getEnabledElement(element); + const { viewportId, renderingEngineId } = enabledElement || {}; const eventType = Events.ANNOTATION_MODIFIED; const eventDetail: AnnotationModifiedEventDetail = { annotation, diff --git a/packages/tools/src/stateManagement/segmentation/helpers/normalizeSegmentationInput.ts b/packages/tools/src/stateManagement/segmentation/helpers/normalizeSegmentationInput.ts index a3186393b2..845ca14d2c 100644 --- a/packages/tools/src/stateManagement/segmentation/helpers/normalizeSegmentationInput.ts +++ b/packages/tools/src/stateManagement/segmentation/helpers/normalizeSegmentationInput.ts @@ -102,7 +102,7 @@ function normalizeSurfaceSegments( surfaceData: SurfaceSegmentationData ): void { const { geometryIds } = surfaceData; - geometryIds.forEach((geometryId) => { + geometryIds?.forEach((geometryId) => { const geometry = cache.getGeometry(geometryId); if (geometry?.data) { const { segmentIndex } = geometry.data; diff --git a/packages/tools/src/stateManagement/segmentation/helpers/validateSegmentationInput.ts b/packages/tools/src/stateManagement/segmentation/helpers/validateSegmentationInput.ts index d7b733f63c..5dcfc67b3b 100644 --- a/packages/tools/src/stateManagement/segmentation/helpers/validateSegmentationInput.ts +++ b/packages/tools/src/stateManagement/segmentation/helpers/validateSegmentationInput.ts @@ -1,6 +1,6 @@ import * as Enums from '../../../enums'; import type { SegmentationPublicInput } from '../../../types/SegmentationStateTypes'; -import { validatePublic as validatePublicLabelmap } from '../../../tools/displayTools/Labelmap/validateLabelmap'; +import { validatePublic as validatePublicLabelmap } from '../../../utilities/segmentation/validateLabelmap'; /** * Validates the given segmentationInputArray to ensure it contains diff --git a/packages/tools/src/stateManagement/segmentation/index.ts b/packages/tools/src/stateManagement/segmentation/index.ts index 6542417cb7..1c5e879ef6 100644 --- a/packages/tools/src/stateManagement/segmentation/index.ts +++ b/packages/tools/src/stateManagement/segmentation/index.ts @@ -28,17 +28,17 @@ import * as segmentIndex from './segmentIndex'; import * as triggerSegmentationEvents from './triggerSegmentationEvents'; import { convertStackToVolumeLabelmap } from './helpers/convertStackToVolumeLabelmap'; import { computeVolumeLabelmapFromStack } from './helpers/computeVolumeLabelmapFromStack'; -import * as polySegManager from './polySeg'; import { clearSegmentValue } from './helpers/clearSegmentValue'; import { convertVolumeToStackLabelmap } from './helpers/computeStackLabelmapFromVolume'; import { removeSegment } from './removeSegment'; import { getLabelmapImageIds } from './getLabelmapImageIds'; import * as strategies from './../../tools/segmentation/strategies'; - import { removeAllSegmentations, removeSegmentation, } from './removeSegmentation'; +import { segmentationStyle } from './SegmentationStyle'; +import { defaultSegmentationStateManager } from './SegmentationStateManager'; const helpers = { clearSegmentValue, @@ -74,9 +74,10 @@ export { segmentIndex, triggerSegmentationEvents, helpers, - polySegManager as polySeg, removeSegment, getLabelmapImageIds, addRepresentationData, strategies, + segmentationStyle, + defaultSegmentationStateManager, }; diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Contour/computeAndAddContourRepresentation.ts b/packages/tools/src/stateManagement/segmentation/polySeg/Contour/computeAndAddContourRepresentation.ts deleted file mode 100644 index 513f8a8a8d..0000000000 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Contour/computeAndAddContourRepresentation.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { SegmentationRepresentations } from '../../../../enums'; -import type { PolySegConversionOptions } from '../../../../types'; -import { computeAndAddRepresentation } from '../computeAndAddRepresentation'; -import { computeContourData } from './contourComputationStrategies'; - -/** - * Computes and adds the contour representation for a given segmentation. - * - * @param segmentationId - The id of the segmentation - * @param options - Optional parameters for computing the contour representation - * @returns A promise that resolves when the contour representation is computed and added - */ -export function computeAndAddContourRepresentation( - segmentationId: string, - options: PolySegConversionOptions = {} -) { - return computeAndAddRepresentation( - segmentationId, - SegmentationRepresentations.Contour, - () => computeContourData(segmentationId, options), - () => undefined - ); -} diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/computeAndAddLabelmapRepresentation.ts b/packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/computeAndAddLabelmapRepresentation.ts deleted file mode 100644 index e38c9bf949..0000000000 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Labelmap/computeAndAddLabelmapRepresentation.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SegmentationRepresentations } from '../../../../enums'; -import { computeAndAddRepresentation } from '../computeAndAddRepresentation'; -import { computeLabelmapData } from './labelmapComputationStrategies'; -import type { PolySegConversionOptions } from '../../../../types'; -import { defaultSegmentationStateManager } from '../../SegmentationStateManager'; -import { triggerSegmentationDataModified } from '../../triggerSegmentationEvents'; - -/** - * Computes and adds the labelmap representation for a given segmentation. - * - * @param segmentationId - The id of the segmentation - * @param options - Optional parameters for computing the labelmap representation - * @returns A promise that resolves when the labelmap representation is computed and added - */ -export async function computeAndAddLabelmapRepresentation( - segmentationId: string, - options: PolySegConversionOptions = {} -) { - return computeAndAddRepresentation( - segmentationId, - SegmentationRepresentations.Labelmap, - () => computeLabelmapData(segmentationId, options), - () => null, - () => { - defaultSegmentationStateManager.processLabelmapRepresentationAddition( - options.viewport.id, - segmentationId - ); - - /// need to figure out how to trigger the labelmap update properly - setTimeout(() => { - triggerSegmentationDataModified(segmentationId); - }, 0); - } - ); -} diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/Surface/computeAndAddSurfaceRepresentation.ts b/packages/tools/src/stateManagement/segmentation/polySeg/Surface/computeAndAddSurfaceRepresentation.ts deleted file mode 100644 index 63d413ae48..0000000000 --- a/packages/tools/src/stateManagement/segmentation/polySeg/Surface/computeAndAddSurfaceRepresentation.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SegmentationRepresentations } from '../../../../enums'; -import type { PolySegConversionOptions } from '../../../../types'; -import { computeAndAddRepresentation } from '../computeAndAddRepresentation'; -import { computeSurfaceData } from './surfaceComputationStrategies'; -import { updateSurfaceData } from './updateSurfaceData'; - -/** - * Computes and adds a surface representation for a given segmentation. - * - * @param segmentationId - The id of the segmentation - * @param options - Optional parameters for computing the surface representation - * @returns A promise that resolves when the surface representation is computed and added - */ -export function computeAndAddSurfaceRepresentation( - segmentationId: string, - options: PolySegConversionOptions = {} -) { - return computeAndAddRepresentation( - segmentationId, - SegmentationRepresentations.Surface, - () => computeSurfaceData(segmentationId, options), - () => updateSurfaceData(segmentationId) - ); -} diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/index.ts b/packages/tools/src/stateManagement/segmentation/polySeg/index.ts deleted file mode 100644 index bbcdcdd635..0000000000 --- a/packages/tools/src/stateManagement/segmentation/polySeg/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { computeAndAddSurfaceRepresentation } from './Surface/computeAndAddSurfaceRepresentation'; -import { computeAndAddLabelmapRepresentation } from './Labelmap/computeAndAddLabelmapRepresentation'; -import { computeAndAddContourRepresentation } from './Contour/computeAndAddContourRepresentation'; -import { canComputeRequestedRepresentation } from './canComputeRequestedRepresentation'; - -export { - canComputeRequestedRepresentation, - // computed representations - computeAndAddSurfaceRepresentation, - computeAndAddLabelmapRepresentation, - computeAndAddContourRepresentation, -}; diff --git a/packages/tools/src/tools/CrosshairsTool.ts b/packages/tools/src/tools/CrosshairsTool.ts index ed2244c8a2..869dabb0dd 100644 --- a/packages/tools/src/tools/CrosshairsTool.ts +++ b/packages/tools/src/tools/CrosshairsTool.ts @@ -109,9 +109,6 @@ class CrosshairsTool extends AnnotationTool { _getReferenceLineControllable?: (viewportId: string) => boolean; _getReferenceLineDraggableRotatable?: (viewportId: string) => boolean; _getReferenceLineSlabThicknessControlsOn?: (viewportId: string) => boolean; - editData: { - annotation: Annotation; - } | null; constructor( toolProps: PublicToolProps = {}, diff --git a/packages/tools/src/tools/annotation/KeyImageTool.ts b/packages/tools/src/tools/annotation/KeyImageTool.ts index 61dbd7982a..8705f3adc4 100644 --- a/packages/tools/src/tools/annotation/KeyImageTool.ts +++ b/packages/tools/src/tools/annotation/KeyImageTool.ts @@ -13,21 +13,27 @@ import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state'; -import { drawArrow as drawArrowSvg } from '../../drawingSvg'; +import { + drawArrow as drawArrowSvg, + drawHandles as drawHandlesSvg, +} from '../../drawingSvg'; import { state } from '../../store/state'; import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters'; import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnotationRenderForViewportIds'; -import { resetElementCursor } from '../../cursors/elementCursor'; +import { + resetElementCursor, + hideElementCursor, +} from '../../cursors/elementCursor'; import type { EventTypes, - ToolHandle, PublicToolProps, ToolProps, SVGDrawingHelper, Annotation, } from '../../types'; +import type { KeyImageAnnotation } from '../../types/ToolSpecificAnnotationTypes'; import type { StyleSpecifier } from '../../types/AnnotationStyle'; type Point2 = Types.Point2; @@ -35,17 +41,23 @@ type Point2 = Types.Point2; class KeyImageTool extends AnnotationTool { static toolName = 'KeyImage'; + /** A mix in data element to set the series level annotation */ + public static dataSeries = { + data: { + seriesLevel: true, + }, + }; + + /** A mix in data element to set the point to be true. That renders as a point + * on the image rather than just selecting the image itself + */ + public static dataPoint = { + data: { + isPoint: true, + }, + }; + _throttledCalculateCachedStats: Function; - editData: { - annotation: Annotation; - viewportIdsToRender: string[]; - handleIndex?: number; - movingTextBox?: boolean; - newAnnotation?: boolean; - hasMoved?: boolean; - } | null; - isDrawing: boolean; - isHandleOutsideImage: boolean; constructor( toolProps: PublicToolProps = {}, @@ -56,6 +68,11 @@ class KeyImageTool extends AnnotationTool { changeTextCallback, canvasPosition: [10, 10], canvasSize: 10, + handleRadius: '6', + /** If true, this selects the entire series/display set */ + seriesLevel: false, + /** If true, shows the point selected */ + isPoint: false, }, } ) { @@ -72,13 +89,20 @@ class KeyImageTool extends AnnotationTool { */ addNewAnnotation = (evt: EventTypes.InteractionEventType) => { const eventDetail = evt.detail; - const { element } = eventDetail; + const { element, currentPoints } = eventDetail; const enabledElement = getEnabledElement(element); const { viewport } = enabledElement; + const worldPos = currentPoints.world; const annotation = (( this.constructor - )).createAnnotationForViewport(viewport); + )).createAnnotationForViewport(viewport, { + data: { + handles: { points: [[...worldPos]] }, + seriesLevel: this.configuration.seriesLevel, + isPoint: this.configuration.isPoint, + }, + }); addAnnotation(annotation, element); @@ -110,10 +134,6 @@ class KeyImageTool extends AnnotationTool { return annotation; }; - public cancel() { - // No op - the annotation can't be in a partial state - } - /** * It returns if the canvas point is near the provided length annotation in the provided * element or not. A proximity is passed to the function to determine the @@ -135,6 +155,10 @@ class KeyImageTool extends AnnotationTool { const { viewport } = enabledElement; const { data } = annotation; + if (!data?.isPoint) { + return false; + } + const { canvasPosition, canvasSize } = this.configuration; if (!canvasPosition?.length) { return false; @@ -161,20 +185,79 @@ class KeyImageTool extends AnnotationTool { handleSelectedCallback( evt: EventTypes.InteractionEventType, - annotation: Annotation, - handle: ToolHandle + annotation: KeyImageAnnotation ): void { - // Nothing special to do here. + const eventDetail = evt.detail; + const { element } = eventDetail; + + annotation.highlighted = true; + + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName() + ); + + // Find viewports to render on drag. + + this.editData = { + //handle, // This would be useful for other tools with more than one handle + annotation, + viewportIdsToRender, + }; + this._activateModify(element); + + hideElementCursor(element); + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + + evt.preventDefault(); + } + + public static setPoint( + annotation, + isPoint: boolean = !annotation.data.isPoint, + element? + ) { + annotation.data.isPoint = isPoint; + triggerAnnotationModified(annotation, element); } _endCallback = (evt: EventTypes.InteractionEventType): void => { const eventDetail = evt.detail; const { element } = eventDetail; - this.doneEditMemo(); + const { annotation, viewportIdsToRender, newAnnotation } = this.editData; + + const { viewportId, renderingEngine } = getEnabledElement(element); + this.eventDispatchDetail = { + viewportId, + renderingEngineId: renderingEngine.id, + }; this._deactivateModify(element); + resetElementCursor(element); + + if (newAnnotation) { + this.createMemo(element, annotation, { newAnnotation }); + } + + this.editData = null; + this.isDrawing = false; + this.doneEditMemo(); + + if ( + this.isHandleOutsideImage && + this.configuration.preventHandleOutsideImage + ) { + removeAnnotation(annotation.annotationUID); + } + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + + if (newAnnotation) { + triggerAnnotationCompleted(annotation); + } }; doubleClickCallback = (evt: EventTypes.TouchTapEventType): void => { @@ -226,9 +309,6 @@ class KeyImageTool extends AnnotationTool { _doneChangingTextCallback(element, annotation, updatedText): void { annotation.data.text = updatedText; - const enabledElement = getEnabledElement(element); - const { renderingEngine } = enabledElement; - const viewportIdsToRender = getViewportIdsWithToolToRender( element, this.getToolName() @@ -239,48 +319,69 @@ class KeyImageTool extends AnnotationTool { triggerAnnotationModified(annotation, element); } - _activateModify = (element: HTMLDivElement) => { + _dragCallback = (evt) => { + this.isDrawing = true; + const eventDetail = evt.detail; + const { currentPoints, element } = eventDetail; + const worldPos = currentPoints.world; + + const { annotation, viewportIdsToRender, newAnnotation } = this.editData; + const { data } = annotation; + + this.createMemo(element, annotation, { newAnnotation }); + + data.handles.points[0] = [...worldPos] as Types.Point3; + annotation.invalidated = true; + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + }; + + public cancel(element: HTMLDivElement) { + // If it is mid-draw or mid-modify + if (this.isDrawing) { + this.isDrawing = false; + this._deactivateModify(element); + resetElementCursor(element); + + const { annotation, viewportIdsToRender, newAnnotation } = this.editData; + const { data } = annotation; + + annotation.highlighted = false; + data.handles.activeHandleIndex = null; + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + + if (newAnnotation) { + triggerAnnotationCompleted(annotation); + } + + this.editData = null; + return annotation.annotationUID; + } + } + + _activateModify = (element) => { state.isInteractingWithTool = true; - element.addEventListener( - Events.MOUSE_UP, - this._endCallback as EventListener - ); - element.addEventListener( - Events.MOUSE_CLICK, - this._endCallback as EventListener - ); + element.addEventListener(Events.MOUSE_UP, this._endCallback); + element.addEventListener(Events.MOUSE_DRAG, this._dragCallback); + element.addEventListener(Events.MOUSE_CLICK, this._endCallback); - element.addEventListener( - Events.TOUCH_TAP, - this._endCallback as EventListener - ); - element.addEventListener( - Events.TOUCH_END, - this._endCallback as EventListener - ); + element.addEventListener(Events.TOUCH_END, this._endCallback); + element.addEventListener(Events.TOUCH_DRAG, this._dragCallback); + element.addEventListener(Events.TOUCH_TAP, this._endCallback); }; - _deactivateModify = (element: HTMLDivElement) => { + _deactivateModify = (element) => { state.isInteractingWithTool = false; - element.removeEventListener( - Events.MOUSE_UP, - this._endCallback as EventListener - ); - element.removeEventListener( - Events.MOUSE_CLICK, - this._endCallback as EventListener - ); + element.removeEventListener(Events.MOUSE_UP, this._endCallback); + element.removeEventListener(Events.MOUSE_DRAG, this._dragCallback); + element.removeEventListener(Events.MOUSE_CLICK, this._endCallback); - element.removeEventListener( - Events.TOUCH_TAP, - this._endCallback as EventListener - ); - element.removeEventListener( - Events.TOUCH_END, - this._endCallback as EventListener - ); + element.removeEventListener(Events.TOUCH_END, this._endCallback); + element.removeEventListener(Events.TOUCH_DRAG, this._dragCallback); + element.removeEventListener(Events.TOUCH_TAP, this._endCallback); }; /** @@ -324,18 +425,33 @@ class KeyImageTool extends AnnotationTool { // Draw SVG for (let i = 0; i < annotations.length; i++) { const annotation = annotations[i]; - const { annotationUID } = annotation; + const { annotationUID, data } = annotation; styleSpecifier.annotationUID = annotationUID; - const { color } = this.getAnnotationStyle({ + const { color, lineWidth } = this.getAnnotationStyle({ annotation, styleSpecifier, }); const { canvasPosition, canvasSize } = this.configuration; - if (canvasPosition?.length) { - const arrowUID = '1'; + const arrowUID = '1'; + if (data?.isPoint) { + const point = data.handles.points[0]; + const canvasCoordinates = viewport.worldToCanvas(point); + + drawHandlesSvg( + svgDrawingHelper, + annotationUID, + arrowUID, + [canvasCoordinates], + { + color, + lineWidth, + handleRadius: this.configuration.handleRadius, + } + ); + } else if (canvasPosition?.length) { drawArrowSvg( svgDrawingHelper, annotationUID, diff --git a/packages/tools/src/tools/annotation/ProbeTool.ts b/packages/tools/src/tools/annotation/ProbeTool.ts index 013ea23c6f..fdf047b1ec 100644 --- a/packages/tools/src/tools/annotation/ProbeTool.ts +++ b/packages/tools/src/tools/annotation/ProbeTool.ts @@ -39,7 +39,6 @@ import type { ToolHandle, PublicToolProps, SVGDrawingHelper, - Annotation, } from '../../types'; import type { ProbeAnnotation } from '../../types/ToolSpecificAnnotationTypes'; import type { StyleSpecifier } from '../../types/AnnotationStyle'; @@ -94,18 +93,6 @@ const { transformWorldToIndex } = csUtils; class ProbeTool extends AnnotationTool { static toolName = 'Probe'; - editData: { - annotation: Annotation; - viewportIdsToRender: string[]; - newAnnotation?: boolean; - } | null; - eventDispatchDetail: { - viewportId: string; - renderingEngineId: string; - }; - isDrawing: boolean; - isHandleOutsideImage: boolean; - public static probeDefaults = { supportedInteractionTypes: ['Mouse', 'Touch'], configuration: { diff --git a/packages/tools/src/tools/base/AnnotationTool.ts b/packages/tools/src/tools/base/AnnotationTool.ts index 12e82f77ef..db9d22a616 100644 --- a/packages/tools/src/tools/base/AnnotationTool.ts +++ b/packages/tools/src/tools/base/AnnotationTool.ts @@ -51,6 +51,21 @@ const { PointsManager } = csUtils; * abstract methods. */ abstract class AnnotationTool extends AnnotationDisplayTool { + protected eventDispatchDetail: { + viewportId: string; + renderingEngineId: string; + }; + isDrawing: boolean; + isHandleOutsideImage: boolean; + editData: { + annotation: Annotation; + viewportIdsToRender?: string[]; + newAnnotation?: boolean; + handleIndex?: number; + movingTextBox?: boolean; + hasMoved?: boolean; + } | null; + /** * Creates a base annotation object, adding in any annotation base data provided */ diff --git a/packages/tools/src/tools/displayTools/Contour/contourDisplay.ts b/packages/tools/src/tools/displayTools/Contour/contourDisplay.ts index 8545f30a39..4f2fd5cd34 100644 --- a/packages/tools/src/tools/displayTools/Contour/contourDisplay.ts +++ b/packages/tools/src/tools/displayTools/Contour/contourDisplay.ts @@ -4,10 +4,10 @@ import { getEnabledElementByViewportId } from '@cornerstonejs/core'; import Representations from '../../../enums/SegmentationRepresentations'; import { handleContourSegmentation } from './contourHandler/handleContourSegmentation'; import { getSegmentation } from '../../../stateManagement/segmentation/getSegmentation'; -import { canComputeRequestedRepresentation } from '../../../stateManagement/segmentation/polySeg/canComputeRequestedRepresentation'; -import { computeAndAddContourRepresentation } from '../../../stateManagement/segmentation/polySeg/Contour/computeAndAddContourRepresentation'; import type { ContourRepresentation } from '../../../types/SegmentationStateTypes'; import removeContourFromElement from './removeContourFromElement'; +import { getPolySeg } from '../../../config'; +import { computeAndAddRepresentation } from '../../../utilities/segmentation/computeAndAddRepresentation'; let polySegConversionInProgress = false; @@ -71,7 +71,7 @@ async function render( if ( !contourData && - canComputeRequestedRepresentation( + getPolySeg()?.canComputeRequestedRepresentation( segmentationId, Representations.Contour ) && @@ -79,11 +79,20 @@ async function render( ) { polySegConversionInProgress = true; - contourData = await computeAndAddContourRepresentation(segmentationId, { - viewport, - }); + const polySeg = getPolySeg(); + + contourData = await computeAndAddRepresentation( + segmentationId, + Representations.Contour, + () => polySeg.computeContourData(segmentationId, { viewport }), + () => undefined + ); polySegConversionInProgress = false; + } else if (!contourData && !getPolySeg()) { + console.debug( + `No contour data found for segmentationId ${segmentationId} and PolySeg add-on is not configured. Unable to convert from other representations to contour. Please register PolySeg using cornerstoneTools.init({ addons: { polySeg } }) to enable automatic conversion.` + ); } if (!contourData) { diff --git a/packages/tools/src/tools/displayTools/Labelmap/index.ts b/packages/tools/src/tools/displayTools/Labelmap/index.ts index 071711dad1..755409555e 100644 --- a/packages/tools/src/tools/displayTools/Labelmap/index.ts +++ b/packages/tools/src/tools/displayTools/Labelmap/index.ts @@ -3,7 +3,7 @@ import labelmapConfig from './labelmapConfig'; import { validate as validateLabelmap, validatePublic as validateLabelmapPublic, -} from './validateLabelmap'; +} from '../../../utilities/segmentation/validateLabelmap'; export { labelmapDisplay, diff --git a/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts b/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts index 6e4e1ad96b..43643c7519 100644 --- a/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts +++ b/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts @@ -20,8 +20,6 @@ import { getActiveSegmentation } from '../../../stateManagement/segmentation/act import { getColorLUT } from '../../../stateManagement/segmentation/getColorLUT'; import { getCurrentLabelmapImageIdForViewport } from '../../../stateManagement/segmentation/getCurrentLabelmapImageIdForViewport'; import { getSegmentation } from '../../../stateManagement/segmentation/getSegmentation'; -import { canComputeRequestedRepresentation } from '../../../stateManagement/segmentation/polySeg/canComputeRequestedRepresentation'; -import { computeAndAddLabelmapRepresentation } from '../../../stateManagement/segmentation/polySeg/Labelmap/computeAndAddLabelmapRepresentation'; import type vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; import type vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; import { segmentationStyle } from '../../../stateManagement/segmentation/SegmentationStyle'; @@ -30,6 +28,10 @@ import { internalGetHiddenSegmentIndices } from '../../../stateManagement/segmen import { getActiveSegmentIndex } from '../../../stateManagement/segmentation/getActiveSegmentIndex'; import type vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume'; import { getLabelmapActorEntry } from '../../../stateManagement/segmentation/helpers/getSegmentationActor'; +import { getPolySeg } from '../../../config'; +import { computeAndAddRepresentation } from '../../../utilities/segmentation/computeAndAddRepresentation'; +import { triggerSegmentationDataModified } from '../../../stateManagement/segmentation/triggerSegmentationEvents'; +import { defaultSegmentationStateManager } from '../../../stateManagement/segmentation/SegmentationStateManager'; // 255 itself is used as preview color, so basically // we have 254 colors to use for the segments if we are using the preview. @@ -102,7 +104,7 @@ async function render( if ( !labelmapData && - canComputeRequestedRepresentation( + getPolySeg()?.canComputeRequestedRepresentation( segmentationId, SegmentationRepresentations.Labelmap ) && @@ -115,9 +117,25 @@ async function render( // underlying representations to Surface polySegConversionInProgress = true; - labelmapData = await computeAndAddLabelmapRepresentation(segmentationId, { - viewport, - }); + const polySeg = getPolySeg(); + + labelmapData = await computeAndAddRepresentation( + segmentationId, + SegmentationRepresentations.Labelmap, + () => polySeg.computeLabelmapData(segmentationId, { viewport }), + () => null, + () => { + defaultSegmentationStateManager.processLabelmapRepresentationAddition( + viewport.id, + segmentationId + ); + + /// need to figure out how to trigger the labelmap update properly + setTimeout(() => { + triggerSegmentationDataModified(segmentationId); + }, 0); + } + ); if (!labelmapData) { throw new Error( @@ -126,6 +144,10 @@ async function render( } polySegConversionInProgress = false; + } else if (!labelmapData && !getPolySeg()) { + console.debug( + `No labelmap data found for segmentationId ${segmentationId} and PolySeg add-on is not configured. Unable to convert from other representations to labelmap. Please register PolySeg using cornerstoneTools.init({ addons: { polySeg } }) to enable automatic conversion.` + ); } if (!labelmapData) { diff --git a/packages/tools/src/tools/displayTools/Surface/surfaceDisplay.ts b/packages/tools/src/tools/displayTools/Surface/surfaceDisplay.ts index b258d05572..de06cfb335 100644 --- a/packages/tools/src/tools/displayTools/Surface/surfaceDisplay.ts +++ b/packages/tools/src/tools/displayTools/Surface/surfaceDisplay.ts @@ -11,10 +11,9 @@ import removeSurfaceFromElement from './removeSurfaceFromElement'; import addOrUpdateSurfaceToElement from './addOrUpdateSurfaceToElement'; import { getSegmentation } from '../../../stateManagement/segmentation/getSegmentation'; import { getColorLUT } from '../../../stateManagement/segmentation/getColorLUT'; -import { canComputeRequestedRepresentation } from '../../../stateManagement/segmentation/polySeg/canComputeRequestedRepresentation'; -import { computeAndAddSurfaceRepresentation } from '../../../stateManagement/segmentation/polySeg/Surface/computeAndAddSurfaceRepresentation'; +import { getPolySeg } from '../../../config'; +import { computeAndAddRepresentation } from '../../../utilities/segmentation/computeAndAddRepresentation'; -const { ViewportType } = Enums; /** * It removes a segmentation representation from the tool group's viewports and * from the segmentation state @@ -68,19 +67,38 @@ async function render( if ( !SurfaceData && - canComputeRequestedRepresentation(segmentationId, Representations.Surface) + getPolySeg()?.canComputeRequestedRepresentation( + segmentationId, + Representations.Surface + ) ) { // we need to check if we can request polySEG to convert the other // underlying representations to Surface - SurfaceData = await computeAndAddSurfaceRepresentation(segmentationId, { - viewport, - }); + const polySeg = getPolySeg(); + + SurfaceData = await computeAndAddRepresentation( + segmentationId, + Representations.Surface, + () => polySeg.computeSurfaceData(segmentationId, { viewport }), + () => polySeg.updateSurfaceData(segmentationId, { viewport }) + ); if (!SurfaceData) { throw new Error( - `No Surface data found for segmentationId ${segmentationId}.` + `No Surface data found for segmentationId ${segmentationId} even we tried to compute it` ); } + } else if (!SurfaceData && !getPolySeg()) { + console.debug( + `No surface data found for segmentationId ${segmentationId} and PolySeg add-on is not configured. Unable to convert from other representations to surface. Please register PolySeg using cornerstoneTools.init({ addons: { polySeg } }) to enable automatic conversion.` + ); + } + + if (!SurfaceData) { + console.warn( + `No Surface data found for segmentationId ${segmentationId}. Skipping render.` + ); + return; } const { geometryIds } = SurfaceData; diff --git a/packages/tools/src/tools/segmentation/BrushTool.ts b/packages/tools/src/tools/segmentation/BrushTool.ts index 2482a991b1..4723f1ee5b 100644 --- a/packages/tools/src/tools/segmentation/BrushTool.ts +++ b/packages/tools/src/tools/segmentation/BrushTool.ts @@ -71,15 +71,8 @@ class BrushTool extends LabelmapBaseTool { THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL: thresholdInsideSphereIsland, }, - - strategySpecificConfiguration: { - THRESHOLD: { - threshold: [-150, -70], // E.g. CT Fat // Only used during threshold strategies. - }, - }, defaultStrategy: 'FILL_INSIDE_CIRCLE', activeStrategy: 'FILL_INSIDE_CIRCLE', - thresholdVolumeId: null, brushSize: 25, preview: { // Have to enable the preview to use this @@ -640,9 +633,7 @@ class BrushTool extends LabelmapBaseTool { } ); - const activeStrategy = this.configuration.activeStrategy; - const { dynamicRadiusInCanvas } = this.configuration - .strategySpecificConfiguration[activeStrategy] || { + const { dynamicRadiusInCanvas } = this.configuration?.threshold || { dynamicRadiusInCanvas: 0, }; diff --git a/packages/tools/src/tools/segmentation/CircleScissorsTool.ts b/packages/tools/src/tools/segmentation/CircleScissorsTool.ts index 7c1fa1bab4..45c5cc85a6 100644 --- a/packages/tools/src/tools/segmentation/CircleScissorsTool.ts +++ b/packages/tools/src/tools/segmentation/CircleScissorsTool.ts @@ -297,7 +297,6 @@ class CircleScissorsTool extends LabelmapBaseTool { points: data.handles.points, viewPlaneNormal, viewUp, - strategySpecificConfiguration: {}, createMemo: this.createMemo.bind(this), }; diff --git a/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts b/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts index 32542b19dc..8154c209c0 100644 --- a/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts +++ b/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts @@ -5,15 +5,11 @@ import { Enums, eventTarget, BaseVolumeViewport, - volumeLoader, } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; import { BaseTool } from '../base'; -import type { - LabelmapSegmentationDataStack, - LabelmapSegmentationDataVolume, -} from '../../types/LabelmapTypes'; +import type { 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'; @@ -21,7 +17,6 @@ import { getLockedSegmentIndices } from '../../stateManagement/segmentation/segm import { getSegmentation } from '../../stateManagement/segmentation/getSegmentation'; import { getClosestImageIdForStackViewport } from '../../utilities/annotationHydration'; import { getCurrentLabelmapImageIdForViewport } from '../../stateManagement/segmentation/getCurrentLabelmapImageIdForViewport'; -import { getStackSegmentationImageIdsForViewport } from '../../stateManagement/segmentation/getStackSegmentationImageIdsForViewport'; import { getSegmentIndexColor } from '../../stateManagement/segmentation/config/segmentationColor'; import { getActiveSegmentIndex } from '../../stateManagement/segmentation/getActiveSegmentIndex'; import { StrategyCallbacks } from '../../enums'; @@ -189,7 +184,6 @@ export default class LabelmapBaseTool extends BaseTool { representationData, segmentsLocked, segmentationId, - volumeOperation = false, }): EditDataReturnType { if (viewport instanceof BaseVolumeViewport) { const { volumeId } = representationData[ @@ -228,7 +222,8 @@ export default class LabelmapBaseTool extends BaseTool { return { volumeId, referencedVolumeId: - this.configuration.thresholdVolumeId ?? referencedVolumeIdToThreshold, + this.configuration.threshold?.volumeId ?? + referencedVolumeIdToThreshold, segmentsLocked, }; } else { @@ -243,67 +238,10 @@ export default class LabelmapBaseTool extends BaseTool { return; } - // I hate this, but what can you do sometimes - if ( - this.configuration.activeStrategy.includes('SPHERE') || - volumeOperation - ) { - const referencedImageIds = viewport.getImageIds(); - const isValidVolumeForSphere = - csUtils.isValidVolume(referencedImageIds); - - if (!isValidVolumeForSphere) { - throw new Error( - 'Volume is not reconstructable for sphere manipulation' - ); - } - - const volumeId = `${segmentationId}_${viewport.id}`; - const volume = cache.getVolume(volumeId); - if (volume) { - return { - imageId: segmentationImageId, - segmentsLocked, - override: { - voxelManager: volume.voxelManager, - imageData: volume.imageData, - }, - }; - } else { - // 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 { - imageId: segmentationImageId, - segmentsLocked, - }; - } - - // it will return the cached volume if it already exists - const volume = volumeLoader.createAndCacheVolumeFromImagesSync( - volumeId, - labelmapImageIds - ); - - return { - imageId: segmentationImageId, - segmentsLocked, - override: { - voxelManager: volume.voxelManager, - imageData: volume.imageData, - }, - }; - } - } else { - return { - imageId: segmentationImageId, - segmentsLocked, - }; - } + return { + imageId: segmentationImageId, + segmentsLocked, + }; } } @@ -389,8 +327,8 @@ export default class LabelmapBaseTool extends BaseTool { toolGroupId: this.toolGroupId, segmentationId, viewUp, - strategySpecificConfiguration: - this.configuration.strategySpecificConfiguration, + activeStrategy: this.configuration.activeStrategy, + configuration: this.configuration, // Provide the preview information so that data can be used directly preview: this._previewData?.preview, createMemo: this.createMemo.bind(this), diff --git a/packages/tools/src/tools/segmentation/RectangleScissorsTool.ts b/packages/tools/src/tools/segmentation/RectangleScissorsTool.ts index fa75ce9835..87f2ffc8cf 100644 --- a/packages/tools/src/tools/segmentation/RectangleScissorsTool.ts +++ b/packages/tools/src/tools/segmentation/RectangleScissorsTool.ts @@ -318,7 +318,6 @@ class RectangleScissorsTool extends LabelmapBaseTool { const operationData = { ...this.editData, points: data.handles.points, - strategySpecificConfiguration: {}, createMemo: this.createMemo.bind(this), }; diff --git a/packages/tools/src/tools/segmentation/SphereScissorsTool.ts b/packages/tools/src/tools/segmentation/SphereScissorsTool.ts index 6bf7a82cff..d004d4eb62 100644 --- a/packages/tools/src/tools/segmentation/SphereScissorsTool.ts +++ b/packages/tools/src/tools/segmentation/SphereScissorsTool.ts @@ -184,7 +184,6 @@ class SphereScissorsTool extends LabelmapBaseTool { representationData, segmentsLocked, segmentationId, - volumeOperation: true, }); this.editData = { diff --git a/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts b/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts index 663dee7a09..f7f953a0f0 100644 --- a/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts +++ b/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts @@ -33,8 +33,20 @@ export type InitializedOperationData = LabelmapToolOperationDataAny & { previewSegmentIndex?: number; brushStrategy: BrushStrategy; + activeStrategy: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any - configuration?: Record; + configuration?: { + [key: string]: unknown; + centerSegmentIndex?: { + segmentIndex: number; + }; + threshold?: { + range?: number[]; + isDynamic: boolean; + dynamicRadius: number; + dynamicRadiusInCanvas?: number; + }; + }; memo?: LabelmapMemo; }; @@ -66,13 +78,13 @@ export type Composition = CompositionFunction | CompositionInstance; * * These combine to form an actual brush: * - * Circle - convexFill, defaultSetValue, inEllipse/boundingbox ellipse, empty threshold - * Rectangle - - convexFill, defaultSetValue, inRectangle/boundingbox rectangle, empty threshold + * Circle - convexFill, defaultSetValue, inEllipse/bounding box ellipse, empty threshold + * Rectangle - - convexFill, defaultSetValue, inRectangle/bounding box rectangle, empty threshold * might also get parameter values from input, init for setup of convexFill * * The pieces are combined to generate a strategyFunction, which performs * the actual strategy operation, as well as various callbacks for the strategy - * to allow more control over behaviour in the specific strategy (such as displaying + * to allow more control over behavior in the specific strategy (such as displaying * preview) */ @@ -119,6 +131,13 @@ export default class BrushStrategy { [StrategyCallbacks.ComputeInnerCircleRadius]: addListMethod( StrategyCallbacks.ComputeInnerCircleRadius ), + [StrategyCallbacks.EnsureSegmentationVolumeFor3DManipulation]: + addListMethod( + StrategyCallbacks.EnsureSegmentationVolumeFor3DManipulation + ), + [StrategyCallbacks.EnsureImageVolumeFor3DManipulation]: addListMethod( + StrategyCallbacks.EnsureImageVolumeFor3DManipulation + ), [StrategyCallbacks.AddPreview]: addListMethod(StrategyCallbacks.AddPreview), [StrategyCallbacks.GetStatistics]: addSingletonMethod( StrategyCallbacks.GetStatistics @@ -182,13 +201,13 @@ export default class BrushStrategy { return; } - const { strategySpecificConfiguration = {}, centerIJK } = initializedData; + const { configuration = {}, centerIJK } = initializedData; // Store the center IJK location so that we can skip an immediate same-point update // TODO - move this to the BrushTool - if (csUtils.isEqual(centerIJK, strategySpecificConfiguration.centerIJK)) { + if (csUtils.isEqual(centerIJK, configuration.centerIJK)) { return operationData.preview; } else { - strategySpecificConfiguration.centerIJK = centerIJK; + configuration.centerIJK = centerIJK; } this._fill.forEach((func) => func(initializedData)); @@ -227,7 +246,9 @@ export default class BrushStrategy { operationName?: string ): InitializedOperationData { const { viewport } = enabledElement; - const data = getStrategyData({ operationData, viewport }); + + // pass in the strategy to getStrategyData + const data = getStrategyData({ operationData, viewport, strategy: this }); if (!data) { console.warn('No data found for BrushStrategy'); @@ -240,14 +261,10 @@ export default class BrushStrategy { segmentationImageData, } = data; - const segmentationVoxelManagerToUse = - operationData.override?.voxelManager || segmentationVoxelManager; - const segmentationImageDataToUse = - operationData.override?.imageData || segmentationImageData; - const previewVoxelManager = operationData.preview?.previewVoxelManager || VoxelManager.createRLEHistoryVoxelManager(segmentationVoxelManager); + const previewEnabled = !!operationData.previewColors; const previewSegmentIndex = previewEnabled ? 255 : undefined; @@ -257,8 +274,8 @@ export default class BrushStrategy { ...operationData, enabledElement, imageVoxelManager, - segmentationVoxelManager: segmentationVoxelManagerToUse, - segmentationImageData: segmentationImageDataToUse, + segmentationVoxelManager, + segmentationImageData, previewVoxelManager, viewport, centerWorld: null, @@ -268,7 +285,6 @@ export default class BrushStrategy { }; this._initialize.forEach((func) => func(initializedData)); - return initializedData; } diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/determineSegmentIndex.ts b/packages/tools/src/tools/segmentation/strategies/compositions/determineSegmentIndex.ts index 148181e628..feff8e3ad5 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/determineSegmentIndex.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/determineSegmentIndex.ts @@ -20,14 +20,13 @@ import type { Types } from '@cornerstonejs/core'; */ export default { [StrategyCallbacks.Initialize]: (operationData: InitializedOperationData) => { - const { strategySpecificConfiguration } = operationData; - if (!strategySpecificConfiguration) { + const { centerSegmentIndex } = operationData.configuration || {}; + + if (!centerSegmentIndex) { return; } - const { centerSegmentIndex } = strategySpecificConfiguration; - if (centerSegmentIndex) { - operationData.segmentIndex = centerSegmentIndex.segmentIndex; - } + + operationData.segmentIndex = centerSegmentIndex.segmentIndex; }, [StrategyCallbacks.OnInteractionStart]: ( @@ -38,16 +37,17 @@ export default { previewSegmentIndex, segmentationVoxelManager, centerIJK, - strategySpecificConfiguration, viewPlaneNormal, segmentationImageData, preview, + configuration, } = operationData; - if (!strategySpecificConfiguration?.useCenterSegmentIndex) { + + if (!configuration?.useCenterSegmentIndex) { return; } // Get rid of the previous data - delete strategySpecificConfiguration.centerSegmentIndex; + delete configuration.centerSegmentIndex; let hasSegmentIndex = false; let hasPreviewIndex = false; @@ -91,7 +91,7 @@ export default { existingValue = null; } operationData.segmentIndex = existingValue; - strategySpecificConfiguration.centerSegmentIndex = { + configuration.centerSegmentIndex = { segmentIndex: existingValue, }; }, diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts b/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts index 1a801a9f71..57c5706c64 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts @@ -17,15 +17,14 @@ export default { const { operationName, centerIJK, - strategySpecificConfiguration, segmentationVoxelManager, imageVoxelManager, + configuration, segmentIndex, viewport, } = operationData; - const { THRESHOLD } = strategySpecificConfiguration; - if (!THRESHOLD?.isDynamic || !centerIJK || !segmentIndex) { + if (!configuration?.threshold?.isDynamic || !centerIJK || !segmentIndex) { return; } if ( @@ -36,7 +35,7 @@ export default { } const boundsIJK = segmentationVoxelManager.getBoundsIJK(); - const { threshold: oldThreshold, dynamicRadius = 0 } = THRESHOLD; + const { range: oldThreshold, dynamicRadius = 0 } = configuration.threshold; const useDelta = oldThreshold ? 0 : dynamicRadius; const { viewPlaneNormal } = viewport.getCamera(); @@ -73,35 +72,39 @@ export default { }; imageVoxelManager.forEach(callback, { boundsIJK: nestedBounds }); - operationData.strategySpecificConfiguration.THRESHOLD.threshold = threshold; + configuration.threshold.range = threshold; }, // Setup a clear threshold value on mouse/touch down [StrategyCallbacks.OnInteractionStart]: ( operationData: InitializedOperationData ) => { - const { strategySpecificConfiguration, preview } = operationData; - if (!strategySpecificConfiguration?.THRESHOLD?.isDynamic && !preview) { + const { configuration } = operationData; + + if (!configuration?.threshold?.isDynamic) { return; } - strategySpecificConfiguration.THRESHOLD.threshold = null; + + configuration.threshold.range = null; }, /** * It computes the inner circle radius in canvas coordinates and stores it - * in the strategySpecificConfiguration. This is used to show the user - * the area that is used to compute the threshold. + * This is used to show the user the area that is used to compute the threshold. */ [StrategyCallbacks.ComputeInnerCircleRadius]: ( operationData: InitializedOperationData ) => { const { configuration, viewport } = operationData; - const { THRESHOLD: { dynamicRadius = 0 } = {} } = - configuration.strategySpecificConfiguration || {}; + const { dynamicRadius = 0, isDynamic } = configuration.threshold; + + if (!isDynamic) { + configuration.threshold.dynamicRadiusInCanvas = 0; + return; + } if (dynamicRadius === 0) { return; } - // @ts-ignore const imageData = viewport.getImageData(); if (!imageData) { @@ -125,18 +128,12 @@ export default { centerCanvas[0] - offSetCenterCanvas[0] ); - // this is a bit of a hack, since we have switched to using THRESHOLD - // as strategy but really strategy names are CIRCLE_THRESHOLD and SPHERE_THRESHOLD - // and we can't really change the name of the strategy in the configuration - const { strategySpecificConfiguration, activeStrategy } = configuration; - - if (!strategySpecificConfiguration[activeStrategy]) { - strategySpecificConfiguration[activeStrategy] = {}; + if (!configuration.threshold.dynamicRadiusInCanvas) { + configuration.threshold.dynamicRadiusInCanvas = 0; } // Add a couple of pixels to the radius to make it more obvious what is // included. - strategySpecificConfiguration[activeStrategy].dynamicRadiusInCanvas = - 3 + dynamicRadiusInCanvas; + configuration.threshold.dynamicRadiusInCanvas = 3 + dynamicRadiusInCanvas; }, }; diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/ensureImageVolume.ts b/packages/tools/src/tools/segmentation/strategies/compositions/ensureImageVolume.ts new file mode 100644 index 0000000000..83324c41f8 --- /dev/null +++ b/packages/tools/src/tools/segmentation/strategies/compositions/ensureImageVolume.ts @@ -0,0 +1,50 @@ +import { cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core'; +import StrategyCallbacks from '../../../../enums/StrategyCallbacks'; +import { getSegmentation } from '../../../../stateManagement/segmentation/getSegmentation'; +import type { LabelmapSegmentationDataStack } from '../../../../types'; + +export default { + [StrategyCallbacks.EnsureImageVolumeFor3DManipulation]: (data) => { + const { operationData, viewport } = data; + + let referencedImageIds; + if (viewport) { + const referencedImageIds = viewport.getImageIds(); + const isValidVolumeForSphere = csUtils.isValidVolume(referencedImageIds); + if (!isValidVolumeForSphere) { + throw new Error( + 'Volume is not reconstructable for sphere manipulation' + ); + } + } else { + const segmentation = getSegmentation(operationData.segmentationId); + const imageIds = ( + segmentation.representationData + .Labelmap as LabelmapSegmentationDataStack + ).imageIds; + + referencedImageIds = imageIds.map((imageId) => { + const image = cache.getImage(imageId); + return image.referencedImageId; + }); + } + + const volumeId = cache.generateVolumeId(referencedImageIds); + + let imageVolume = cache.getVolume(volumeId); + if (imageVolume) { + operationData.imageVoxelManager = imageVolume.voxelManager; + operationData.imageData = imageVolume.imageData; + return; + } + + // it will return the cached volume if it already exists + imageVolume = volumeLoader.createAndCacheVolumeFromImagesSync( + volumeId, + referencedImageIds + ); + + operationData.imageVoxelManager = imageVolume.voxelManager; + operationData.imageData = imageVolume.imageData; + }, +}; diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/ensureSegmentationVolume.ts b/packages/tools/src/tools/segmentation/strategies/compositions/ensureSegmentationVolume.ts new file mode 100644 index 0000000000..03f6ec93cc --- /dev/null +++ b/packages/tools/src/tools/segmentation/strategies/compositions/ensureSegmentationVolume.ts @@ -0,0 +1,26 @@ +import { utilities, cache } from '@cornerstonejs/core'; +import StrategyCallbacks from '../../../../enums/StrategyCallbacks'; +import getOrCreateSegmentationVolume from '../../../../utilities/segmentation/getOrCreateSegmentationVolume'; + +export default { + [StrategyCallbacks.EnsureSegmentationVolumeFor3DManipulation]: (data) => { + const { operationData, viewport } = data; + const { segmentationId, imageIds: segImageIds } = operationData; + + // Get referenced image IDs from viewport or from segmentation image IDs + const referencedImageIds = viewport + ? viewport.getImageIds() + : segImageIds.map((imageId) => cache.getImage(imageId).referencedImageId); + + const isValidVolumeForSphere = utilities.isValidVolume(referencedImageIds); + if (!isValidVolumeForSphere) { + throw new Error('Volume is not reconstructable for sphere manipulation'); + } + + const segVolume = getOrCreateSegmentationVolume(segmentationId); + + operationData.segmentationVoxelManager = segVolume.voxelManager; + operationData.segmentationImageData = segVolume.imageData; + return; + }, +}; diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/index.ts b/packages/tools/src/tools/segmentation/strategies/compositions/index.ts index d809b2c8f8..a1fdf3c894 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/index.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/index.ts @@ -7,7 +7,8 @@ import regionFill from './regionFill'; import setValue from './setValue'; import threshold from './threshold'; import labelmapStatistics from './labelmapStatistics'; -import labelmapInterpolation from './labelmapInterpolation'; +import ensureSegmentationVolumeFor3DManipulation from './ensureSegmentationVolume'; +import ensureImageVolumeFor3DManipulation from './ensureImageVolume'; export default { determineSegmentIndex, @@ -19,5 +20,6 @@ export default { setValue, threshold, labelmapStatistics, - labelmapInterpolation, + ensureSegmentationVolumeFor3DManipulation, + ensureImageVolumeFor3DManipulation, }; diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/islandRemovalComposition.ts b/packages/tools/src/tools/segmentation/strategies/compositions/islandRemovalComposition.ts index a3a7cf90aa..8c68211221 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/islandRemovalComposition.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/islandRemovalComposition.ts @@ -15,15 +15,18 @@ export default { operationData: InitializedOperationData ) => { const { - strategySpecificConfiguration, previewSegmentIndex, segmentIndex, viewport, previewVoxelManager, segmentationVoxelManager, + activeStrategy, } = operationData; - if (!strategySpecificConfiguration.THRESHOLD || segmentIndex === null) { + if ( + activeStrategy !== 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL' || + segmentIndex === null + ) { return; } diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/labelmapInterpolation.ts b/packages/tools/src/tools/segmentation/strategies/compositions/labelmapInterpolation.ts deleted file mode 100644 index 6fc650fced..0000000000 --- a/packages/tools/src/tools/segmentation/strategies/compositions/labelmapInterpolation.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { utilities, peerImport } from '@cornerstonejs/core'; -import type { InitializedOperationData } from '../BrushStrategy'; -import StrategyCallbacks from '../../../../enums/StrategyCallbacks'; -import getItkImage from '../utils/getItkImage'; -import { triggerSegmentationDataModified } from '../../../../stateManagement/segmentation/triggerSegmentationEvents'; -import PreviewMethods from './preview'; - -type MorphologicalContourInterpolationOptions = { - label?: number; - axis?: number; - noHeuristicAlignment?: boolean; - noUseDistanceTransform?: boolean; - useCustomSlicePositions?: boolean; -}; - -/** - * Adds an isWithinThreshold to the operation data that checks that the - * image value is within threshold[0]...threshold[1] - * No-op if threshold not defined. - */ -export default { - [StrategyCallbacks.Interpolate]: async ( - operationData: InitializedOperationData, - configuration: MorphologicalContourInterpolationOptions - ) => { - const { - segmentationImageData, - segmentIndex, - preview, - segmentationVoxelManager, - previewSegmentIndex, - previewVoxelManager, - } = operationData; - - if (preview) { - // Mark everything as segment index value so the interpolation works - const callback = ({ index }) => { - segmentationVoxelManager.setAtIndex(index, segmentIndex); - }; - previewVoxelManager.forEach(callback); - } - - let itkModule; - try { - // Use peerImport instead of dynamic import - itkModule = await peerImport( - '@itk-wasm/morphological-contour-interpolation' - ); - if (!itkModule) { - throw new Error('Module not found'); - } - } catch (error) { - console.warn( - "Warning: '@itk-wasm/morphological-contour-interpolation' module not found. Please install it separately." - ); - return operationData; - } - - let inputImage; - try { - inputImage = await getItkImage(segmentationImageData, 'interpolation'); - if (!inputImage) { - throw new Error('Failed to get ITK image'); - } - } catch (error) { - console.warn('Warning: Failed to get ITK image for interpolation'); - return operationData; - } - - const outputPromise = itkModule.morphologicalContourInterpolation( - inputImage, - { - ...configuration, - label: segmentIndex, - webWorker: false, - } - ); - outputPromise.then((value) => { - const { outputImage } = value; - - const previewColors = operationData.configuration?.preview?.previewColors; - const assignIndex = - previewSegmentIndex ?? (previewColors ? 255 : segmentIndex); - // Reset the colors - needs operation data set to do this - operationData.previewColors ||= previewColors; - operationData.previewSegmentIndex ||= previewColors ? 255 : undefined; - PreviewMethods[StrategyCallbacks.Initialize](operationData); - - segmentationVoxelManager.forEach(({ value: originalValue, index }) => { - const newValue = outputImage.data[index]; - if (newValue === originalValue) { - return; - } - previewVoxelManager.setAtIndex(index, assignIndex); - }); - - triggerSegmentationDataModified( - operationData.segmentationId, - previewVoxelManager.getArrayOfModifiedSlices(), - assignIndex - ); - }); - return operationData; - }, -}; diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/labelmapStatistics.ts b/packages/tools/src/tools/segmentation/strategies/compositions/labelmapStatistics.ts index 49ee1514cd..ad3868196f 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/labelmapStatistics.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/labelmapStatistics.ts @@ -1,15 +1,6 @@ import StrategyCallbacks from '../../../../enums/StrategyCallbacks'; import type { InitializedOperationData } from '../BrushStrategy'; -import VolumetricCalculator from '../../../../utilities/segmentation/VolumetricCalculator'; -import { getActiveSegmentIndex } from '../../../../stateManagement/segmentation/getActiveSegmentIndex'; -import { getStrategyData } from '../utils/getStrategyData'; -import { utilities, type Types } from '@cornerstonejs/core'; -import { getPixelValueUnits } from '../../../../utilities/getPixelValueUnits'; -import { AnnotationTool } from '../../../base'; -import { isViewportPreScaled } from '../../../../utilities/viewport/isViewportPreScaled'; - -// Radius for a volume of 10, eg 1 cm^3 = 1000 mm^3 -const radiusForVol1 = Math.pow((3 * 1000) / (4 * Math.PI), 1 / 3); +import getStatistics from '../../../../utilities/segmentation/getStatistics'; /** * Compute basic labelmap segmentation statistics. @@ -20,139 +11,11 @@ export default { operationData: InitializedOperationData, options?: { indices?: number | number[] } ) { - const { viewport } = enabledElement; - let { indices } = options; - const { segmentationId } = operationData; - if (!indices) { - indices = [getActiveSegmentIndex(segmentationId)]; - } else if (!Array.isArray(indices)) { - // Include the preview index - indices = [indices, 255]; - } - const indicesArr = indices as number[]; - - const { - segmentationVoxelManager, - imageVoxelManager, - segmentationImageData, - } = getStrategyData({ - operationData, - viewport, + const { indices } = options; + const { segmentationId, viewport } = operationData; + getStatistics({ + segmentationId, + segmentIndices: indices, }); - - const spacing = segmentationImageData.getSpacing(); - - const { boundsIJK: boundsOrig } = segmentationVoxelManager; - if (!boundsOrig) { - return VolumetricCalculator.getStatistics({ spacing }); - } - - segmentationVoxelManager.forEach((voxel) => { - const { value, pointIJK } = voxel; - if (indicesArr.indexOf(value) === -1) { - return; - } - const imageValue = imageVoxelManager.getAtIJKPoint(pointIJK); - VolumetricCalculator.statsCallback({ value: imageValue, pointIJK }); - }); - const targetId = viewport.getViewReferenceId(); - const modalityUnitOptions = { - isPreScaled: isViewportPreScaled(viewport, targetId), - isSuvScaled: AnnotationTool.isSuvScaled( - viewport, - targetId, - viewport.getCurrentImageId() - ), - }; - - const imageData = (viewport as Types.IVolumeViewport).getImageData(); - const unit = getPixelValueUnits( - imageData.metadata.Modality, - viewport.getCurrentImageId(), - modalityUnitOptions - ); - - const stats = VolumetricCalculator.getStatistics({ spacing, unit }); - const { maxIJKs } = stats; - if (!maxIJKs?.length) { - return stats; - } - - // The calculation isn't very good at setting units - stats.mean.unit = unit; - stats.max.unit = unit; - stats.min.unit = unit; - - if (unit !== 'SUV') { - return stats; - } - - // Get the IJK rounded radius, not using less than 1, and using the - // radius for the spacing given the desired mm spacing of 10 - // Add 10% to the radius to account for whole pixel in/out issues - const radiusIJK = spacing.map((s) => - Math.max(1, Math.round((1.1 * radiusForVol1) / s)) - ); - for (const testMax of maxIJKs) { - const testStats = getSphereStats( - testMax, - radiusIJK, - segmentationImageData, - imageVoxelManager, - spacing - ); - if (!testStats) { - continue; - } - const { mean } = testStats; - // @ts-expect-error - TODO: fix this - if (!stats.peakValue || stats.peakValue.value <= mean.value) { - // @ts-expect-error - TODO: fix this - stats.peakValue = { - name: 'peakValue', - label: 'Peak Value', - value: mean.value, - unit, - }; - } - } - - return stats; }, }; - -/** - * Gets the statistics for a 1 cm^3 sphere centered on radiusIJK. - * Assumes the segmentation and pixel data are co-incident. - */ -function getSphereStats(testMax, radiusIJK, segData, imageVoxels, spacing) { - const { pointIJK: centerIJK } = testMax; - const boundsIJK = centerIJK.map((ijk, idx) => [ - ijk - radiusIJK[idx], - ijk + radiusIJK[idx], - ]); - const testFunction = (_pointLPS, pointIJK) => { - const i = (pointIJK[0] - centerIJK[0]) / radiusIJK[0]; - const j = (pointIJK[1] - centerIJK[1]) / radiusIJK[1]; - const k = (pointIJK[2] - centerIJK[2]) / radiusIJK[2]; - const radius = i * i + j * j + k * k; - return radius <= 1; - }; - const statsFunction = ({ pointIJK, pointLPS }) => { - const value = imageVoxels.getAtIJKPoint(pointIJK); - if (value === undefined) { - return; - } - VolumetricCalculator.statsCallback({ value, pointLPS, pointIJK }); - }; - VolumetricCalculator.statsInit({ storePointData: false }); - // pointInShapeCallback(segData, testFunction, statsFunction, boundsIJK); - - utilities.pointInShapeCallback(segData, { - pointInShapeFn: testFunction, - callback: statsFunction, - boundsIJK, - }); - - return VolumetricCalculator.getStatistics({ spacing }); -} diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/preview.ts b/packages/tools/src/tools/segmentation/strategies/compositions/preview.ts index 8c36fd84e8..8e402b9f81 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/preview.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/preview.ts @@ -28,9 +28,9 @@ export default { [StrategyCallbacks.Preview]: function ( operationData: InitializedOperationData ) { - const { previewColors, strategySpecificConfiguration, enabledElement } = - operationData; - if (!previewColors || !strategySpecificConfiguration) { + const { previewColors, configuration, enabledElement } = operationData; + + if (!previewColors || !configuration) { return; } @@ -38,16 +38,19 @@ export default { if (operationData.preview) { delete operationData.preview; } - delete strategySpecificConfiguration.centerSegmentIndex; + + delete configuration.centerSegmentIndex; // Now generate a normal preview as though the user had clicked, filled, released this.onInteractionStart?.(enabledElement, operationData); + const preview = this.fill(enabledElement, operationData); if (preview) { preview.isPreviewFromHover = true; operationData.preview = preview; this.onInteractionEnd?.(enabledElement, operationData); } + return preview; }, @@ -125,6 +128,7 @@ export default { if (previewSegmentIndex === undefined) { return; } + const segmentIndex = preview?.segmentIndex ?? operationData.segmentIndex; if (!previewVoxelManager || previewVoxelManager.modifiedSlices.size === 0) { return; @@ -175,7 +179,7 @@ export default { previewVoxelManager.forEach(callback); // Primarily rejects back to zero, so use 0 as the segment index - even - // if somtimes it modifies the data to other values on reject. + // if sometimes it modifies the data to other values on reject. triggerSegmentationDataModified( operationData.segmentationId, previewVoxelManager.getArrayOfModifiedSlices(), diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/threshold.ts b/packages/tools/src/tools/segmentation/strategies/compositions/threshold.ts index f4fb26c09f..254e334602 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/threshold.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/threshold.ts @@ -12,26 +12,24 @@ export default { [StrategyCallbacks.CreateIsInThreshold]: ( operationData: InitializedOperationData ) => { - const { imageVoxelManager, strategySpecificConfiguration, segmentIndex } = - operationData; - if (!strategySpecificConfiguration || !segmentIndex) { + const { imageVoxelManager, segmentIndex, configuration } = operationData; + + if (!configuration || !segmentIndex) { return; } - return (index) => { - const { THRESHOLD, THRESHOLD_INSIDE_CIRCLE } = - strategySpecificConfiguration; + return (index) => { const voxelValue = imageVoxelManager.getAtIndex(index); const gray = Array.isArray(voxelValue) ? vec3.length(voxelValue as Types.Point3) : voxelValue; - // Prefer the generic version of the THRESHOLD configuration, but fallback - // to the older THRESHOLD_INSIDE_CIRCLE version. - const { threshold } = THRESHOLD || THRESHOLD_INSIDE_CIRCLE || {}; - if (!threshold?.length) { + + const { threshold } = configuration || {}; + + if (!threshold?.range?.length) { return true; } - return threshold[0] <= gray && gray <= threshold[1]; + return threshold.range[0] <= gray && gray <= threshold.range[1]; }; }, }; diff --git a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts index eb66bf44c2..cc6e39f5d2 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts @@ -126,8 +126,7 @@ const CIRCLE_STRATEGY = new BrushStrategy( initializeCircle, compositions.determineSegmentIndex, compositions.preview, - compositions.labelmapStatistics, - compositions.labelmapInterpolation + compositions.labelmapStatistics ); const CIRCLE_THRESHOLD_STRATEGY = new BrushStrategy( @@ -140,8 +139,7 @@ const CIRCLE_THRESHOLD_STRATEGY = new BrushStrategy( compositions.threshold, compositions.preview, compositions.islandRemoval, - compositions.labelmapStatistics, - compositions.labelmapInterpolation + compositions.labelmapStatistics ); /** diff --git a/packages/tools/src/tools/segmentation/strategies/fillRectangle.ts b/packages/tools/src/tools/segmentation/strategies/fillRectangle.ts index a5d03973fb..4513f18848 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillRectangle.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillRectangle.ts @@ -145,7 +145,6 @@ const RECTANGLE_STRATEGY = new BrushStrategy( compositions.determineSegmentIndex, compositions.preview, compositions.labelmapStatistics - // compositions.labelmapInterpolation ); const RECTANGLE_THRESHOLD_STRATEGY = new BrushStrategy( @@ -159,7 +158,6 @@ const RECTANGLE_THRESHOLD_STRATEGY = new BrushStrategy( compositions.preview, compositions.islandRemoval, compositions.labelmapStatistics - // compositions.labelmapInterpolation ); /** diff --git a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts index 34886098a1..31b3327c82 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts @@ -58,7 +58,8 @@ const SPHERE_STRATEGY = new BrushStrategy( sphereComposition, compositions.determineSegmentIndex, compositions.preview, - compositions.labelmapStatistics + compositions.labelmapStatistics, + compositions.ensureSegmentationVolumeFor3DManipulation ); /** @@ -73,7 +74,9 @@ const SPHERE_THRESHOLD_STRATEGY = new BrushStrategy( 'SphereThreshold', ...SPHERE_STRATEGY.compositions, compositions.dynamicThreshold, - compositions.threshold + compositions.threshold, + compositions.ensureSegmentationVolumeFor3DManipulation, + compositions.ensureImageVolumeFor3DManipulation ); const SPHERE_THRESHOLD_STRATEGY_ISLAND = new BrushStrategy( @@ -81,7 +84,9 @@ const SPHERE_THRESHOLD_STRATEGY_ISLAND = new BrushStrategy( ...SPHERE_STRATEGY.compositions, compositions.dynamicThreshold, compositions.threshold, - compositions.islandRemoval + compositions.islandRemoval, + compositions.ensureSegmentationVolumeFor3DManipulation, + compositions.ensureImageVolumeFor3DManipulation ); /** diff --git a/packages/tools/src/tools/segmentation/strategies/utils/getItkImage.ts b/packages/tools/src/tools/segmentation/strategies/utils/getItkImage.ts deleted file mode 100644 index 5c2d4cda63..0000000000 --- a/packages/tools/src/tools/segmentation/strategies/utils/getItkImage.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { peerImport } from '@cornerstonejs/core'; - -/** - * Get the ITK Image from the image data - * - * @param viewportId - Viewport Id - * @param imageName - Any random name that shall be set in the image - * @returns An ITK Image that can be used as fixed or moving image - */ -export default async function getItkImage( - imageData, - imageName?: string -): Promise { - let Image, ImageType, IntTypes, FloatTypes, PixelTypes; - - try { - const itkModule = await peerImport('itk-wasm'); - if (!itkModule) { - throw new Error('Module not found'); - } - ({ Image, ImageType, IntTypes, FloatTypes, PixelTypes } = itkModule); - } catch (error) { - console.warn( - "Warning: 'itk-wasm' module not found. Please install it separately." - ); - return null; - } - - const dataTypesMap = { - Int8: IntTypes.Int8, - UInt8: IntTypes.UInt8, - Int16: IntTypes.Int16, - UInt16: IntTypes.UInt16, - Int32: IntTypes.Int32, - UInt32: IntTypes.UInt32, - Int64: IntTypes.Int64, - UInt64: IntTypes.UInt64, - Float32: FloatTypes.Float32, - Float64: FloatTypes.Float64, - }; - - const { voxelManager } = imageData.get('voxelManager'); - const { numberOfComponents } = imageData.get('numberOfComponents'); - const scalarData = voxelManager.getCompleteScalarDataArray(); - - const dimensions = imageData.getDimensions(); - const origin = imageData.getOrigin(); - const spacing = imageData.getSpacing(); - const directionArray = imageData.getDirection(); - const direction = new Float64Array(directionArray); - const dataType = scalarData.constructor.name - .replace(/^Ui/, 'UI') - .replace(/Array$/, ''); - const metadata = undefined; - const imageType = new ImageType( - dimensions.length, - dataTypesMap[dataType], - PixelTypes.Scalar, - numberOfComponents - ); - - const image = new Image(imageType); - image.name = imageName; - image.origin = origin; - image.spacing = spacing; - image.direction = direction; - image.size = dimensions; - image.metadata = metadata; - image.data = scalarData; - - // image.data = new scalarData.constructor(scalarData.length); - // image.data.set(scalarData, 0); - - return image; -} diff --git a/packages/tools/src/tools/segmentation/strategies/utils/getStrategyData.ts b/packages/tools/src/tools/segmentation/strategies/utils/getStrategyData.ts index 31ffb3c390..faf8d2c6b8 100644 --- a/packages/tools/src/tools/segmentation/strategies/utils/getStrategyData.ts +++ b/packages/tools/src/tools/segmentation/strategies/utils/getStrategyData.ts @@ -3,82 +3,140 @@ import { cache, Enums, eventTarget, + type Types, } from '@cornerstonejs/core'; -import type { LabelmapToolOperationDataStack } from '../../../../types'; +import type { + LabelmapToolOperationDataStack, + LabelmapToolOperationDataVolume, +} from '../../../../types'; import { getCurrentLabelmapImageIdForViewport } from '../../../../stateManagement/segmentation/segmentationState'; import { getLabelmapActorEntry } from '../../../../stateManagement/segmentation/helpers'; -function getStrategyData({ operationData, viewport }) { - let segmentationImageData, segmentationScalarData, imageScalarData; - let imageVoxelManager; - let segmentationVoxelManager; +/** + * Get strategy data for volume viewport + * @param operationData - The operation data containing volumeId and referencedVolumeId + * @returns The strategy data for volume viewport or null if error + */ +function getStrategyDataForVolumeViewport({ operationData }) { + const { volumeId } = operationData; + + if (!volumeId) { + const event = new CustomEvent(Enums.Events.ERROR_EVENT, { + detail: { + type: 'Segmentation', + message: 'No volume id found for the segmentation', + }, + cancelable: true, + }); + eventTarget.dispatchEvent(event); + return null; + } - if (viewport instanceof BaseVolumeViewport) { - const { volumeId, referencedVolumeId } = operationData; - - if (!volumeId) { - const event = new CustomEvent(Enums.Events.ERROR_EVENT, { - detail: { - type: 'Segmentation', - message: 'No volume id found for the segmentation', - }, - cancelable: true, - }); - eventTarget.dispatchEvent(event); - return null; - } + const segmentationVolume = cache.getVolume(volumeId); - const segmentationVolume = cache.getVolume(volumeId); + if (!segmentationVolume) { + return null; + } - if (!segmentationVolume) { - return; - } - segmentationVoxelManager = segmentationVolume.voxelManager; + const referencedVolumeId = segmentationVolume.referencedVolumeId; - // we only need the referenceVolumeId if we do thresholding - // but for other operations we don't need it so make it optional - if (referencedVolumeId) { - const imageVolume = cache.getVolume(referencedVolumeId); - imageVoxelManager = imageVolume.voxelManager; - } + const segmentationVoxelManager = segmentationVolume.voxelManager; + let imageVoxelManager; + let imageData; + + // we only need the referenceVolumeId if we do thresholding + // but for other operations we don't need it so make it optional + if (referencedVolumeId) { + const imageVolume = cache.getVolume(referencedVolumeId); + imageVoxelManager = imageVolume.voxelManager; + imageData = imageVolume.imageData; + } - ({ imageData: segmentationImageData } = segmentationVolume); - // segmentationDimensions = segmentationVolume.dimensions; - } else { - const { segmentationId } = operationData as LabelmapToolOperationDataStack; + const { imageData: segmentationImageData } = segmentationVolume; + return { + segmentationImageData, + segmentationVoxelManager, + segmentationScalarData: null, + imageScalarData: null, + imageVoxelManager, + imageData, + }; +} + +/** + * Get strategy data for stack viewport + * @param operationData - The operation data containing segmentationId and imageId + * @param viewport - The viewport instance + * @returns The strategy data for stack viewport or null if error + */ +function getStrategyDataForStackViewport({ + operationData, + viewport, + strategy, +}) { + const { segmentationId } = operationData as LabelmapToolOperationDataStack; + + let segmentationImageData; + let segmentationVoxelManager; + let segmentationScalarData; + let imageScalarData; + let imageVoxelManager; + let imageData; + if (strategy.ensureSegmentationVolumeFor3DManipulation) { + // Todo: I don't know how to handle this, seems like strategies cannot return anything + // and just manipulate the operationData? + strategy.ensureSegmentationVolumeFor3DManipulation({ + operationData, + viewport, + }); + + segmentationVoxelManager = operationData.segmentationVoxelManager; + segmentationImageData = operationData.segmentationImageData; + segmentationScalarData = null; + } else { const labelmapImageId = getCurrentLabelmapImageIdForViewport( viewport.id, segmentationId ); if (!labelmapImageId) { - return; - } - - const currentImageId = viewport.getCurrentImageId(); - if (!currentImageId) { - return; + return null; } - const actorEntry = getLabelmapActorEntry(viewport.id, segmentationId); if (!actorEntry) { - return; + return null; } - const currentSegImage = cache.getImage(labelmapImageId); segmentationImageData = actorEntry.actor.getMapper().getInputData(); segmentationVoxelManager = currentSegImage.voxelManager; + const currentSegmentationImageId = operationData.imageId; const segmentationImage = cache.getImage(currentSegmentationImageId); if (!segmentationImage) { - return; + return null; } segmentationScalarData = segmentationImage.getPixelData?.(); + } + + if (strategy.ensureImageVolumeFor3DManipulation) { + strategy.ensureImageVolumeFor3DManipulation({ + operationData, + viewport, + }); + + imageVoxelManager = operationData.imageVoxelManager; + imageScalarData = operationData.imageScalarData; + imageData = operationData.imageData; + } else { + const currentImageId = viewport.getCurrentImageId(); + if (!currentImageId) { + return null; + } const image = cache.getImage(currentImageId); - const imageData = image ? null : viewport.getImageData(); + imageData = image ? null : viewport.getImageData(); // VERY IMPORTANT // This is the pixel data of the image that is being segmented in the cache @@ -88,15 +146,40 @@ function getStrategyData({ operationData, viewport }) { } return { - // image data segmentationImageData, - // scalar data segmentationScalarData, imageScalarData, - // voxel managers segmentationVoxelManager, imageVoxelManager, + imageData, }; } +/** + * Get strategy data based on viewport type + * @param params - Object containing operationData and viewport + * @returns The strategy data or null if error + */ +function getStrategyData({ + operationData, + viewport, + strategy, +}: { + operationData: + | LabelmapToolOperationDataStack + | LabelmapToolOperationDataVolume; + viewport?: Types.IStackViewport | Types.IVolumeViewport; + strategy: unknown; +}) { + if ( + ('volumeId' in operationData && operationData.volumeId !== undefined) || + ('referencedVolumeId' in operationData && + operationData.referencedVolumeId !== undefined) + ) { + return getStrategyDataForVolumeViewport({ operationData }); + } + + return getStrategyDataForStackViewport({ operationData, viewport, strategy }); +} + export { getStrategyData }; diff --git a/packages/tools/src/tools/segmentation/strategies/utils/isWithinThreshold.ts b/packages/tools/src/tools/segmentation/strategies/utils/isWithinThreshold.ts index 37564d7794..27646b2aa0 100644 --- a/packages/tools/src/tools/segmentation/strategies/utils/isWithinThreshold.ts +++ b/packages/tools/src/tools/segmentation/strategies/utils/isWithinThreshold.ts @@ -3,18 +3,16 @@ import type { Types } from '@cornerstonejs/core'; function isWithinThreshold( index: number, imageScalarData: Types.PixelDataTypedArray, - strategySpecificConfiguration: { - THRESHOLD?: { threshold: number[] }; - THRESHOLD_INSIDE_CIRCLE?: { threshold: number[] }; + threshold: { + range: number[]; } ) { - const { THRESHOLD, THRESHOLD_INSIDE_CIRCLE } = strategySpecificConfiguration; + if (!threshold) { + return true; + } const voxelValue = imageScalarData[index]; - // Prefer the generic version of the THRESHOLD configuration, but fallback - // to the older THRESHOLD_INSIDE_CIRCLE version. - const { threshold } = THRESHOLD || THRESHOLD_INSIDE_CIRCLE; - return threshold[0] <= voxelValue && voxelValue <= threshold[1]; + return threshold.range[0] <= voxelValue && voxelValue <= threshold.range[1]; } export default isWithinThreshold; diff --git a/packages/tools/src/types/CalculatorTypes.ts b/packages/tools/src/types/CalculatorTypes.ts index e78be70576..b68dfc566f 100644 --- a/packages/tools/src/types/CalculatorTypes.ts +++ b/packages/tools/src/types/CalculatorTypes.ts @@ -5,6 +5,8 @@ type Statistics = { label?: string; value: number | number[]; unit: null | string; + pointIJK?: Types.Point3; + pointLPS?: Types.Point3; }; type NamedStatistics = { diff --git a/packages/tools/src/types/EventTypes.ts b/packages/tools/src/types/EventTypes.ts index c5d893a8b9..8850f50385 100644 --- a/packages/tools/src/types/EventTypes.ts +++ b/packages/tools/src/types/EventTypes.ts @@ -128,14 +128,17 @@ type AnnotationCompletedEventDetail = { * The data that is passed to the event handler when an annotation is modified. */ type AnnotationModifiedEventDetail = { - /** unique id of the viewport */ - viewportId: string; - /** unique id of the rendering engine */ - renderingEngineId: string; /** The annotation that is being added to the annotations manager. */ annotation: Annotation; /** The type of this change */ changeType?: ChangeTypes; + /** + * unique id of the viewport. Note this is optional and only included if the + * element is known/being modified on a given viewport + */ + viewportId?: string; + /** unique id of the rendering engine */ + renderingEngineId?: string; }; /** diff --git a/packages/tools/src/types/LabelmapToolOperationData.ts b/packages/tools/src/types/LabelmapToolOperationData.ts index 52e7c9acd6..544846689f 100644 --- a/packages/tools/src/types/LabelmapToolOperationData.ts +++ b/packages/tools/src/types/LabelmapToolOperationData.ts @@ -17,9 +17,7 @@ type LabelmapToolOperationData = { segmentsLocked: number[]; viewPlaneNormal: number[]; viewUp: number[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - strategySpecificConfiguration: any; - // constraintFn: (pointIJK: number) => boolean; + activeStrategy: string; points: Types.Point3[]; voxelManager; override: { diff --git a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts index c8a73f2d93..c8b5d1e6d9 100644 --- a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts +++ b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts @@ -56,6 +56,15 @@ export interface ProbeAnnotation extends Annotation { }; } +export type KeyImageAnnotation = ProbeAnnotation & { + data: { + /** Indicates that the point selected is relevant rather than just the image */ + isPoint: boolean; + /** Indicates that this key image selects the entire stack/volume (series) */ + seriesLevel: boolean; + }; +}; + export interface LengthAnnotation extends Annotation { data: { handles: { diff --git a/packages/tools/src/types/index.ts b/packages/tools/src/types/index.ts index 9b05c61704..a43d199e98 100644 --- a/packages/tools/src/types/index.ts +++ b/packages/tools/src/types/index.ts @@ -82,11 +82,21 @@ import type { SplineCurveSegment } from './SplineCurveSegment'; import type { SplineLineSegment } from './SplineLineSegment'; import type { SplineProps } from './SplineProps'; import type { BidirectionalData } from '../utilities/segmentation/createBidirectionalToolData'; -import type { PolySegConversionOptions } from './PolySeg'; import type { IBaseTool } from './IBaseTool'; import type { RepresentationStyle } from './../stateManagement/segmentation/SegmentationStyle'; -import type { LabelmapStyle } from './LabelmapTypes'; -import type { SurfaceStyle } from './SurfaceTypes'; +import type { + LabelmapStyle, + LabelmapSegmentationData, + LabelmapSegmentationDataStack, + LabelmapSegmentationDataVolume, + BaseLabelmapStyle, + InactiveLabelmapStyle, +} from './LabelmapTypes'; +import type { + SurfaceStyle, + SurfaceSegmentationData, + SurfaceStateStyles, +} from './SurfaceTypes'; export type { // AnnotationState @@ -171,7 +181,6 @@ export type { SplineLineSegment, SplineProps, // polySeg - PolySegConversionOptions, IBaseTool, RepresentationStyle, Segment, @@ -179,4 +188,11 @@ export type { LabelmapStyle, ContourStyle, SurfaceStyle, + SurfaceSegmentationData, + SurfaceStateStyles, + LabelmapSegmentationData, + LabelmapSegmentationDataStack, + LabelmapSegmentationDataVolume, + BaseLabelmapStyle, + InactiveLabelmapStyle, }; diff --git a/packages/tools/src/geometricSurfaceUtils.ts b/packages/tools/src/utilities/geometricSurfaceUtils.ts similarity index 100% rename from packages/tools/src/geometricSurfaceUtils.ts rename to packages/tools/src/utilities/geometricSurfaceUtils.ts diff --git a/packages/tools/src/utilities/getPixelValueUnits.ts b/packages/tools/src/utilities/getPixelValueUnits.ts index 31563602bc..a56457ccf1 100644 --- a/packages/tools/src/utilities/getPixelValueUnits.ts +++ b/packages/tools/src/utilities/getPixelValueUnits.ts @@ -5,6 +5,14 @@ type pixelUnitsOptions = { isSuvScaled: boolean; }; +function getPixelValueUnitsImageId( + imageId: string, + options: pixelUnitsOptions +): string { + const generalSeriesModule = metaData.get('generalSeriesModule', imageId); + return getPixelValueUnits(generalSeriesModule.modality, imageId, options); +} + /** * Determines the appropriate pixel value units based on the image modality and options. * @param modality - The modality of the image (e.g., 'CT', 'PT'). @@ -57,4 +65,4 @@ function _handlePTModality( } export type { pixelUnitsOptions }; -export { getPixelValueUnits }; +export { getPixelValueUnits, getPixelValueUnitsImageId }; diff --git a/packages/tools/src/utilities/index.ts b/packages/tools/src/utilities/index.ts index fca1df2d2e..110c0d13e7 100644 --- a/packages/tools/src/utilities/index.ts +++ b/packages/tools/src/utilities/index.ts @@ -50,7 +50,11 @@ import { pointInSurroundingSphereCallback } from './pointInSurroundingSphereCall const roundNumber = utilities.roundNumber; import normalizeViewportPlane from './normalizeViewportPlane'; import IslandRemoval from './segmentation/islandRemoval'; -import { getPixelValueUnits } from './getPixelValueUnits'; +import { + getPixelValueUnits, + getPixelValueUnitsImageId, +} from './getPixelValueUnits'; +import * as geometricSurfaceUtils from './geometricSurfaceUtils'; export { math, @@ -69,6 +73,7 @@ export { getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, + getPixelValueUnitsImageId, segmentation, contours, triggerAnnotationRenderForViewportIds, @@ -96,4 +101,5 @@ export { pointInSurroundingSphereCallback, normalizeViewportPlane, IslandRemoval, + geometricSurfaceUtils, }; diff --git a/packages/tools/src/utilities/math/basic/BasicStatsCalculator.ts b/packages/tools/src/utilities/math/basic/BasicStatsCalculator.ts index ab8255df08..460d405c30 100644 --- a/packages/tools/src/utilities/math/basic/BasicStatsCalculator.ts +++ b/packages/tools/src/utilities/math/basic/BasicStatsCalculator.ts @@ -9,6 +9,10 @@ export default class BasicStatsCalculator extends Calculator { private static min = [Infinity]; private static sum = [0]; private static count = 0; + private static maxIJK = null; + private static maxLPS = null; + private static minIJK = null; + private static minLPS = null; // Values for Welford's algorithm private static runMean = [0]; @@ -27,7 +31,11 @@ export default class BasicStatsCalculator extends Calculator { * This callback is used when we verify if the point is in the annotation drawn * so we can get every point in the shape to calculate the statistics */ - static statsCallback = ({ value: newValue, pointLPS = null }): void => { + static statsCallback = ({ + value: newValue, + pointLPS = null, + pointIJK = null, + }): void => { if ( Array.isArray(newValue) && newValue.length > 1 && @@ -56,7 +64,21 @@ export default class BasicStatsCalculator extends Calculator { this.m2[idx] += delta * delta2; this.min[idx] = Math.min(this.min[idx], value); - this.max[idx] = Math.max(it, value); + if (value < this.min[idx]) { + this.min[idx] = value; + if (idx === 0) { + this.minIJK = pointIJK; + this.minLPS = pointLPS; + } + } + + if (value > this.max[idx]) { + this.max[idx] = value; + if (idx === 0) { + this.maxIJK = pointIJK; + this.maxLPS = pointLPS; + } + } }); }; @@ -83,12 +105,16 @@ export default class BasicStatsCalculator extends Calculator { label: 'Max Pixel', value: singleArrayAsNumber(this.max), unit, + pointIJK: this.maxIJK, + pointLPS: this.maxLPS, }, min: { name: 'min', label: 'Min Pixel', value: singleArrayAsNumber(this.min), unit, + pointIJK: this.minIJK, + pointLPS: this.minLPS, }, mean: { name: 'mean', @@ -124,10 +150,13 @@ export default class BasicStatsCalculator extends Calculator { this.max = [-Infinity]; this.min = [Infinity]; this.sum = [0]; - // this.sumSquares = [0]; this.m2 = [0]; this.runMean = [0]; this.count = 0; + this.maxIJK = null; + this.maxLPS = null; + this.minIJK = null; + this.minLPS = null; this.pointsInShape = PointsManager.create3(1024); return named; diff --git a/packages/tools/src/utilities/registerComputeWorker.ts b/packages/tools/src/utilities/registerComputeWorker.ts new file mode 100644 index 0000000000..fa3f447ee7 --- /dev/null +++ b/packages/tools/src/utilities/registerComputeWorker.ts @@ -0,0 +1,34 @@ +import { getWebWorkerManager } from '@cornerstonejs/core'; +let registered = false; + +export function registerComputeWorker() { + if (registered) { + return; + } + + registered = true; + + const workerFn = () => { + // @ts-ignore + return new Worker( + // @ts-ignore + new URL('../workers/computeWorker.js', import.meta.url), + { + name: 'compute', + type: 'module', + } + ); + }; + + const workerManager = getWebWorkerManager(); + + const options = { + maxWorkerInstances: 1, + autoTerminateOnIdle: { + enabled: true, + idleTimeThreshold: 2000, + }, + }; + + workerManager.registerWorker('compute', workerFn, options); +} diff --git a/packages/tools/src/utilities/segmentation/brushThresholdForToolGroup.ts b/packages/tools/src/utilities/segmentation/brushThresholdForToolGroup.ts index 53cb5df298..06f8fc0a27 100644 --- a/packages/tools/src/utilities/segmentation/brushThresholdForToolGroup.ts +++ b/packages/tools/src/utilities/segmentation/brushThresholdForToolGroup.ts @@ -1,13 +1,15 @@ import type { Types } from '@cornerstonejs/core'; import { getToolGroup } from '../../store/ToolGroupManager'; import triggerAnnotationRenderForViewportIds from '../triggerAnnotationRenderForViewportIds'; -import { getRenderingEngine } from '@cornerstonejs/core'; import { getBrushToolInstances } from './getBrushToolInstances'; export function setBrushThresholdForToolGroup( toolGroupId: string, - threshold: Types.Point2, - otherArgs: Record = { isDynamic: false } + threshold: { + range: Types.Point2; + isDynamic: boolean; + dynamicRadius: number; + } ) { const toolGroup = getToolGroup(toolGroupId); @@ -16,15 +18,20 @@ export function setBrushThresholdForToolGroup( } const brushBasedToolInstances = getBrushToolInstances(toolGroupId); - const configuration = { - ...otherArgs, - ...(threshold !== undefined && { threshold }), - }; brushBasedToolInstances.forEach((tool) => { - tool.configuration.strategySpecificConfiguration.THRESHOLD = { - ...tool.configuration.strategySpecificConfiguration.THRESHOLD, - ...configuration, + const activeStrategy = tool.configuration.activeStrategy; + + if (!activeStrategy.toLowerCase().includes('threshold')) { + return; + } + + tool.configuration = { + ...tool.configuration, + threshold: { + ...tool.configuration.threshold, + ...threshold, + }, }; }); @@ -35,14 +42,10 @@ export function setBrushThresholdForToolGroup( return; } - const { renderingEngineId } = viewportsInfo[0]; - // Use helper to get array of viewportIds, or we just end up doing this mapping // ourselves here. const viewportIds = toolGroup.getViewportIds(); - const renderingEngine = getRenderingEngine(renderingEngineId); - triggerAnnotationRenderForViewportIds(viewportIds); } @@ -66,7 +69,5 @@ export function getBrushThresholdForToolGroup(toolGroupId: string) { return; } - // TODO -> Assumes the - return brushToolInstance.configuration.strategySpecificConfiguration.THRESHOLD - .threshold; + return brushToolInstance.configuration.threshold.range; } diff --git a/packages/tools/src/stateManagement/segmentation/polySeg/computeAndAddRepresentation.ts b/packages/tools/src/utilities/segmentation/computeAndAddRepresentation.ts similarity index 86% rename from packages/tools/src/stateManagement/segmentation/polySeg/computeAndAddRepresentation.ts rename to packages/tools/src/utilities/segmentation/computeAndAddRepresentation.ts index 9c79db1197..63904629d0 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/computeAndAddRepresentation.ts +++ b/packages/tools/src/utilities/segmentation/computeAndAddRepresentation.ts @@ -1,10 +1,9 @@ import { eventTarget } from '@cornerstonejs/core'; -import type { SegmentationRepresentations } from '../../../enums'; -import { Events } from '../../../enums'; -import addRepresentationData from '../internalAddRepresentationData'; -import { triggerSegmentationModified } from '../triggerSegmentationEvents'; -import debounce from '../../../utilities/debounce'; -import { registerPolySegWorker } from './registerPolySegWorker'; +import type { SegmentationRepresentations } from '../../enums'; +import { Events } from '../../enums'; +import addRepresentationData from '../../stateManagement/segmentation/internalAddRepresentationData'; +import { triggerSegmentationModified } from '../../stateManagement/segmentation/triggerSegmentationEvents'; +import debounce from '../debounce'; const computedRepresentations = new Map< string, @@ -28,9 +27,6 @@ async function computeAndAddRepresentation( updateFunction?: () => void, onComputationComplete?: () => void ): Promise { - // register the worker if it hasn't been registered yet - registerPolySegWorker(); - // Compute the specific representation data const data = await computeFunction(); // Add the computed data to the system diff --git a/packages/tools/src/utilities/segmentation/getOrCreateSegmentationVolume.ts b/packages/tools/src/utilities/segmentation/getOrCreateSegmentationVolume.ts new file mode 100644 index 0000000000..07aba66ce4 --- /dev/null +++ b/packages/tools/src/utilities/segmentation/getOrCreateSegmentationVolume.ts @@ -0,0 +1,44 @@ +import { cache, volumeLoader } from '@cornerstonejs/core'; +import { getSegmentation } from '../../stateManagement/segmentation/getSegmentation'; +import type { + LabelmapSegmentationDataStack, + LabelmapSegmentationDataVolume, +} from '../../types/LabelmapTypes'; + +function getOrCreateSegmentationVolume(segmentationId) { + const { representationData } = getSegmentation(segmentationId); + let { volumeId } = + representationData.Labelmap as LabelmapSegmentationDataVolume; + + let segVolume; + if (volumeId) { + segVolume = cache.getVolume(volumeId); + + if (segVolume) { + return segVolume; + } + } + + const { imageIds: labelmapImageIds } = + representationData.Labelmap as LabelmapSegmentationDataStack; + + volumeId = cache.generateVolumeId(labelmapImageIds); + + // 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. + + if (!labelmapImageIds || labelmapImageIds.length === 1) { + return; + } + + // it will return the cached volume if it already exists + segVolume = volumeLoader.createAndCacheVolumeFromImagesSync( + volumeId, + labelmapImageIds + ); + + return segVolume; +} + +export default getOrCreateSegmentationVolume; diff --git a/packages/tools/src/utilities/segmentation/getStatistics.ts b/packages/tools/src/utilities/segmentation/getStatistics.ts new file mode 100644 index 0000000000..f5c213b112 --- /dev/null +++ b/packages/tools/src/utilities/segmentation/getStatistics.ts @@ -0,0 +1,330 @@ +import { + cache, + utilities, + getWebWorkerManager, + eventTarget, + Enums, + triggerEvent, + metaData, +} from '@cornerstonejs/core'; +import { getActiveSegmentIndex } from '../../stateManagement/segmentation/getActiveSegmentIndex'; +import VolumetricCalculator from './VolumetricCalculator'; +import { getStrategyData } from '../../tools/segmentation/strategies/utils/getStrategyData'; +import { getPixelValueUnitsImageId } from '../getPixelValueUnits'; +import ensureSegmentationVolume from '../../tools/segmentation/strategies/compositions/ensureSegmentationVolume'; +import ensureImageVolume from '../../tools/segmentation/strategies/compositions/ensureImageVolume'; +import { getSegmentation } from '../../stateManagement/segmentation/getSegmentation'; +import { registerComputeWorker } from '../registerComputeWorker'; +import { WorkerTypes } from '../../enums'; +import type { + LabelmapSegmentationDataStack, + LabelmapSegmentationDataVolume, +} from '../../types/LabelmapTypes'; +// Radius for a volume of 10, eg 1 cm^3 = 1000 mm^3 +const radiusForVol1 = Math.pow((3 * 1000) / (4 * Math.PI), 1 / 3); + +const workerManager = getWebWorkerManager(); + +const triggerWorkerProgress = (eventTarget, progress) => { + triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, { + progress, + type: WorkerTypes.COMPUTE_STATISTICS, + }); +}; + +async function getStatistics({ + segmentationId, + segmentIndices, +}: { + segmentationId: string; + segmentIndices: number[] | number; +}) { + registerComputeWorker(); + + triggerWorkerProgress(eventTarget, 0); + + const segmentation = getSegmentation(segmentationId); + const { representationData } = segmentation; + + const { Labelmap } = representationData; + + if (!Labelmap) { + console.debug('No labelmap found for segmentation', segmentationId); + return; + } + + const segVolumeId = (Labelmap as LabelmapSegmentationDataVolume).volumeId; + const segImageIds = (Labelmap as LabelmapSegmentationDataStack).imageIds; + + // Create a minimal operationData object + const operationData = { + segmentationId, + volumeId: segVolumeId, + imageIds: segImageIds, + }; + + let reconstructableVolume = false; + if (segImageIds) { + const refImageIds = segImageIds.map((imageId) => { + const image = cache.getImage(imageId); + return image.referencedImageId; + }); + reconstructableVolume = utilities.isValidVolume(refImageIds); + } + + let indices = segmentIndices; + + if (!indices) { + indices = [getActiveSegmentIndex(segmentationId)]; + } else if (!Array.isArray(indices)) { + // Include the preview index + indices = [indices, 255]; + } + + // Get reference image ID and modality unit options + const { refImageId, modalityUnitOptions } = getImageReferenceInfo( + segVolumeId, + segImageIds + ); + + const unit = getPixelValueUnitsImageId(refImageId, modalityUnitOptions); + + const stats = reconstructableVolume + ? await calculateVolumeStatistics(operationData, indices, unit) + : await calculateStackStatistics(segImageIds, indices, unit); + + return stats; +} + +/** + * Calculate statistics for a reconstructable volume + */ +async function calculateVolumeStatistics(operationData, indices, unit) { + // Get the strategy data + const strategyData = getStrategyData({ + operationData, + strategy: { + ensureSegmentationVolumeFor3DManipulation: + ensureSegmentationVolume.ensureSegmentationVolumeFor3DManipulation, + ensureImageVolumeFor3DManipulation: + ensureImageVolume.ensureImageVolumeFor3DManipulation, + }, + }); + + const { + segmentationVoxelManager, + imageVoxelManager, + segmentationImageData, + imageData, + } = strategyData; + + const spacing = segmentationImageData.getSpacing(); + + const { boundsIJK: boundsOrig } = segmentationVoxelManager; + if (!boundsOrig) { + return VolumetricCalculator.getStatistics({ spacing }); + } + + const segmentationScalarData = + segmentationVoxelManager.getCompleteScalarDataArray(); + + const imageScalarData = imageVoxelManager.getCompleteScalarDataArray(); + + const segmentationInfo = { + scalarData: segmentationScalarData, + dimensions: segmentationImageData.getDimensions(), + spacing: segmentationImageData.getSpacing(), + origin: segmentationImageData.getOrigin(), + }; + + const imageInfo = { + scalarData: imageScalarData, + dimensions: imageData.getDimensions(), + spacing: imageData.getSpacing(), + origin: imageData.getOrigin(), + }; + + const stats = await workerManager.executeTask( + 'compute', + 'calculateSegmentsStatisticsVolume', + { + segmentationInfo, + imageInfo, + indices, + } + ); + + triggerWorkerProgress(eventTarget, 100); + + // Update units + stats.mean.unit = unit; + stats.max.unit = unit; + stats.min.unit = unit; + + if (unit !== 'SUV') { + return stats; + } + + // Get the IJK rounded radius, not using less than 1, and using the + // radius for the spacing given the desired mm spacing of 10 + // Add 10% to the radius to account for whole pixel in/out issues + const radiusIJK = spacing.map((s) => + Math.max(1, Math.round((1.1 * radiusForVol1) / s)) + ); + + for (const testMax of stats.maxIJKs) { + const testStats = getSphereStats( + testMax, + radiusIJK, + segmentationImageData, + imageVoxelManager, + spacing + ); + if (!testStats) { + continue; + } + const { mean } = testStats; + if (!stats.peakValue || stats.peakValue.value <= mean.value) { + stats.peakValue = { + name: 'peakValue', + label: 'Peak Value', + value: mean.value, + unit, + }; + } + } + + return stats; +} + +/** + * Calculate statistics for a stack of images + */ +async function calculateStackStatistics(segImageIds, indices, unit) { + triggerWorkerProgress(eventTarget, 0); + // we need to loop over each seg image separately and calculate the stats + const segmentationInfo = []; + const imageInfo = []; + for (const segImageId of segImageIds) { + const segImage = cache.getImage(segImageId); + const segPixelData = segImage.getPixelData(); + const segVoxelManager = segImage.voxelManager; + const segSpacing = [segImage.rowPixelSpacing, segImage.columnPixelSpacing]; + + const refImageId = segImage.referencedImageId; + const refImage = cache.getImage(refImageId); + const refPixelData = refImage.getPixelData(); + const refVoxelManager = refImage.voxelManager; + const refSpacing = [refImage.rowPixelSpacing, refImage.columnPixelSpacing]; + + segmentationInfo.push({ + scalarData: segPixelData, + dimensions: segVoxelManager.dimensions, + spacing: segSpacing, + }); + + imageInfo.push({ + scalarData: refPixelData, + dimensions: refVoxelManager.dimensions, + spacing: refSpacing, + }); + } + + const stats = await workerManager.executeTask( + 'compute', + 'calculateSegmentsStatisticsStack', + { + segmentationInfo, + imageInfo, + indices, + } + ); + + triggerWorkerProgress(eventTarget, 100); + + stats.mean.unit = unit; + stats.max.unit = unit; + stats.min.unit = unit; + + return stats; +} + +/** + * Gets the statistics for a 1 cm^3 sphere centered on radiusIJK. + * Assumes the segmentation and pixel data are co-incident. + */ +function getSphereStats(testMax, radiusIJK, segData, imageVoxels, spacing) { + const { pointIJK: centerIJK } = testMax; + + if (!centerIJK) { + return; + } + + const boundsIJK = centerIJK.map((ijk, idx) => [ + ijk - radiusIJK[idx], + ijk + radiusIJK[idx], + ]); + const testFunction = (_pointLPS, pointIJK) => { + const i = (pointIJK[0] - centerIJK[0]) / radiusIJK[0]; + const j = (pointIJK[1] - centerIJK[1]) / radiusIJK[1]; + const k = (pointIJK[2] - centerIJK[2]) / radiusIJK[2]; + const radius = i * i + j * j + k * k; + return radius <= 1; + }; + const statsFunction = ({ pointIJK, pointLPS }) => { + const value = imageVoxels.getAtIJKPoint(pointIJK); + if (value === undefined) { + return; + } + VolumetricCalculator.statsCallback({ value, pointLPS, pointIJK }); + }; + VolumetricCalculator.statsInit({ storePointData: false }); + + utilities.pointInShapeCallback(segData, { + pointInShapeFn: testFunction, + callback: statsFunction, + boundsIJK, + }); + + return VolumetricCalculator.getStatistics({ spacing }); +} + +/** + * Gets the reference image ID and modality unit options based on segmentation data + * @param segVolumeId - The segmentation volume ID + * @param segImageIds - The segmentation image IDs + * @returns Object containing reference image ID and modality unit options + */ +function getImageReferenceInfo(segVolumeId, segImageIds) { + let refImageId; + let modalityUnitOptions; + + if (segVolumeId) { + const segmentationVolume = cache.getVolume(segVolumeId); + const referencedVolumeId = segmentationVolume.referencedVolumeId; + const volume = cache.getVolume(referencedVolumeId); + + if (volume?.imageIds?.length > 0) { + refImageId = volume.imageIds[0]; + } + + modalityUnitOptions = { + isPreScaled: Object.keys(volume.scaling || {}).length > 0, + isSuvScaled: Boolean(volume.scaling?.PT), + }; + } else if (segImageIds?.length) { + const segImage = cache.getImage(segImageIds[0]); + refImageId = segImage.referencedImageId; + const refImage = cache.getImage(refImageId); + const scalingModule = metaData.get('scalingModule', refImageId); + + modalityUnitOptions = { + isPreScaled: Boolean(refImage.preScale?.scaled), + isSuvScaled: typeof scalingModule?.preScale?.scaled === 'number', + }; + } + + return { refImageId, modalityUnitOptions }; +} + +export default getStatistics; diff --git a/packages/tools/src/utilities/segmentation/index.ts b/packages/tools/src/utilities/segmentation/index.ts index 1352a952a3..356d6c380b 100644 --- a/packages/tools/src/utilities/segmentation/index.ts +++ b/packages/tools/src/utilities/segmentation/index.ts @@ -29,6 +29,11 @@ import { getBrushToolInstances } from './getBrushToolInstances'; import * as growCut from './growCut'; import * as LabelmapMemo from './createLabelmapMemo'; import IslandRemoval from './islandRemoval'; +import getOrCreateSegmentationVolume from './getOrCreateSegmentationVolume'; +import getStatistics from './getStatistics'; +import * as validateLabelmap from './validateLabelmap'; +import { computeStackLabelmapFromVolume } from '../../stateManagement/segmentation/helpers/computeStackLabelmapFromVolume'; +import { computeVolumeLabelmapFromStack } from '../../stateManagement/segmentation/helpers/computeVolumeLabelmapFromStack'; export { thresholdVolumeByRange, @@ -56,4 +61,9 @@ export { growCut, LabelmapMemo, IslandRemoval, + getOrCreateSegmentationVolume, + getStatistics, + validateLabelmap, + computeStackLabelmapFromVolume, + computeVolumeLabelmapFromStack, }; diff --git a/packages/tools/src/tools/displayTools/Labelmap/validateLabelmap.ts b/packages/tools/src/utilities/segmentation/validateLabelmap.ts similarity index 94% rename from packages/tools/src/tools/displayTools/Labelmap/validateLabelmap.ts rename to packages/tools/src/utilities/segmentation/validateLabelmap.ts index 96fba0154d..7887a9a111 100644 --- a/packages/tools/src/tools/displayTools/Labelmap/validateLabelmap.ts +++ b/packages/tools/src/utilities/segmentation/validateLabelmap.ts @@ -1,10 +1,10 @@ import { cache } from '@cornerstonejs/core'; -import type { SegmentationPublicInput } from '../../../types/SegmentationStateTypes'; +import type { SegmentationPublicInput } from '../../types/SegmentationStateTypes'; import type { LabelmapSegmentationData, LabelmapSegmentationDataStack, LabelmapSegmentationDataVolume, -} from '../../../types/LabelmapTypes'; +} from '../../types/LabelmapTypes'; function validateRepresentationData( segmentationRepresentationData: LabelmapSegmentationData diff --git a/packages/tools/src/workers/computeWorker.js b/packages/tools/src/workers/computeWorker.js new file mode 100644 index 0000000000..8664a9e195 --- /dev/null +++ b/packages/tools/src/workers/computeWorker.js @@ -0,0 +1,129 @@ +import { expose } from 'comlink'; +import { utilities } from '@cornerstonejs/core'; +import VolumetricCalculator from '../utilities/segmentation/VolumetricCalculator'; + +const { VoxelManager } = utilities; + +const computeWorker = { + calculateSegmentsStatisticsVolume: (args) => { + const { segmentationInfo, imageInfo, indices } = args; + + const { + scalarData: segmentationScalarData, + dimensions: segmentationDimensions, + spacing: segmentationSpacing, + } = segmentationInfo; + const { scalarData: imageScalarData, dimensions: imageDimensions } = + imageInfo; + + // if dimensions are not the same, for now just throw an error + if ( + segmentationDimensions[0] !== imageDimensions[0] || + segmentationDimensions[1] !== imageDimensions[1] || + segmentationDimensions[2] !== imageDimensions[2] + ) { + throw new Error( + 'Dimensions do not match to calculate statistics, different dimensions not supported yet' + ); + } + + // Create VoxelManagers for both segmentation and image data + const segVoxelManager = VoxelManager.createScalarVolumeVoxelManager({ + dimensions: segmentationDimensions, + scalarData: segmentationScalarData, + }); + + const imageVoxelManager = VoxelManager.createScalarVolumeVoxelManager({ + dimensions: imageDimensions, + scalarData: imageScalarData, + }); + + // Use forEach to iterate over all voxels and call statsCallback for those in the segmentation + segVoxelManager.forEach( + ({ value, pointIJK, index }) => { + if (indices.indexOf(value) === -1) { + return; + } + + // get the value from the image voxel manager + const imageValue = imageVoxelManager.getAtIndex(index); + + // Todo: later add the isInObject check based on lps for the different dimensions + // for now just assume the pointIJK is within the bounds + VolumetricCalculator.statsCallback({ + value: imageValue, + pointIJK, + }); + }, + { + boundsIJK: imageVoxelManager.getDefaultBounds(), + } + ); + + const stats = VolumetricCalculator.getStatistics({ + spacing: segmentationSpacing, + unit: 'mm', + }); + + return stats; + }, + + calculateSegmentsStatisticsStack: (args) => { + const { segmentationInfo, imageInfo, indices } = args; + + // Create voxel managers for each pair of segmentation and image info + for (let i = 0; i < segmentationInfo.length; i++) { + const segInfo = segmentationInfo[i]; + const imgInfo = imageInfo[i]; + + const segDimensions = [ + segInfo.dimensions[0], + segInfo.dimensions[1], + 1, // For a single slice + ]; + + const segVoxelManager = VoxelManager.createScalarVolumeVoxelManager({ + dimensions: segDimensions, + scalarData: segInfo.scalarData, + }); + + const imageVoxelManager = VoxelManager.createScalarVolumeVoxelManager({ + dimensions: segDimensions, + scalarData: imgInfo.scalarData, + }); + + // Use forEach to iterate and call statsCallback + segVoxelManager.forEach( + ({ value, pointIJK, index }) => { + // Todo: later add the isInObject check based on lps for the different dimensions + // for now just assume the pointIJK is within the bounds + + if (indices.indexOf(value) === -1) { + return; + } + + // get the value from the image voxel manager + const imageValue = imageVoxelManager.getAtIndex(index); + + VolumetricCalculator.statsCallback({ + value: imageValue, + }); + }, + { + boundsIJK: imageVoxelManager.getDefaultBounds(), + } + ); + } + + // Pick first one for spacing + const spacing = segmentationInfo[0].spacing; + + const stats = VolumetricCalculator.getStatistics({ + spacing, + }); + + return stats; + }, +}; + +expose(computeWorker); diff --git a/tsconfig.json b/tsconfig.json index 93c8a099e8..21596cd2c1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,8 @@ "@cornerstonejs/dicomImageLoader": ["dicomImageLoader/src"], "@cornerstonejs/nifti-volume-loader": ["nifti-volume-loader/src"], "@cornerstonejs/ai": ["ai/src"], + "@cornerstonejs/labelmap-interpolation": ["labelmap-interpolation/src"], + "@cornerstonejs/polymorphic-segmentation": ["polymorphic-segmentation/src"], "@cornerstonejs/adapters": ["adapters/src"] } } diff --git a/utils/ExampleRunner/build-all-examples-cli.js b/utils/ExampleRunner/build-all-examples-cli.js index 977b7b87e5..80e6e43783 100644 --- a/utils/ExampleRunner/build-all-examples-cli.js +++ b/utils/ExampleRunner/build-all-examples-cli.js @@ -56,6 +56,11 @@ if (options.fromRoot === true) { { path: 'packages/core/examples', regexp: 'index.ts' }, { path: 'packages/tools/examples', regexp: 'index.ts' }, { path: 'packages/ai/examples', regexp: 'index.ts' }, + { path: 'packages/labelmap-interpolation/examples', regexp: 'index.ts' }, + { + path: 'packages/polymorphic-segmentation/examples', + regexp: 'index.ts', + }, { path: 'packages/dicomImageLoader/examples', regexp: 'index.ts', diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index 95ddd48971..83a1d1a7b1 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -175,6 +175,10 @@ "name": "Stack Annotation Tools", "description": "Demonstrates usage of various annotation tools (Probe, Rectangle ROI, Elliptical ROI, Bidirectional measurements) on a Stack Viewport." }, + "stackRange": { + "name": "Stack Range", + "description": "Demonstrates use of a selection range for key image and other tools" + }, "calibrationTools": { "name": "Calibration Tools", "description": "Demonstrates usage of calibration tools on a Stack Viewport." @@ -258,10 +262,6 @@ "livewireContour": { "name": "Livewire", "description": "Demonstrates how to use the livewire tool to create ROIs" - }, - "stackRange": { - "name": "Stack Range", - "description": "Demonstrates use of a selection range for key image and other tools" } }, "viewports": { diff --git a/utils/ExampleRunner/example-runner-cli.js b/utils/ExampleRunner/example-runner-cli.js index ea86e83d16..7e22654c97 100755 --- a/utils/ExampleRunner/example-runner-cli.js +++ b/utils/ExampleRunner/example-runner-cli.js @@ -111,6 +111,11 @@ const configuration = { { path: 'packages/core/examples', regexp: 'index.ts' }, { path: 'packages/tools/examples', regexp: 'index.ts' }, { path: 'packages/ai/examples', regexp: 'index.ts' }, + { path: 'packages/labelmap-interpolation/examples', regexp: 'index.ts' }, + { + path: 'packages/polymorphic-segmentation/examples', + regexp: 'index.ts', + }, { path: 'packages/dicomImageLoader/examples', regexp: 'index.ts', diff --git a/utils/ExampleRunner/template-config.js b/utils/ExampleRunner/template-config.js index 057c2daa2f..a9db1dba7d 100644 --- a/utils/ExampleRunner/template-config.js +++ b/utils/ExampleRunner/template-config.js @@ -3,6 +3,12 @@ const path = require('path'); const csRenderBasePath = path.resolve('packages/core/src/index'); const csToolsBasePath = path.resolve('packages/tools/src/index'); const csAiBasePath = path.resolve('packages/ai/src/index'); +const csLabelmapInterpolationBasePath = path.resolve( + 'packages/labelmap-interpolation/src/index' +); +const csPolymorphicSegmentationBasePath = path.resolve( + 'packages/polymorphic-segmentation/src/index' +); const csAdapters = path.resolve('packages/adapters/src/index'); const csDICOMImageLoaderDistPath = path.resolve( 'packages/dicomImageLoader/src/index' @@ -67,6 +73,14 @@ module.exports = { '@cornerstonejs/core': '${csRenderBasePath.replace(/\\/g, '/')}', '@cornerstonejs/tools': '${csToolsBasePath.replace(/\\/g, '/')}', '@cornerstonejs/ai': '${csAiBasePath.replace(/\\/g, '/')}', + '@cornerstonejs/polymorphic-segmentation': '${csPolymorphicSegmentationBasePath.replace( + /\\/g, + '/' + )}', + '@cornerstonejs/labelmap-interpolation': '${csLabelmapInterpolationBasePath.replace( + /\\/g, + '/' + )}', '@cornerstonejs/nifti-volume-loader': '${csNiftiPath.replace( /\\/g, '/' diff --git a/utils/ExampleRunner/template-multiexample-config.js b/utils/ExampleRunner/template-multiexample-config.js index 7f84f0028e..e1695100e5 100644 --- a/utils/ExampleRunner/template-multiexample-config.js +++ b/utils/ExampleRunner/template-multiexample-config.js @@ -3,6 +3,12 @@ const path = require('path'); const csRenderBasePath = path.resolve('./packages/core/src/index'); const csToolsBasePath = path.resolve('./packages/tools/src/index'); const csAiBasePath = path.resolve('./packages/ai/src/index'); +const csLabelmapInterpolationBasePath = path.resolve( + './packages/labelmap-interpolation/src/index' +); +const csPolymorphicSegmentationBasePath = path.resolve( + 'packages/polymorphic-segmentation/src/index' +); const csAdaptersBasePath = path.resolve('./packages/adapters/src/index'); const csDICOMImageLoaderDistPath = path.resolve( 'packages/dicomImageLoader/src/index' @@ -102,6 +108,14 @@ module.exports = { '@cornerstonejs/core': '${csRenderBasePath.replace(/\\/g, '/')}', '@cornerstonejs/tools': '${csToolsBasePath.replace(/\\/g, '/')}', '@cornerstonejs/ai': '${csAiBasePath.replace(/\\/g, '/')}', + '@cornerstonejs/polymorphic-segmentation': '${csPolymorphicSegmentationBasePath.replace( + /\\/g, + '/' + )}', + '@cornerstonejs/labelmap-interpolation': '${csLabelmapInterpolationBasePath.replace( + /\\/g, + '/' + )}', '@cornerstonejs/adapters': '${csAdaptersBasePath.replace(/\\/g, '/')}', '@cornerstonejs/dicom-image-loader': '${csDICOMImageLoaderDistPath.replace( /\\/g, diff --git a/utils/demo/helpers/initDemo.js b/utils/demo/helpers/initDemo.ts similarity index 84% rename from utils/demo/helpers/initDemo.js rename to utils/demo/helpers/initDemo.ts index 59959148b1..de0677b091 100644 --- a/utils/demo/helpers/initDemo.js +++ b/utils/demo/helpers/initDemo.ts @@ -15,6 +15,7 @@ import { fakeMetaDataProvider, } from '../../test/testUtilsImageLoader'; import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'; +import * as polySeg from '@cornerstonejs/polymorphic-segmentation'; window.cornerstone = cornerstone; window.cornerstoneTools = cornerstoneTools; @@ -27,7 +28,11 @@ export default async function initDemo(config) { peerImport, ...(config?.core ? config.core : {}), }); - await csToolsInit(); + await csToolsInit({ + addons: { + polySeg, + }, + }); // for testings, you don't need any of these volumeLoader.registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader); @@ -47,18 +52,6 @@ export async function peerImport(moduleId) { 'dicomMicroscopyViewer' ); } - - if (moduleId === '@icr/polyseg-wasm') { - return import('@icr/polyseg-wasm'); - } - - if (moduleId === 'itk-wasm') { - return import('itk-wasm'); - } - - if (moduleId === '@itk-wasm/morphological-contour-interpolation') { - return import('@itk-wasm/morphological-contour-interpolation'); - } } async function importGlobal(path, globalName) { diff --git a/utils/demo/helpers/labelmapTools.ts b/utils/demo/helpers/labelmapTools.ts index 287bd431f4..c2333c4260 100644 --- a/utils/demo/helpers/labelmapTools.ts +++ b/utils/demo/helpers/labelmapTools.ts @@ -19,16 +19,12 @@ const preview = { const configuration = { preview, - strategySpecificConfiguration: { - useCenterSegmentIndex: false, - }, + useCenterSegmentIndex: false, }; const configurationNoPreview = { preview: { enabled: false, previewColors }, - strategySpecificConfiguration: { - useCenterSegmentIndex: false, - }, + useCenterSegmentIndex: false, }; const thresholdOptions = new Map(); @@ -41,11 +37,11 @@ thresholdOptions.set('Use Existing Threshold', { dynamicRadius: 5, }); thresholdOptions.set('CT Fat: (-150, -70)', { - threshold: [-150, -70], + range: [-150, -70], isDynamic: false, }); thresholdOptions.set('CT Bone: (200, 1000)', { - threshold: [200, 1000], + range: [200, 1000], isDynamic: false, }); @@ -59,10 +55,7 @@ toolMap.set('ThresholdCircle', { configuration: { ...configuration, activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', - strategySpecificConfiguration: { - ...configuration.strategySpecificConfiguration, - THRESHOLD: { ...thresholdArgs }, - }, + threshold: thresholdArgs, }, }); @@ -71,10 +64,7 @@ toolMap.set('ThresholdSphere', { configuration: { ...configuration, activeStrategy: 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL', - strategySpecificConfiguration: { - ...configuration.strategySpecificConfiguration, - THRESHOLD: { ...thresholdArgs }, - }, + threshold: thresholdArgs, }, }); diff --git a/version.json b/version.json index e6e156b279..ddbebdfc11 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "version": "2.19.16", - "commit": "0dc51241392451a581de0372760ff6d5acf398c3" -} \ No newline at end of file + "version": "3.0.0-beta.6", + "commit": "a1fd3f9d0ea40d53cafd792d59bc1dbfc90663a5" +} diff --git a/version.txt b/version.txt index 464a55bd21..40f5e16e91 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.19.16 \ No newline at end of file +3.0.0-beta.6 diff --git a/yarn.lock b/yarn.lock index aaa0d1b385..539058be4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -444,6 +444,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz#9cbdd63a9443a2c92a725cca7ebca12cc8dd9f46" integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== +"@babel/helper-plugin-utils@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35" + integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg== + "@babel/helper-remap-async-to-generator@^7.24.7", "@babel/helper-remap-async-to-generator@^7.25.0": version "7.25.0" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz#d2f0fbba059a42d68e5e378feaf181ef6055365e" @@ -1607,14 +1612,14 @@ "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-runtime@^7.21.4": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz#00a5bfaf8c43cf5c8703a8a6e82b59d9c58f38ca" - integrity sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw== + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.9.tgz#ea8be19ef134668e98f7b54daf7c4f853859dc44" + integrity sha512-Jf+8y9wXQbbxvVYTM8gO5oEF2POdNji0NMltEkG7FtmzD9PVz7/lxpqSdTvwsjTMU5HIHuDVNf2SOxLkWi+wPQ== dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.26.5" babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.1" + babel-plugin-polyfill-corejs3 "^0.10.6" babel-plugin-polyfill-regenerator "^0.6.1" semver "^6.3.1" @@ -2011,14 +2016,22 @@ integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime-corejs2@^7.17.8": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.25.0.tgz#c1e677774a618f48caff129a8e2a066f1ecd0384" - integrity sha512-aoYVE3tm+vgAoezmXFWmVcp+NlSdsUqQMPL7c6zRxq8KDHCf570pamC7005Q/UkSlTuoL6oeE16zIw/9J3YFyw== + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.26.9.tgz#1936c9847aa09faaa941e06f48d3e391e5ca5e1b" + integrity sha512-DHEUkm9RD4RfIThlLTemmHNFVTj9Z/augVFMuyheFjwoFv1jFjauncHrcSW6Kv1TpzTupB01zFk2iRFAh2iE9A== dependencies: core-js "^2.6.12" regenerator-runtime "^0.14.0" -"@babel/runtime-corejs3@^7.15.4", "@babel/runtime-corejs3@^7.22.5": +"@babel/runtime-corejs3@^7.15.4": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.26.9.tgz#8b73bae47453aa3dd2839ac52598581a7dd8332f" + integrity sha512-5EVjbTegqN7RSJle6hMWYxO4voo4rI+9krITk+DWR+diJgGrjZjrIBnJhjrHYYQsFgI7j1w1QnrvV7YSKBfYGg== + dependencies: + core-js-pure "^3.30.2" + regenerator-runtime "^0.14.0" + +"@babel/runtime-corejs3@^7.22.5": version "7.25.0" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.25.0.tgz#0a318b66dfc765ad10562d829fea372ed7e1eb7d" integrity sha512-BOehWE7MgQ8W8Qn0CQnMtg2tHPHPulcS/5AVpFvs2KCK1ET+0WqZqPvnpRpFN81gYoFopdIEJX9Sgjw3ZBccPg== @@ -3777,7 +3790,7 @@ resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg== -"@icr/polyseg-wasm@^0.4.0": +"@icr/polyseg-wasm@0.4.0": version "0.4.0" resolved "https://registry.yarnpkg.com/@icr/polyseg-wasm/-/polyseg-wasm-0.4.0.tgz#755e23d07c3d8d8fca1113278c803c1ef0185da0" integrity sha512-3sZmiwG8I0NaqPle0L7+V/ZexiR7IjIUFkUsaOoFI9rNuBGyyMMmxAxnCmqcDFtBDk9h+JEYJf6e3NnqlHi/HQ== @@ -3878,12 +3891,12 @@ ipfs-car "^1.0.0" tar "^6.1.13" -"@itk-wasm/morphological-contour-interpolation@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@itk-wasm/morphological-contour-interpolation/-/morphological-contour-interpolation-1.0.1.tgz#c2fcfdc593df85001276918e8ef684ba0b73a3c9" - integrity sha512-wxLB4nX6CiWpNQyTWC7oeFXogiZbtmSuLhyAtY66sM0SEnMoOcAuSX2+osPcOo13rfYnHLA02uQiICp8hvUGwA== +"@itk-wasm/morphological-contour-interpolation@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@itk-wasm/morphological-contour-interpolation/-/morphological-contour-interpolation-1.1.0.tgz#a2982dc27cdcc27026b61e12f76e7687c88cad7e" + integrity sha512-n6JIyDcSCCjlpfCW8mnTTzwPTE8U1QT87hNmyAknxdpGR4dfAzIutuKNrwgvr9UiKEBcit0X3HNx9dkzDwcIcw== dependencies: - itk-wasm "1.0.0-b.165" + itk-wasm "1.0.0-b.173" "@jest/console@^29.7.0": version "29.7.0" @@ -8115,7 +8128,7 @@ babel-plugin-polyfill-corejs2@^0.4.10: "@babel/helper-define-polyfill-provider" "^0.6.2" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.10.1, babel-plugin-polyfill-corejs3@^0.10.4, babel-plugin-polyfill-corejs3@^0.10.6: +babel-plugin-polyfill-corejs3@^0.10.4, babel-plugin-polyfill-corejs3@^0.10.6: version "0.10.6" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz#2deda57caef50f59c525aeb4964d3b2f867710c7" integrity sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA== @@ -8130,6 +8143,14 @@ babel-plugin-polyfill-regenerator@^0.6.1: dependencies: "@babel/helper-define-polyfill-provider" "^0.6.2" +babel-plugin-transform-import-meta@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-2.3.2.tgz#320bd0cd878fe38f39e136edb198c535391bf156" + integrity sha512-902o4GiQqI1GqAXfD5rEoz0PJamUfJ3VllpdWaNsFTwdaNjFSFHawvBO+cp5K2j+g2h3bZ4lnM1Xb6yFYGihtA== + dependencies: + "@babel/template" "^7.25.9" + tslib "^2.8.1" + babel-preset-current-node-syntax@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz#9a929eafece419612ef4ae4f60b1862ebad8ef30" @@ -8737,15 +8758,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646: - version "1.0.30001651" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz#52de59529e8b02b1aedcaaf5c05d9e23c0c28138" - integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== - -caniuse-lite@^1.0.30001616, caniuse-lite@^1.0.30001669: - version "1.0.30001673" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz#5aa291557af1c71340e809987367410aab7a5a9e" - integrity sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001616, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: + version "1.0.30001699" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz" + integrity sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w== canvas@2.11.2, canvas@^2.11.2: version "2.11.2" @@ -9684,7 +9700,12 @@ core-js@^2.6.12: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== -core-js@^3.26.1, core-js@^3.31.1: +core-js@^3.26.1: + version "3.40.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.40.0.tgz#2773f6b06877d8eda102fc42f828176437062476" + integrity sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ== + +core-js@^3.31.1: version "3.38.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.0.tgz#8acb7c050bf2ccbb35f938c0d040132f6110f636" integrity sha512-XPpwqEodRljce9KswjZShh95qJ1URisBeKCjUdq27YdenkslVe7OO0ZJhlYXAChW7OhXaRLl8AAba7IBfoIHug== @@ -14922,6 +14943,24 @@ itk-wasm@1.0.0-b.165: mime-types "^2.1.35" wasm-feature-detect "^1.6.1" +itk-wasm@1.0.0-b.173: + version "1.0.0-b.173" + resolved "https://registry.yarnpkg.com/itk-wasm/-/itk-wasm-1.0.0-b.173.tgz#e484e1765f4205a5704f8fac205d2ecf74e726a2" + integrity sha512-SV2lfZ1mClWuSK/noaZgGj9jhroY4MZu19ci9pIucuyhoGdXrVSmWlPH/JYMDi9RP3BogmQwe9wfFc3X1dcEPg== + dependencies: + "@itk-wasm/dam" "^1.1.1" + "@thewtex/zstddec" "^0.2.0" + "@types/emscripten" "^1.39.10" + axios "^1.6.2" + chalk "^5.3.0" + comlink "^4.4.1" + commander "^11.1.0" + fs-extra "^11.2.0" + glob "^8.1.0" + markdown-table "^3.0.3" + mime-types "^2.1.35" + wasm-feature-detect "^1.6.1" + jackspeak@^3.1.2: version "3.4.3" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" @@ -23707,7 +23746,7 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0: +tslib@^2.0.0, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==