diff --git a/bun.lock b/bun.lock index cbef80b75a..17db6fe1dd 100644 --- a/bun.lock +++ b/bun.lock @@ -138,7 +138,7 @@ }, "packages/adapters": { "name": "@cornerstonejs/adapters", - "version": "2.19.10", + "version": "2.19.11", "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", @@ -147,13 +147,13 @@ "ndarray": "^1.0.19", }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.10", - "@cornerstonejs/tools": "^2.19.10", + "@cornerstonejs/core": "^2.19.11", + "@cornerstonejs/tools": "^2.19.11", }, }, "packages/ai": { "name": "@cornerstonejs/ai", - "version": "2.19.10", + "version": "2.19.11", "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", @@ -171,7 +171,7 @@ }, "packages/core": { "name": "@cornerstonejs/core", - "version": "2.19.10", + "version": "2.19.11", "dependencies": { "@kitware/vtk.js": "32.9.0", "comlink": "^4.4.1", @@ -180,7 +180,7 @@ }, "packages/dicomImageLoader": { "name": "@cornerstonejs/dicom-image-loader", - "version": "2.19.10", + "version": "2.19.11", "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", @@ -192,7 +192,7 @@ "uuid": "^9.0.0", }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.10", + "@cornerstonejs/core": "^2.19.11", "dicom-parser": "^1.8.9", }, }, @@ -200,11 +200,11 @@ "name": "docs", "version": "2.1.10", "dependencies": { - "@cornerstonejs/adapters": "^2.19.10", - "@cornerstonejs/core": "^2.19.10", - "@cornerstonejs/dicom-image-loader": "^2.19.10", - "@cornerstonejs/nifti-volume-loader": "^2.19.10", - "@cornerstonejs/tools": "^2.19.10", + "@cornerstonejs/adapters": "^2.19.11", + "@cornerstonejs/core": "^2.19.11", + "@cornerstonejs/dicom-image-loader": "^2.19.11", + "@cornerstonejs/nifti-volume-loader": "^2.19.11", + "@cornerstonejs/tools": "^2.19.11", "@docusaurus/core": "3.6.3", "@docusaurus/faster": "3.6.3", "@docusaurus/module-type-aliases": "3.6.3", @@ -240,19 +240,32 @@ "typedoc": "0.26.10", }, }, + "packages/labelmap-interpolation": { + "name": "@cornerstonejs/labelmap-interpolation", + "version": "2.19.11", + "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.10", + "version": "2.19.11", "dependencies": { "nifti-reader-js": "^0.6.8", }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.10", + "@cornerstonejs/core": "^2.19.11", }, }, "packages/tools": { "name": "@cornerstonejs/tools", - "version": "2.19.10", + "version": "2.19.11", "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", @@ -262,7 +275,7 @@ "canvas": "^2.11.2", }, "peerDependencies": { - "@cornerstonejs/core": "^2.19.10", + "@cornerstonejs/core": "^2.19.11", "@kitware/vtk.js": "32.9.0", "@types/d3-array": "^3.0.4", "@types/d3-interpolate": "^3.0.1", @@ -601,6 +614,8 @@ "@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/tools": ["@cornerstonejs/tools@workspace:packages/tools"], @@ -869,7 +884,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=="], @@ -4843,7 +4858,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=="], @@ -5523,6 +5538,8 @@ "@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@externals/polyseg-wasm/@itk-wasm/morphological-contour-interpolation": ["@itk-wasm/morphological-contour-interpolation@1.0.1", "", { "dependencies": { "itk-wasm": "1.0.0-b.165" } }, "sha512-wxLB4nX6CiWpNQyTWC7oeFXogiZbtmSuLhyAtY66sM0SEnMoOcAuSX2+osPcOo13rfYnHLA02uQiICp8hvUGwA=="], + "@fastify/ajv-compiler/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], "@fastify/ajv-compiler/fast-uri": ["fast-uri@2.4.0", "", {}, "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA=="], @@ -5539,6 +5556,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 +5844,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=="], @@ -6005,8 +6026,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 +6870,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=="], @@ -7211,6 +7228,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 +7494,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=="], @@ -8385,6 +8412,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=="], @@ -8909,6 +8938,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 +9076,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/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/tools.api.md b/common/reviews/api/tools.api.md index 39ab0f21a1..e34fc2f3aa 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -757,9 +757,10 @@ class BasicStatsCalculator_2 extends Calculator { unit: string; }) => NamedStatistics; // (undocumented) - static statsCallback: ({ value: newValue, pointLPS }: { + static statsCallback: ({ value: newValue, pointLPS, pointIJK, }: { value: any; pointLPS?: any; + pointIJK?: any; }) => void; // (undocumented) static statsInit(options: { @@ -1004,6 +1005,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) @@ -2819,12 +2824,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 +2914,13 @@ function getStackSegmentationImageIdsForViewport(viewportId: string, segmentatio // @public (undocumented) function getState(annotation?: Annotation): AnnotationStyleStates; +// @public (undocumented) +function getStatistics({ segmentationId, segmentIndices, viewportId, }: { + segmentationId: string; + segmentIndices: number[] | number; + viewportId: string; +}): Promise; + // @public (undocumented) function getStyle(specifier: SpecifierWithType): StyleForType; @@ -3512,12 +3530,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; @@ -3574,6 +3591,7 @@ type LabelmapToolOperationData = { viewPlaneNormal: number[]; viewUp: number[]; strategySpecificConfiguration: any; + activeStrategy: string; points: Types_2.Point3[]; voxelManager: any; override: { @@ -5281,7 +5299,9 @@ declare namespace segmentation_2 { getBrushToolInstances, growCut, LabelmapMemo, - IslandRemoval + IslandRemoval, + getOrCreateSegmentationVolume, + getStatistics } } @@ -5889,6 +5909,8 @@ type Statistics = { label?: string; value: number | number[]; unit: null | string; + pointIJK?: Types_2.Point3; + pointLPS?: Types_2.Point3; }; // @public (undocumented) @@ -5931,6 +5953,10 @@ enum StrategyCallbacks { // (undocumented) CreateIsInThreshold = "createIsInThreshold", // (undocumented) + EnsureImageVolumeFor3DManipulation = "ensureImageVolumeFor3DManipulation", + // (undocumented) + EnsureSegmentationVolumeFor3DManipulation = "ensureSegmentationVolumeFor3DManipulation", + // (undocumented) Fill = "fill", // (undocumented) GetStatistics = "getStatistics", @@ -6711,6 +6737,7 @@ declare namespace utilities { getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, + getPixelValueUnitsImageId, segmentation_2 as segmentation, contours, triggerAnnotationRenderForViewportIds, diff --git a/lerna.json b/lerna.json index fc3eb5ca71..8bb163a473 100644 --- a/lerna.json +++ b/lerna.json @@ -6,7 +6,8 @@ "packages/adapters", "packages/nifti-volume-loader", "packages/dicomImageLoader", - "packages/ai" + "packages/ai", + "packages/labelmap-interpolation" ], "npmClient": "yarn" } diff --git a/package.json b/package.json index c8086e38e0..316ab73af4 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,12 @@ "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: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", "build:update-api:dicomImageLoader": "cd packages/dicomImageLoader && npm run build:update-api", - "build:update-api": "npm run build && npm run build:update-api:ai && npm run build:update-api:core && npm run build:update-api:tools && npm run build:update-api:nifti && npm run build:update-api:dicomImageLoader", + "build:update-api": "npm run build && npm run build:update-api:ai && npm run build:update-api:core && npm run build:update-api:tools && npm run build:update-api:nifti && npm run build:update-api:dicomImageLoader && npm run build:update-api:labelmap-interpolation", "clean": "npx lerna run clean --stream", "clean:deep": "npx lerna run clean:deep --stream", "example": "node ./utils/ExampleRunner/example-runner-cli.js", @@ -184,5 +185,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/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/utilities/VoxelManager.ts b/packages/core/src/utilities/VoxelManager.ts index e805e5b4c1..6075c3b3ca 100644 --- a/packages/core/src/utilities/VoxelManager.ts +++ b/packages/core/src/utilities/VoxelManager.ts @@ -893,7 +893,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++) { 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/labelmap-interpolation/CHANGELOG.md b/packages/labelmap-interpolation/CHANGELOG.md new file mode 100644 index 0000000000..420e6f23d0 --- /dev/null +++ b/packages/labelmap-interpolation/CHANGELOG.md @@ -0,0 +1 @@ +# Change Log diff --git a/packages/labelmap-interpolation/README.md b/packages/labelmap-interpolation/README.md new file mode 100644 index 0000000000..6004e30e95 --- /dev/null +++ b/packages/labelmap-interpolation/README.md @@ -0,0 +1,5 @@ +# Cornerstone Segmentation Labelmap Interpolation + + +This package provides a utility for interpolating labelmaps in 3D. It has dependencies +on `itk-wasm` and `@itk-wasm/morphological-contour-interpolation`. 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/tools/examples/labelmapInterpolation/index.ts b/packages/labelmap-interpolation/examples/labelmapInterpolation/index.ts similarity index 94% rename from packages/tools/examples/labelmapInterpolation/index.ts rename to packages/labelmap-interpolation/examples/labelmapInterpolation/index.ts index 1a477f0808..51da202d76 100644 --- a/packages/tools/examples/labelmapInterpolation/index.ts +++ b/packages/labelmap-interpolation/examples/labelmapInterpolation/index.ts @@ -16,6 +16,7 @@ import { addManipulationBindings, } from '../../../../utils/demo/helpers'; import * as cornerstoneTools from '@cornerstonejs/tools'; +import * as labelmapInterpolation from '@cornerstonejs/labelmap-interpolation'; // This is for debugging purposes console.warn( @@ -162,20 +163,12 @@ addDropdownToToolbar({ 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 }); + const activeSegmentIndex = + segmentation.segmentIndex.getActiveSegmentIndex(segmentationId); + labelmapInterpolation.interpolate({ + segmentationId, + segmentIndex: activeSegmentIndex, + }); }, }); diff --git a/packages/labelmap-interpolation/package.json b/packages/labelmap-interpolation/package.json new file mode 100644 index 0000000000..01347c403b --- /dev/null +++ b/packages/labelmap-interpolation/package.json @@ -0,0 +1,54 @@ +{ + "name": "@cornerstonejs/labelmap-interpolation", + "version": "2.19.11", + "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", + "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", + "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 ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/dcmjs-org/dcmjs.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": "1.0.0-b.165", + "@itk-wasm/morphological-contour-interpolation": "1.1.0" + }, + "peerDependencies": { + "@cornerstonejs/tools": "^2.19.11", + "@cornerstonejs/core": "^2.19.11", + "@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..7203472a26 --- /dev/null +++ b/packages/labelmap-interpolation/src/workers/interpolationWorker.js @@ -0,0 +1,172 @@ +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, + } + ); + + 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/tools/examples/labelmapSegmentationDynamicThreshold/index.ts b/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts index b96a751704..749728e001 100644 --- a/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts +++ b/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts @@ -120,7 +120,7 @@ interpolationTools.set('ThresholdSphereIsland', { ...configuration, activeStrategy: 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL', strategySpecificConfiguration: { - THRESHOLD: { ...thresholdArgs }, + THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL: { ...thresholdArgs }, }, }, }); @@ -131,7 +131,7 @@ interpolationTools.set('ThresholdCircle', { ...configuration, activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', strategySpecificConfiguration: { - THRESHOLD: { ...thresholdArgs }, + THRESHOLD_INSIDE_CIRCLE: { ...thresholdArgs }, }, }, }); @@ -142,7 +142,7 @@ interpolationTools.set('ThresholdSphere', { ...configuration, activeStrategy: 'THRESHOLD_INSIDE_SPHERE', strategySpecificConfiguration: { - THRESHOLD: { ...thresholdArgs }, + THRESHOLD_INSIDE_SPHERE: { ...thresholdArgs }, }, }, }); diff --git a/packages/tools/examples/labelmapStatistics/index.ts b/packages/tools/examples/labelmapStatistics/index.ts index 082fc530df..d8b9064836 100644 --- a/packages/tools/examples/labelmapStatistics/index.ts +++ b/packages/tools/examples/labelmapStatistics/index.ts @@ -13,7 +13,6 @@ import { addDropdownToToolbar, addSliderToToolbar, setCtTransferFunctionForVolumeActor, - getLocalUrl, addManipulationBindings, } from '../../../../utils/demo/helpers'; import * as cornerstoneTools from '@cornerstonejs/tools'; @@ -146,14 +145,32 @@ thresholdOptions.set('CT Bone: (200, 1000)', { const defaultThresholdOption = [...thresholdOptions.keys()][2]; const thresholdArgs = thresholdOptions.get(defaultThresholdOption); +interpolationTools.set('CircularBrush', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'FILL_INSIDE_CIRCLE', + }, +}); + +interpolationTools.set('ThresholdCircle', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', + strategySpecificConfiguration: { + THRESHOLD_INSIDE_CIRCLE: { ...thresholdArgs }, + }, + }, +}); + interpolationTools.set('ThresholdSphere', { baseTool: BrushTool.toolName, configuration: { ...configuration, activeStrategy: 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL', strategySpecificConfiguration: { - useCenterSegmentIndex: true, - THRESHOLD: { ...thresholdArgs }, + THRESHOLD_INSIDE_SPHERE: { ...thresholdArgs }, }, }, }); @@ -263,12 +280,13 @@ function displayStat(stat) { }`; } -function calculateStatistics(id, indices) { +async 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 }); + const stats = await segmentationUtils.getStatistics({ + segmentationId, + segmentIndices: indices, + viewportId: viewport.id, + }); if (!stats) { return; @@ -286,7 +304,8 @@ function calculateStatistics(id, indices) { // 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'); @@ -393,13 +412,28 @@ async function run() { }); // 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: + // getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + // }); 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.879445243400782656317561081015', + wadoRsRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb', + }); + + const ctImageIds = 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: - getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRsRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb', }); // Define a volume in memory @@ -463,7 +497,7 @@ async function run() { // Set volumes on the viewports await setVolumesForViewports( renderingEngine, - [{ volumeId, callback: setCtTransferFunctionForVolumeActor }], + [{ volumeId }], [viewportId1, viewportId2, viewportId3] ); diff --git a/packages/tools/examples/stackLabelmapSegmentation/index.ts b/packages/tools/examples/stackLabelmapSegmentation/index.ts index be326f1957..039c21280f 100644 --- a/packages/tools/examples/stackLabelmapSegmentation/index.ts +++ b/packages/tools/examples/stackLabelmapSegmentation/index.ts @@ -1,5 +1,4 @@ import { Enums, RenderingEngine, imageLoader } from '@cornerstonejs/core'; -import * as cornerstone from '@cornerstonejs/core'; import * as cornerstoneTools from '@cornerstonejs/tools'; import { createImageIdsAndCacheMetaData, @@ -9,7 +8,6 @@ import { addButtonToToolbar, addBrushSizeSlider, } from '../../../../utils/demo/helpers'; -import { fillStackSegmentationWithMockData } from '../../../../utils/test/testUtils'; // This is for debugging purposes console.warn( @@ -85,21 +83,30 @@ content.append(instructions); const brushInstanceNames = { CircularBrush: 'CircularBrush', CircularEraser: 'CircularEraser', - ThresholdBrush: 'ThresholdBrush', + ThresholdBrushCircle: 'ThresholdBrushCircle', + ThresholdBrushSphere: 'ThresholdBrushSphere', DynamicThreshold: 'DynamicThreshold', }; const brushStrategies = { [brushInstanceNames.CircularBrush]: 'FILL_INSIDE_CIRCLE', [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', }; const brushValues = [ brushInstanceNames.CircularBrush, brushInstanceNames.CircularEraser, - brushInstanceNames.ThresholdBrush, + brushInstanceNames.ThresholdBrushCircle, + brushInstanceNames.ThresholdBrushSphere, + brushInstanceNames.DynamicThreshold, +]; + +const thresholdBrushValues = [ + brushInstanceNames.ThresholdBrushCircle, + brushInstanceNames.ThresholdBrushSphere, brushInstanceNames.DynamicThreshold, ]; @@ -154,19 +161,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 }], }); } @@ -193,7 +205,7 @@ addDropdownToToolbar({ segmentationUtils.setBrushThresholdForToolGroup(toolGroupId, threshold); }, -}); +}).style.display = 'none'; addButtonToToolbar({ title: 'Create New Segmentation on Current Image', @@ -243,7 +255,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 @@ -283,6 +294,7 @@ function setupTools(toolGroupId) { activeStrategy: brushStrategies.CircularBrush, } ); + toolGroup.addToolInstance( brushInstanceNames.CircularEraser, BrushTool.toolName, @@ -290,25 +302,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, } ); @@ -341,8 +348,7 @@ function setupTools(toolGroupId) { toolGroup.setToolActive(StackScrollTool.toolName, { bindings: [ { - mouseButton: MouseBindings.Primary, - modifierKey: KeyboardBindings.Alt, + mouseButton: MouseBindings.Wheel, }, ], }); @@ -396,7 +402,7 @@ async function run() { toolGroup.addViewport(viewportId, renderingEngineId); viewport = renderingEngine.getViewport(viewportId); - const imageIdsArray = [imageIds[0], imageIds[1], mgImageIds[0]]; + const imageIdsArray = [imageIds[0], imageIds[1]]; const segImages = await imageLoader.createAndCacheDerivedLabelmapImages( imageIdsArray @@ -404,16 +410,10 @@ async function run() { const viewport2 = renderingEngine.getViewport(viewportId2); await viewport.setStack(imageIdsArray, 0); - await viewport2.setStack([imageIdsArray[2]], 0); + await viewport2.setStack([mgImageIds[0]], 0); cornerstoneTools.utilities.stackContextPrefetch.enable(element1); cornerstoneTools.utilities.stackContextPrefetch.enable(element2); - // fillStackSegmentationWithMockData({ - // imageIds: [imageIds[0]], - // segmentationImageIds: segImages.map((it) => it.imageId), - // cornerstone, - // }); - renderingEngine.renderViewports([viewportId]); segmentation.addSegmentations([ diff --git a/packages/tools/examples/videoSegmentation/index.ts b/packages/tools/examples/videoSegmentation/index.ts index 08839726fd..d1dc1d7608 100644 --- a/packages/tools/examples/videoSegmentation/index.ts +++ b/packages/tools/examples/videoSegmentation/index.ts @@ -166,8 +166,11 @@ function setupTools(toolGroupId) { enabled: true, }, strategySpecificConfiguration: { - useCenterSegmentIndex: true, - THRESHOLD: { isDynamic: true, dynamicRadius: 3 }, + [brushStrategies.DynamicThreshold]: { + useCenterSegmentIndex: true, + isDynamic: true, + dynamicRadius: 3, + }, }, } ); 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/stateManagement/segmentation/polySeg/registerPolySegWorker.ts b/packages/tools/src/stateManagement/segmentation/polySeg/registerPolySegWorker.ts index 400f6c954c..531871e44e 100644 --- a/packages/tools/src/stateManagement/segmentation/polySeg/registerPolySegWorker.ts +++ b/packages/tools/src/stateManagement/segmentation/polySeg/registerPolySegWorker.ts @@ -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/tools/segmentation/BrushTool.ts b/packages/tools/src/tools/segmentation/BrushTool.ts index 2482a991b1..a04ec64e20 100644 --- a/packages/tools/src/tools/segmentation/BrushTool.ts +++ b/packages/tools/src/tools/segmentation/BrushTool.ts @@ -72,11 +72,7 @@ class BrushTool extends LabelmapBaseTool { thresholdInsideSphereIsland, }, - strategySpecificConfiguration: { - THRESHOLD: { - threshold: [-150, -70], // E.g. CT Fat // Only used during threshold strategies. - }, - }, + strategySpecificConfiguration: {}, defaultStrategy: 'FILL_INSIDE_CIRCLE', activeStrategy: 'FILL_INSIDE_CIRCLE', thresholdVolumeId: null, diff --git a/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts b/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts index 32542b19dc..fda3160cdf 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[ @@ -243,67 +237,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,6 +326,7 @@ export default class LabelmapBaseTool extends BaseTool { toolGroupId: this.toolGroupId, segmentationId, viewUp, + activeStrategy: this.configuration.activeStrategy, strategySpecificConfiguration: this.configuration.strategySpecificConfiguration, // Provide the preview information so that data can be used directly 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..8a3037306b 100644 --- a/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts +++ b/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts @@ -33,6 +33,7 @@ export type InitializedOperationData = LabelmapToolOperationDataAny & { previewSegmentIndex?: number; brushStrategy: BrushStrategy; + activeStrategy: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any configuration?: Record; memo?: LabelmapMemo; @@ -66,13 +67,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 +120,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 @@ -227,7 +235,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 +250,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 +263,8 @@ export default class BrushStrategy { ...operationData, enabledElement, imageVoxelManager, - segmentationVoxelManager: segmentationVoxelManagerToUse, - segmentationImageData: segmentationImageDataToUse, + segmentationVoxelManager, + segmentationImageData, previewVoxelManager, viewport, centerWorld: null, diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts b/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts index 1a801a9f71..4caa90a74d 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts @@ -20,12 +20,13 @@ export default { strategySpecificConfiguration, segmentationVoxelManager, imageVoxelManager, + activeStrategy, segmentIndex, viewport, } = operationData; - const { THRESHOLD } = strategySpecificConfiguration; + const config = strategySpecificConfiguration[activeStrategy] || {}; - if (!THRESHOLD?.isDynamic || !centerIJK || !segmentIndex) { + if (!config?.isDynamic || !centerIJK || !segmentIndex) { return; } if ( @@ -36,7 +37,7 @@ export default { } const boundsIJK = segmentationVoxelManager.getBoundsIJK(); - const { threshold: oldThreshold, dynamicRadius = 0 } = THRESHOLD; + const { threshold: oldThreshold, dynamicRadius = 0 } = config; const useDelta = oldThreshold ? 0 : dynamicRadius; const { viewPlaneNormal } = viewport.getCamera(); @@ -73,17 +74,21 @@ export default { }; imageVoxelManager.forEach(callback, { boundsIJK: nestedBounds }); - operationData.strategySpecificConfiguration.THRESHOLD.threshold = threshold; + config.threshold = threshold; }, // Setup a clear threshold value on mouse/touch down [StrategyCallbacks.OnInteractionStart]: ( operationData: InitializedOperationData ) => { - const { strategySpecificConfiguration, preview } = operationData; - if (!strategySpecificConfiguration?.THRESHOLD?.isDynamic && !preview) { + const { strategySpecificConfiguration, preview, activeStrategy } = + operationData; + + const config = strategySpecificConfiguration[activeStrategy] || {}; + if (!config?.isDynamic && !preview) { return; } - strategySpecificConfiguration.THRESHOLD.threshold = null; + + config.threshold = null; }, /** * It computes the inner circle radius in canvas coordinates and stores it @@ -94,8 +99,9 @@ export default { operationData: InitializedOperationData ) => { const { configuration, viewport } = operationData; - const { THRESHOLD: { dynamicRadius = 0 } = {} } = - configuration.strategySpecificConfiguration || {}; + const { strategySpecificConfiguration, activeStrategy } = configuration; + const { dynamicRadius = 0 } = + strategySpecificConfiguration[activeStrategy] || {}; if (dynamicRadius === 0) { return; @@ -125,11 +131,6 @@ 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] = {}; } 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..6883daaee1 --- /dev/null +++ b/packages/tools/src/tools/segmentation/strategies/compositions/ensureImageVolume.ts @@ -0,0 +1,32 @@ +import { cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core'; +import StrategyCallbacks from '../../../../enums/StrategyCallbacks'; + +export default { + [StrategyCallbacks.EnsureImageVolumeFor3DManipulation]: (data) => { + const { operationData, viewport } = data; + + const referencedImageIds = viewport.getImageIds(); + const isValidVolumeForSphere = csUtils.isValidVolume(referencedImageIds); + if (!isValidVolumeForSphere) { + throw new Error('Volume is not reconstructable for sphere manipulation'); + } + + 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..039f361688 --- /dev/null +++ b/packages/tools/src/tools/segmentation/strategies/compositions/ensureSegmentationVolume.ts @@ -0,0 +1,22 @@ +import { utilities } 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 } = operationData; + + const referencedImageIds = viewport.getImageIds(); + 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..ff04e80cfd 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,12 @@ 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, + viewportId: viewport.id, }); - - 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/threshold.ts b/packages/tools/src/tools/segmentation/strategies/compositions/threshold.ts index f4fb26c09f..146618903a 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/threshold.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/threshold.ts @@ -12,22 +12,24 @@ export default { [StrategyCallbacks.CreateIsInThreshold]: ( operationData: InitializedOperationData ) => { - const { imageVoxelManager, strategySpecificConfiguration, segmentIndex } = - operationData; + const { + imageVoxelManager, + strategySpecificConfiguration, + segmentIndex, + activeStrategy, + } = operationData; + if (!strategySpecificConfiguration || !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 || {}; + const { threshold } = strategySpecificConfiguration[activeStrategy] || {}; + if (!threshold?.length) { return true; } 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..ecb47a2f4e 100644 --- a/packages/tools/src/tools/segmentation/strategies/utils/getStrategyData.ts +++ b/packages/tools/src/tools/segmentation/strategies/utils/getStrategyData.ts @@ -8,77 +8,133 @@ import type { LabelmapToolOperationDataStack } 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 labelmapImageId = getCurrentLabelmapImageIdForViewport( - viewport.id, - segmentationId - ); - if (!labelmapImageId) { - return; - } + const { imageData: segmentationImageData } = segmentationVolume; - const currentImageId = viewport.getCurrentImageId(); - if (!currentImageId) { - return; - } + return { + segmentationImageData, + segmentationVoxelManager, + segmentationScalarData: null, + imageScalarData: null, + imageVoxelManager, + imageData, + }; +} - const actorEntry = getLabelmapActorEntry(viewport.id, segmentationId); +/** + * 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; + + const labelmapImageId = getCurrentLabelmapImageIdForViewport( + viewport.id, + segmentationId + ); + if (!labelmapImageId) { + return null; + } - if (!actorEntry) { - return; - } + const currentImageId = viewport.getCurrentImageId(); + if (!currentImageId) { + return null; + } + + const actorEntry = getLabelmapActorEntry(viewport.id, segmentationId); + + if (!actorEntry) { + return null; + } + 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 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 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 +144,26 @@ 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 }) { + if (viewport instanceof BaseVolumeViewport) { + return getStrategyDataForVolumeViewport({ operationData }); + } + + return getStrategyDataForStackViewport({ operationData, viewport, strategy }); +} + export { getStrategyData }; 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/LabelmapToolOperationData.ts b/packages/tools/src/types/LabelmapToolOperationData.ts index 52e7c9acd6..66dc30e6f0 100644 --- a/packages/tools/src/types/LabelmapToolOperationData.ts +++ b/packages/tools/src/types/LabelmapToolOperationData.ts @@ -19,7 +19,7 @@ type LabelmapToolOperationData = { 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/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..cd4f16ae50 100644 --- a/packages/tools/src/utilities/index.ts +++ b/packages/tools/src/utilities/index.ts @@ -50,7 +50,10 @@ 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'; export { math, @@ -69,6 +72,7 @@ export { getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, + getPixelValueUnitsImageId, segmentation, contours, triggerAnnotationRenderForViewportIds, 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..9710b939fd 100644 --- a/packages/tools/src/utilities/segmentation/brushThresholdForToolGroup.ts +++ b/packages/tools/src/utilities/segmentation/brushThresholdForToolGroup.ts @@ -1,7 +1,6 @@ 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( @@ -22,8 +21,14 @@ export function setBrushThresholdForToolGroup( }; brushBasedToolInstances.forEach((tool) => { - tool.configuration.strategySpecificConfiguration.THRESHOLD = { - ...tool.configuration.strategySpecificConfiguration.THRESHOLD, + const activeStrategy = tool.configuration.activeStrategy; + + if (!activeStrategy.toLowerCase().includes('threshold')) { + return; + } + + tool.configuration.strategySpecificConfiguration[activeStrategy] = { + ...tool.configuration.strategySpecificConfiguration[activeStrategy], ...configuration, }; }); @@ -35,14 +40,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); } diff --git a/packages/tools/src/utilities/segmentation/getOrCreateSegmentationVolume.ts b/packages/tools/src/utilities/segmentation/getOrCreateSegmentationVolume.ts new file mode 100644 index 0000000000..ba7b4ef6e2 --- /dev/null +++ b/packages/tools/src/utilities/segmentation/getOrCreateSegmentationVolume.ts @@ -0,0 +1,44 @@ +import { cache, volumeLoader, utilities } 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..fb191c4542 --- /dev/null +++ b/packages/tools/src/utilities/segmentation/getStatistics.ts @@ -0,0 +1,225 @@ +import { + getEnabledElementByViewportId, + utilities, + getWebWorkerManager, + eventTarget, + Enums, + triggerEvent, +} 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 { AnnotationTool } from '../../tools/base'; +import { isViewportPreScaled } from '../viewport/isViewportPreScaled'; +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, + viewportId, +}: { + segmentationId: string; + segmentIndices: number[] | number; + viewportId: string; +}) { + registerComputeWorker(); + + triggerWorkerProgress(eventTarget, 0); + + const enabledElement = getEnabledElementByViewportId(viewportId); + const viewport = enabledElement.viewport; + + const segmentation = getSegmentation(segmentationId); + const { representationData } = segmentation; + + const { Labelmap } = representationData; + + if (!Labelmap) { + console.debug('No labelmap found for segmentation', segmentationId); + return; + } + + const { volumeId } = Labelmap as LabelmapSegmentationDataVolume; + const { imageIds } = Labelmap as LabelmapSegmentationDataStack; + + const { + segmentationVoxelManager, + imageVoxelManager, + segmentationImageData, + imageData, + } = getStrategyData({ + operationData: { segmentationId, viewport, volumeId, imageIds }, + viewport, + strategy: { + ensureSegmentationVolumeFor3DManipulation: + ensureSegmentationVolume.ensureSegmentationVolumeFor3DManipulation, + ensureImageVolumeFor3DManipulation: + ensureImageVolume.ensureImageVolumeFor3DManipulation, + }, + }); + + let indices = segmentIndices; + + if (!indices) { + indices = [getActiveSegmentIndex(segmentationId)]; + } else if (!Array.isArray(indices)) { + // Include the preview index + indices = [indices, 255]; + } + + 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 indicesArr = indices as number[]; + + const stats = await workerManager.executeTask( + 'compute', + 'calculateSegmentsStatistics', + { + segmentationInfo, + imageInfo, + indices: indicesArr, + } + ); + + triggerWorkerProgress(eventTarget, 100); + + const targetId = viewport.getViewReferenceId(); + const modalityUnitOptions = { + isPreScaled: isViewportPreScaled(viewport, targetId), + isSuvScaled: AnnotationTool.isSuvScaled( + viewport, + targetId, + viewport.getCurrentImageId() + ), + }; + + const unit = getPixelValueUnitsImageId( + viewport.getCurrentImageId(), + modalityUnitOptions + ); + + // 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; +} + +/** + * 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 }); +} + +export default getStatistics; diff --git a/packages/tools/src/utilities/segmentation/index.ts b/packages/tools/src/utilities/segmentation/index.ts index 1352a952a3..bd2e76a65b 100644 --- a/packages/tools/src/utilities/segmentation/index.ts +++ b/packages/tools/src/utilities/segmentation/index.ts @@ -29,6 +29,8 @@ 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'; export { thresholdVolumeByRange, @@ -56,4 +58,6 @@ export { growCut, LabelmapMemo, IslandRemoval, + getOrCreateSegmentationVolume, + getStatistics, }; diff --git a/packages/tools/src/workers/computeWorker.js b/packages/tools/src/workers/computeWorker.js new file mode 100644 index 0000000000..3a79f10e9a --- /dev/null +++ b/packages/tools/src/workers/computeWorker.js @@ -0,0 +1,52 @@ +import { expose } from 'comlink'; +import VolumetricCalculator from '../utilities/segmentation/VolumetricCalculator'; + +const computeWorker = { + calculateSegmentsStatistics: (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'); + } + + for (let i = 0; i < segmentationScalarData.length; i++) { + const segmentationValue = segmentationScalarData[i]; + + if (indices.indexOf(segmentationValue) === -1) { + continue; + } + const imageValue = imageScalarData[i]; + + VolumetricCalculator.statsCallback({ + value: imageValue, + pointIJK: [ + i % segmentationDimensions[0], + Math.floor(i / segmentationDimensions[0]) % segmentationDimensions[1], + Math.floor(i / segmentationDimensions[0] / segmentationDimensions[1]), + ], + }); + } + + const stats = VolumetricCalculator.getStatistics({ + spacing: segmentationSpacing, + unit: 'mm', + }); + + return stats; + }, +}; + +expose(computeWorker); diff --git a/packages/tools/src/workers/polySegConverters.js b/packages/tools/src/workers/polySegConverters.js index a2d2b5370c..cabadf3e9f 100644 --- a/packages/tools/src/workers/polySegConverters.js +++ b/packages/tools/src/workers/polySegConverters.js @@ -16,6 +16,22 @@ import { import { isPlaneIntersectingAABB } from '../utilities/planar'; import { checkStandardBasis, rotatePoints } from '../geometricSurfaceUtils'; +async function peerImport(moduleId) { + try { + if (moduleId === '@icr/polyseg-wasm') { + try { + return import('@icr/polyseg-wasm'); + } catch (error) { + console.warn('Error importing @icr/polyseg-wasm:', error); + return null; + } + } + } catch (error) { + console.warn('Error in peerImport:', error); + return null; + } +} + /** * Object containing methods for converting between different representations of * segmentations (e.g., contour, labelmap, surface, etc.) These logics @@ -43,7 +59,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/tsconfig.json b/tsconfig.json index 93c8a099e8..184fc3050f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "@cornerstonejs/dicomImageLoader": ["dicomImageLoader/src"], "@cornerstonejs/nifti-volume-loader": ["nifti-volume-loader/src"], "@cornerstonejs/ai": ["ai/src"], + "@cornerstonejs/labelmap-interpolation": ["labelmap-interpolation/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..e43488f2b7 100644 --- a/utils/ExampleRunner/build-all-examples-cli.js +++ b/utils/ExampleRunner/build-all-examples-cli.js @@ -56,6 +56,7 @@ 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/dicomImageLoader/examples', regexp: 'index.ts', diff --git a/utils/ExampleRunner/example-runner-cli.js b/utils/ExampleRunner/example-runner-cli.js index ea86e83d16..49c489fa7c 100755 --- a/utils/ExampleRunner/example-runner-cli.js +++ b/utils/ExampleRunner/example-runner-cli.js @@ -111,6 +111,7 @@ 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/dicomImageLoader/examples', regexp: 'index.ts', diff --git a/utils/ExampleRunner/template-config.js b/utils/ExampleRunner/template-config.js index 057c2daa2f..d39a38299b 100644 --- a/utils/ExampleRunner/template-config.js +++ b/utils/ExampleRunner/template-config.js @@ -3,6 +3,9 @@ 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 csAdapters = path.resolve('packages/adapters/src/index'); const csDICOMImageLoaderDistPath = path.resolve( 'packages/dicomImageLoader/src/index' @@ -67,6 +70,10 @@ module.exports = { '@cornerstonejs/core': '${csRenderBasePath.replace(/\\/g, '/')}', '@cornerstonejs/tools': '${csToolsBasePath.replace(/\\/g, '/')}', '@cornerstonejs/ai': '${csAiBasePath.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..a2334aed87 100644 --- a/utils/ExampleRunner/template-multiexample-config.js +++ b/utils/ExampleRunner/template-multiexample-config.js @@ -3,6 +3,9 @@ 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 csAdaptersBasePath = path.resolve('./packages/adapters/src/index'); const csDICOMImageLoaderDistPath = path.resolve( 'packages/dicomImageLoader/src/index' @@ -102,6 +105,10 @@ module.exports = { '@cornerstonejs/core': '${csRenderBasePath.replace(/\\/g, '/')}', '@cornerstonejs/tools': '${csToolsBasePath.replace(/\\/g, '/')}', '@cornerstonejs/ai': '${csAiBasePath.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.js index 59959148b1..73c4ad707b 100644 --- a/utils/demo/helpers/initDemo.js +++ b/utils/demo/helpers/initDemo.js @@ -47,18 +47,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..384a22196c 100644 --- a/utils/demo/helpers/labelmapTools.ts +++ b/utils/demo/helpers/labelmapTools.ts @@ -61,7 +61,7 @@ toolMap.set('ThresholdCircle', { activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', strategySpecificConfiguration: { ...configuration.strategySpecificConfiguration, - THRESHOLD: { ...thresholdArgs }, + THRESHOLD_INSIDE_CIRCLE: { ...thresholdArgs }, }, }, }); @@ -73,7 +73,7 @@ toolMap.set('ThresholdSphere', { activeStrategy: 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL', strategySpecificConfiguration: { ...configuration.strategySpecificConfiguration, - THRESHOLD: { ...thresholdArgs }, + THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL: { ...thresholdArgs }, }, }, }); diff --git a/yarn.lock b/yarn.lock index aaa0d1b385..529d0bdd1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3885,6 +3885,13 @@ dependencies: itk-wasm "1.0.0-b.165" +"@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.173" + "@jest/console@^29.7.0": version "29.7.0" resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" @@ -8737,15 +8744,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" @@ -14922,6 +14924,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"