From eaeefe79a079bca16b4fe2bb6fe2cddc338893f7 Mon Sep 17 00:00:00 2001 From: Alireza Date: Mon, 1 May 2023 22:11:19 -0400 Subject: [PATCH 01/20] chore(adapters): ability to cache and build ESM modules --- nx.json | 2 +- package.json | 2 + packages/adapters/config/webpack/index.js | 4 - packages/adapters/config/webpack/merge.js | 15 -- .../adapters/config/webpack/webpack-base.js | 30 --- .../adapters/config/webpack/webpack-dev.js | 16 -- .../adapters/config/webpack/webpack-prod.js | 16 -- packages/adapters/examples/index.html | 21 -- .../examples/segmentationExport/index.ts | 250 ++++++++++++++++++ packages/adapters/netlify.toml | 20 -- .../cornerstoneDICOMSR/index.html | 0 packages/adapters/package.json | 21 +- packages/adapters/rollup.config.mjs | 11 +- .../src/adapters/Cornerstone/index.js | 12 +- .../adapters/Cornerstone3D/Segmentation.ts | 237 +++++++++++++++++ .../src/adapters/Cornerstone3D/index.ts | 9 +- .../src/adapters/VTKjs/Segmentation.js | 4 +- packages/adapters/src/adapters/VTKjs/index.js | 4 +- .../src/adapters/{helpers.js => helpers.ts} | 0 packages/adapters/src/adapters/index.js | 11 - packages/adapters/src/adapters/index.ts | 16 ++ packages/adapters/src/index.ts | 5 +- packages/adapters/tsconfig.json | 12 +- packages/core/package.json | 1 + packages/dicomImageLoader/package.json | 1 - packages/docs/package.json | 2 +- .../package.json | 6 +- .../tools/examples/labelmapRendering/index.ts | 31 ++- packages/tools/package.json | 3 +- tsconfig.base.json | 1 - yarn.lock | 44 +-- 31 files changed, 617 insertions(+), 190 deletions(-) delete mode 100644 packages/adapters/config/webpack/index.js delete mode 100644 packages/adapters/config/webpack/merge.js delete mode 100644 packages/adapters/config/webpack/webpack-base.js delete mode 100644 packages/adapters/config/webpack/webpack-dev.js delete mode 100644 packages/adapters/config/webpack/webpack-prod.js delete mode 100755 packages/adapters/examples/index.html create mode 100644 packages/adapters/examples/segmentationExport/index.ts delete mode 100644 packages/adapters/netlify.toml rename packages/adapters/{examples => old-examples}/cornerstoneDICOMSR/index.html (100%) create mode 100644 packages/adapters/src/adapters/Cornerstone3D/Segmentation.ts rename packages/adapters/src/adapters/{helpers.js => helpers.ts} (100%) delete mode 100644 packages/adapters/src/adapters/index.js create mode 100644 packages/adapters/src/adapters/index.ts diff --git a/nx.json b/nx.json index 259c24b9cc..e9f8ae07e1 100644 --- a/nx.json +++ b/nx.json @@ -3,7 +3,7 @@ "default": { "runner": "nx/tasks-runners/default", "options": { - "cacheableOperations": ["build", "test"] + "cacheableOperations": ["build", "test", "build:esm"] } } }, diff --git a/package.json b/package.json index bb04aa0ba0..f66dbf58f6 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "build": "npx lerna run build --stream", "build:esm": "npx lerna run build:esm --stream", "build:umd": "npx lerna run build:umd --stream", + "build:esm": "npx lerna run build:esm --stream", + "watch": "npx lerna watch -- lerna run build --scope=$LERNA_PACKAGE_NAME --include-dependents", "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:adapters": "cd packages/adapters && npm run build:update-api", diff --git a/packages/adapters/config/webpack/index.js b/packages/adapters/config/webpack/index.js deleted file mode 100644 index 29688123cb..0000000000 --- a/packages/adapters/config/webpack/index.js +++ /dev/null @@ -1,4 +0,0 @@ -const env = process.env.ENV || 'dev'; -const config = require(`./webpack-${env}`); - -module.exports = config; \ No newline at end of file diff --git a/packages/adapters/config/webpack/merge.js b/packages/adapters/config/webpack/merge.js deleted file mode 100644 index 1c754acd94..0000000000 --- a/packages/adapters/config/webpack/merge.js +++ /dev/null @@ -1,15 +0,0 @@ -const _ = require('lodash'); - -// Merge two objects -// Instead of merging array objects index by index (n-th source -// item with n-th object item) it concatenates both arrays -module.exports = function(object, source) { - const clone = _.cloneDeep(object); - const merged = _.mergeWith(clone, source, function(objValue, srcValue, key, object, source, stack) { - if(objValue && srcValue && _.isArray(objValue) && _.isArray(srcValue)) { - return _.concat(objValue, srcValue); - } - }); - - return merged; -} \ No newline at end of file diff --git a/packages/adapters/config/webpack/webpack-base.js b/packages/adapters/config/webpack/webpack-base.js deleted file mode 100644 index e8c5f9e9de..0000000000 --- a/packages/adapters/config/webpack/webpack-base.js +++ /dev/null @@ -1,30 +0,0 @@ -const path = require('path'); -const rootPath = process.cwd(); -const context = path.join(rootPath, "src"); -const outputPath = path.join(rootPath, 'build'); - -module.exports = { - context, - entry: { - dcmjs: './dcmjs.js' - }, - output: { - filename: 'dcmjs.js', - library: 'dcmjs', - libraryTarget: 'umd', - path: outputPath, - umdNamedDefine: true - }, - devtool: 'source-map', - module: { - rules: [ - { - test: /\.js$/, - exclude: /(node_modules)/, - use: [{ - loader: 'babel-loader' - }] - } - ] - } -}; diff --git a/packages/adapters/config/webpack/webpack-dev.js b/packages/adapters/config/webpack/webpack-dev.js deleted file mode 100644 index 97c6e4dc47..0000000000 --- a/packages/adapters/config/webpack/webpack-dev.js +++ /dev/null @@ -1,16 +0,0 @@ -const webpack = require('webpack'); -const merge = require('./merge'); -const baseConfig = require('./webpack-base'); - -const devConfig = { - mode: "development", - devServer: { - hot: true, - publicPath: '/build/' - }, - plugins: [ - new webpack.HotModuleReplacementPlugin({}) - ] -}; - -module.exports = merge(baseConfig, devConfig); \ No newline at end of file diff --git a/packages/adapters/config/webpack/webpack-prod.js b/packages/adapters/config/webpack/webpack-prod.js deleted file mode 100644 index faccc09acd..0000000000 --- a/packages/adapters/config/webpack/webpack-prod.js +++ /dev/null @@ -1,16 +0,0 @@ -const merge = require('./merge'); -const baseConfig = require('./webpack-base'); -const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); - -const prodConfig = { - mode: "production", - optimization: { - minimizer: [ - new UglifyJSPlugin({ - sourceMap: true - }) - ] - }, -}; - -module.exports = merge(baseConfig, prodConfig); \ No newline at end of file diff --git a/packages/adapters/examples/index.html b/packages/adapters/examples/index.html deleted file mode 100755 index b0f9e7cd7f..0000000000 --- a/packages/adapters/examples/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - -
- - - -
- - diff --git a/packages/adapters/examples/segmentationExport/index.ts b/packages/adapters/examples/segmentationExport/index.ts new file mode 100644 index 0000000000..1a825ff882 --- /dev/null +++ b/packages/adapters/examples/segmentationExport/index.ts @@ -0,0 +1,250 @@ +import { + RenderingEngine, + Types, + Enums, + setVolumesForViewports, + volumeLoader +} from "@cornerstonejs/core"; +import { + initDemo, + setTitleAndDescription, + addButtonToToolbar, + wadoURICreateImageIds +} from "../../../../utils/demo/helpers"; +import * as cornerstoneTools from "@cornerstonejs/tools"; +import { adaptersSEG } from "@cornerstonejs/adapters"; + +// This is for debugging purposes +console.warn( + "Click on index.ts to open source code for this example --------->" +); + +const { Cornerstone3D } = adaptersSEG; + +const { + SegmentationDisplayTool, + ToolGroupManager, + Enums: csToolsEnums, + segmentation +} = cornerstoneTools; + +const { ViewportType } = Enums; + +// Define a unique id for the volume +const volumeName = "CT_VOLUME_ID"; // Id of the volume less loader prefix +const volumeLoaderScheme = "cornerstoneStreamingImageVolume"; // Loader id which defines which volume loader to use +const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id +const segmentationId = "MY_SEGMENTATION_ID"; +const toolGroupId = "MY_TOOLGROUP_ID"; +// Create the viewports +const viewportId1 = "CT_AXIAL"; +const viewportId2 = "CT_SAGITTAL"; +const viewportId3 = "CT_CORONAL"; + +const imageIds = wadoURICreateImageIds(); + +// ======== Set up page ======== // +setTitleAndDescription( + "Labelmap Rendering over source data", + "Here we demonstrate rendering of a mock ellipsoid labelmap over source data" +); + +const size = "500px"; +const content = document.getElementById("content"); +const viewportGrid = document.createElement("div"); + +viewportGrid.style.display = "flex"; +viewportGrid.style.display = "flex"; +viewportGrid.style.flexDirection = "row"; + +const element1 = document.createElement("div"); +const element2 = document.createElement("div"); +const element3 = document.createElement("div"); +element1.style.width = size; +element1.style.height = size; +element2.style.width = size; +element2.style.height = size; +element3.style.width = size; +element3.style.height = size; + +viewportGrid.appendChild(element1); +viewportGrid.appendChild(element2); +viewportGrid.appendChild(element3); + +content.appendChild(viewportGrid); +let renderingEngine; +// ============================= // + +addButtonToToolbar({ + title: "Create SEG", + onClick: async () => { + const viewport = renderingEngine.getViewport(viewportId1); + debugger; + + const segBlob = Cornerstone3D.Segmentation.generateSegmentation(); + + //Create a URL for the binary. + const objectUrl = URL.createObjectURL(segBlob); + window.open(objectUrl); + } +}); + +/** + * Adds two concentric circles to each axial slice of the demo segmentation. + */ +function createMockEllipsoidSegmentation(segmentationVolume) { + const scalarData = segmentationVolume.scalarData; + const { dimensions } = segmentationVolume; + + const center = [dimensions[0] / 2, dimensions[1] / 2, dimensions[2] / 2]; + const outerRadius = 50; + const innerRadius = 10; + + let voxelIndex = 0; + + for (let z = 0; z < dimensions[2]; z++) { + for (let y = 0; y < dimensions[1]; y++) { + for (let x = 0; x < dimensions[0]; x++) { + const distanceFromCenter = Math.sqrt( + (x - center[0]) * (x - center[0]) + + (y - center[1]) * (y - center[1]) + + (z - center[2]) * (z - center[2]) + ); + if (distanceFromCenter < innerRadius) { + scalarData[voxelIndex] = 1; + } else if (distanceFromCenter < outerRadius) { + scalarData[voxelIndex] = 2; + } + + voxelIndex++; + } + } + } +} + +async function addSegmentationsToState() { + // Create a segmentation of the same resolution as the source data + // using volumeLoader.createAndCacheDerivedVolume. + const segmentationVolume = await volumeLoader.createAndCacheDerivedVolume( + volumeId, + { + volumeId: segmentationId + } + ); + + // Add the segmentations to state + segmentation.addSegmentations([ + { + segmentationId, + representation: { + // The type of segmentation + type: csToolsEnums.SegmentationRepresentations.Labelmap, + // The actual segmentation data, in the case of labelmap this is a + // reference to the source volume of the segmentation. + data: { + volumeId: segmentationId + } + } + } + ]); + + // Add some data to the segmentations + createMockEllipsoidSegmentation(segmentationVolume); +} + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + // Add tools to Cornerstone3D + cornerstoneTools.addTool(SegmentationDisplayTool); + + // Define tool groups to add the segmentation display tool to + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + + toolGroup.addTool(SegmentationDisplayTool.toolName); + toolGroup.setToolEnabled(SegmentationDisplayTool.toolName); + + // Get Cornerstone imageIds for the source data and fetch metadata into RAM + // const imageIds = await createImageIdsAndCacheMetaData({ + // StudyInstanceUID: + // '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + // SeriesInstanceUID: + // '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + // wadoRsRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + // }); + + // Define a volume in memory + const volume = await volumeLoader.createAndCacheVolume(volumeId, { + imageIds + }); + + // Add some segmentations based on the source data volume + await addSegmentationsToState(); + + // Instantiate a rendering engine + const renderingEngineId = "myRenderingEngine"; + renderingEngine = new RenderingEngine(renderingEngineId); + + const viewportInputArray = [ + { + viewportId: viewportId1, + type: ViewportType.ORTHOGRAPHIC, + element: element1, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + background: [0.2, 0, 0.2] + } + }, + { + viewportId: viewportId2, + type: ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + background: [0.2, 0, 0.2] + } + }, + { + viewportId: viewportId3, + type: ViewportType.ORTHOGRAPHIC, + element: element3, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + background: [0.2, 0, 0.2] + } + } + ]; + + renderingEngine.setViewports(viewportInputArray); + + toolGroup.addViewport(viewportId1, renderingEngineId); + toolGroup.addViewport(viewportId2, renderingEngineId); + toolGroup.addViewport(viewportId3, renderingEngineId); + + // Set the volume to load + volume.load(); + + // Set volumes on the viewports + await setVolumesForViewports( + renderingEngine, + [{ volumeId }], + [viewportId1, viewportId2, viewportId3] + ); + + // // Add the segmentation representation to the toolgroup + await segmentation.addSegmentationRepresentations(toolGroupId, [ + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap + } + ]); + + // Render the image + renderingEngine.renderViewports([viewportId1, viewportId2, viewportId3]); +} + +run(); diff --git a/packages/adapters/netlify.toml b/packages/adapters/netlify.toml deleted file mode 100644 index 22e53a37b0..0000000000 --- a/packages/adapters/netlify.toml +++ /dev/null @@ -1,20 +0,0 @@ -# Netlify Config -# -# TOML Reference: -# https://www.netlify.com/docs/netlify-toml-reference/ - -# Settings in the [build] context are global and are applied to all contexts -# unless otherwise overridden by more specific contexts. -[build] - # Directory to change to before starting a build. - # This is where we will look for package.json/.nvmrc/etc. - base = "" - publish = "examples/" - command = "npm run build:examples" - -# Deploy Preview context: all deploys generated from a pull/merge request will -# inherit these settings. -[context.deploy-preview] - base = "" - publish = "examples/" - command = "npm run build:examples" diff --git a/packages/adapters/examples/cornerstoneDICOMSR/index.html b/packages/adapters/old-examples/cornerstoneDICOMSR/index.html similarity index 100% rename from packages/adapters/examples/cornerstoneDICOMSR/index.html rename to packages/adapters/old-examples/cornerstoneDICOMSR/index.html diff --git a/packages/adapters/package.json b/packages/adapters/package.json index add6ee0d6b..84c8c1c5d1 100644 --- a/packages/adapters/package.json +++ b/packages/adapters/package.json @@ -1,29 +1,27 @@ { "name": "@cornerstonejs/adapters", "version": "0.6.0", - "description": "Adapters for Cornerstone3D to/from formats including DICOM SR and others", - "src": "src/index.ts", - "main": "./dist/@cornerstonejs/adapters.es.js", - "module": "src/index.ts", + "description": "Adapters for Cornerstone3D to/from formats including DICOM SR and SEG and others", "files": [ "dist" ], "directories": { - "example": "examples", "build": "dist" }, "exports": { - ".": "./dist/@cornerstonejs/adapters.es.js" + "import": "./dist/adapters.es.js" }, - "types": "./dist/@cornerstonejs/dts/index.d.ts", + "types": "./dist/types/index.d.ts", "publishConfig": { "access": "public" }, "scripts": { "test": "jest --testTimeout 60000", "build": "rollup -c rollup.config.mjs", + "build:esm": "rollup -c rollup.config.mjs", + "build:esm:watch": "rollup -c -w rollup.config.mjs", + "dev": "rollup -c -w rollup.config.mjs", "build:all": "yarn build", - "build:examples": "npm run build && npx cpx 'dist/**/*.{js,map}' examples/js", "build:update-api": "yarn run build && api-extractor run --local", "start": "rollup -c -w", "format": "prettier --write 'src/**/*.js' 'test/**/*.js'", @@ -41,9 +39,14 @@ "homepage": "https://github.com/cornerstonejs/cornerstone3D-beta/blob/main/packages/adapters/README.md", "dependencies": { "@babel/runtime-corejs2": "^7.17.8", - "dcmjs": "^0.29.5", + "dcmjs": "^0.29.6", "gl-matrix": "^3.4.3", "lodash.clonedeep": "^4.5.0", "ndarray": "^1.0.19" + }, + "devDependencies": { + "@cornerstonejs/core": "file:../core", + "@cornerstonejs/tools": "file:../tools", + "@cornerstonejs/streaming-image-volume-loader": "file:../streaming-image-volume-loader" } } diff --git a/packages/adapters/rollup.config.mjs b/packages/adapters/rollup.config.mjs index ca5c98f54f..40312df9a3 100644 --- a/packages/adapters/rollup.config.mjs +++ b/packages/adapters/rollup.config.mjs @@ -18,7 +18,7 @@ export default { // sourcemap: true // }, { - file: `dist/${pkg.name}.es.js`, + file: `dist/adapters.es.js`, format: "es", sourcemap: true } @@ -28,11 +28,14 @@ export default { browser: true }), commonjs(), - typescript(), - // globals(), + typescript({ + tsconfig: "./tsconfig.json" + }), + // globals(), // builtins(), babel({ - exclude: "node_modules/**" + exclude: "node_modules/**", + babelHelpers: "bundled" }), json() ] diff --git a/packages/adapters/src/adapters/Cornerstone/index.js b/packages/adapters/src/adapters/Cornerstone/index.js index 8cb5f0f01a..dd7cd7e658 100644 --- a/packages/adapters/src/adapters/Cornerstone/index.js +++ b/packages/adapters/src/adapters/Cornerstone/index.js @@ -5,12 +5,13 @@ import Bidirectional from "./Bidirectional"; import EllipticalRoi from "./EllipticalRoi"; import CircleRoi from "./CircleRoi"; import ArrowAnnotate from "./ArrowAnnotate"; -import Segmentation from "./Segmentation"; import CobbAngle from "./CobbAngle"; import Angle from "./Angle"; import RectangleRoi from "./RectangleRoi"; +// Segmentation +import Segmentation from "./Segmentation"; -const Cornerstone = { +const CornerstoneSR = { Length, FreehandRoi, Bidirectional, @@ -18,10 +19,13 @@ const Cornerstone = { CircleRoi, ArrowAnnotate, MeasurementReport, - Segmentation, CobbAngle, Angle, RectangleRoi }; -export default Cornerstone; +const CornerstoneSEG = { + Segmentation +}; + +export { CornerstoneSR, CornerstoneSEG }; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation.ts new file mode 100644 index 0000000000..f1c5f5c863 --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation.ts @@ -0,0 +1,237 @@ +import { utilities, normalizers, derivations } from "dcmjs"; + +const { datasetToBlob, DicomMessage, DicomMetaDictionary } = utilities; + +const { Normalizer } = normalizers; +const { Segmentation: SegmentationDerivation } = derivations; + +const { encode } = utilities.compression; + +const Segmentation = { + generateSegmentation +}; + +/** + * + * @typedef {Object} BrushData + * @property {Object} toolState - The cornerstoneTools global toolState. + * @property {Object[]} segments - The cornerstoneTools segment metadata that corresponds to the + * seriesInstanceUid. + */ + +const generateSegmentationDefaultOptions = { + includeSliceSpacing: true, + rleEncode: true +}; + +/** + * generateSegmentation - Generates a DICOM Segmentation object given cornerstoneTools data. + * + * @param images - An array of the cornerstone image objects. + * @param labelmaps + */ +function generateSegmentation( + images, + labelmaps, + options = { includeSliceSpacing: true } +) { + const isMultiframe = images[0].imageId.includes("?frame"); + const segmentation = _createSegFromImages(images, isMultiframe, options); + + return fillSegmentation(segmentation, labelmaps, options); +} + +/** + * fillSegmentation - Fills a derived segmentation dataset with cornerstoneTools `LabelMap3D` data. + * + * @param {object[]} segmentation An empty segmentation derived dataset. + * @param {Object|Object[]} inputLabelmaps3D The cornerstone `Labelmap3D` object, or an array of objects. + * @param {Object} userOptions Options object to override default options. + * @returns {Blob} description + */ +function fillSegmentation(segmentation, inputLabelmaps3D, userOptions = {}) { + const options = Object.assign( + {}, + generateSegmentationDefaultOptions, + userOptions + ); + + // Use another variable so we don't redefine labelmaps3D. + const labelmaps3D = Array.isArray(inputLabelmaps3D) + ? inputLabelmaps3D + : [inputLabelmaps3D]; + + let numberOfFrames = 0; + const referencedFramesPerLabelmap = []; + + for ( + let labelmapIndex = 0; + labelmapIndex < labelmaps3D.length; + labelmapIndex++ + ) { + const labelmap3D = labelmaps3D[labelmapIndex]; + const { labelmaps2D, metadata } = labelmap3D; + + const referencedFramesPerSegment = []; + + for (let i = 1; i < metadata.length; i++) { + if (metadata[i]) { + referencedFramesPerSegment[i] = []; + } + } + + for (let i = 0; i < labelmaps2D.length; i++) { + const labelmap2D = labelmaps2D[i]; + + if (labelmaps2D[i]) { + const { segmentsOnLabelmap } = labelmap2D; + + segmentsOnLabelmap.forEach(segmentIndex => { + if (segmentIndex !== 0) { + referencedFramesPerSegment[segmentIndex].push(i); + numberOfFrames++; + } + }); + } + } + + referencedFramesPerLabelmap[labelmapIndex] = referencedFramesPerSegment; + } + + segmentation.setNumberOfFrames(numberOfFrames); + + for ( + let labelmapIndex = 0; + labelmapIndex < labelmaps3D.length; + labelmapIndex++ + ) { + const referencedFramesPerSegment = + referencedFramesPerLabelmap[labelmapIndex]; + + const labelmap3D = labelmaps3D[labelmapIndex]; + const { metadata } = labelmap3D; + + for ( + let segmentIndex = 1; + segmentIndex < referencedFramesPerSegment.length; + segmentIndex++ + ) { + const referencedFrameIndices = + referencedFramesPerSegment[segmentIndex]; + + if (referencedFrameIndices) { + // Frame numbers start from 1. + const referencedFrameNumbers = referencedFrameIndices.map( + element => { + return element + 1; + } + ); + const segmentMetadata = metadata[segmentIndex]; + const labelmaps = _getLabelmapsFromReferencedFrameIndices( + labelmap3D, + referencedFrameIndices + ); + + segmentation.addSegmentFromLabelmap( + segmentMetadata, + labelmaps, + segmentIndex, + referencedFrameNumbers + ); + } + } + } + + if (options.rleEncode) { + const rleEncodedFrames = encode( + segmentation.dataset.PixelData, + numberOfFrames, + segmentation.dataset.Rows, + segmentation.dataset.Columns + ); + + // Must use fractional now to RLE encode, as the DICOM standard only allows BitStored && BitsAllocated + // to be 1 for BINARY. This is not ideal and there should be a better format for compression in this manner + // added to the standard. + segmentation.assignToDataset({ + BitsAllocated: "8", + BitsStored: "8", + HighBit: "7", + SegmentationType: "FRACTIONAL", + SegmentationFractionalType: "PROBABILITY", + MaximumFractionalValue: "255" + }); + + segmentation.dataset._meta.TransferSyntaxUID = { + Value: ["1.2.840.10008.1.2.5"], + vr: "UI" + }; + segmentation.dataset._vrMap.PixelData = "OB"; + segmentation.dataset.PixelData = rleEncodedFrames; + } else { + // If no rleEncoding, at least bitpack the data. + segmentation.bitPackPixelData(); + } + + const segBlob = datasetToBlob(segmentation.dataset); + + return segBlob; +} + +function _getLabelmapsFromReferencedFrameIndices( + labelmap3D, + referencedFrameIndices +) { + const { labelmaps2D } = labelmap3D; + + const labelmaps = []; + + for (let i = 0; i < referencedFrameIndices.length; i++) { + const frame = referencedFrameIndices[i]; + + labelmaps.push(labelmaps2D[frame].pixelData); + } + + return labelmaps; +} + +/** + * _createSegFromImages - description + * + * @param images - An array of the cornerstone image objects. + * @param isMultiframe - Whether the images are multiframe. + * @returns The Seg derived dataSet. + */ +function _createSegFromImages(images, isMultiframe, options) { + const datasets = []; + + if (isMultiframe) { + const image = images[0]; + const arrayBuffer = image.data.byteArray.buffer; + + const dicomData = DicomMessage.readFile(arrayBuffer); + const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); + + dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); + + datasets.push(dataset); + } else { + for (let i = 0; i < images.length; i++) { + const image = images[i]; + const arrayBuffer = image.data.byteArray.buffer; + const dicomData = DicomMessage.readFile(arrayBuffer); + const dataset = DicomMetaDictionary.naturalizeDataset( + dicomData.dict + ); + + dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); + datasets.push(dataset); + } + } + + const multiframe = Normalizer.normalizeToDataset(datasets); + + return new SegmentationDerivation([multiframe], options); +} + +export default Segmentation; diff --git a/packages/adapters/src/adapters/Cornerstone3D/index.ts b/packages/adapters/src/adapters/Cornerstone3D/index.ts index d9e1801c4b..fbe56d4418 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/index.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/index.ts @@ -12,8 +12,9 @@ import RectangleROI from "./RectangleROI"; import Length from "./Length"; import PlanarFreehandROI from "./PlanarFreehandROI"; import Probe from "./Probe"; +import Segmentation from "./Segmentation"; -const Cornerstone3D = { +const Cornerstone3DSR = { Bidirectional, CobbAngle, Angle, @@ -29,4 +30,8 @@ const Cornerstone3D = { CORNERSTONE_3D_TAG }; -export default Cornerstone3D; +const Cornerstone3DSEG = { + Segmentation +}; + +export { Cornerstone3DSR, Cornerstone3DSEG }; diff --git a/packages/adapters/src/adapters/VTKjs/Segmentation.js b/packages/adapters/src/adapters/VTKjs/Segmentation.js index 7c08a0317f..58aa58b1a1 100644 --- a/packages/adapters/src/adapters/VTKjs/Segmentation.js +++ b/packages/adapters/src/adapters/VTKjs/Segmentation.js @@ -1,6 +1,6 @@ -import {data} from 'dcmjs'; +import { data } from "dcmjs"; -const {Colors, BitArray} = data; +const { Colors, BitArray } = data; // TODO: Is there a better name for this? RGBAInt? // Should we move it to Colors.js diff --git a/packages/adapters/src/adapters/VTKjs/index.js b/packages/adapters/src/adapters/VTKjs/index.js index c90f6463b3..a383dd7f8f 100644 --- a/packages/adapters/src/adapters/VTKjs/index.js +++ b/packages/adapters/src/adapters/VTKjs/index.js @@ -1,7 +1,7 @@ import Segmentation from "./Segmentation.js"; -const VTKjs = { +const VTKjsSEG = { Segmentation }; -export default VTKjs; +export { VTKjsSEG }; diff --git a/packages/adapters/src/adapters/helpers.js b/packages/adapters/src/adapters/helpers.ts similarity index 100% rename from packages/adapters/src/adapters/helpers.js rename to packages/adapters/src/adapters/helpers.ts diff --git a/packages/adapters/src/adapters/index.js b/packages/adapters/src/adapters/index.js deleted file mode 100644 index 430510cebe..0000000000 --- a/packages/adapters/src/adapters/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import Cornerstone from "./Cornerstone"; -import Cornerstone3D from "./Cornerstone3D"; -import VTKjs from "./VTKjs"; - -const adapters = { - Cornerstone, - Cornerstone3D, - VTKjs, -}; - -export default adapters; diff --git a/packages/adapters/src/adapters/index.ts b/packages/adapters/src/adapters/index.ts new file mode 100644 index 0000000000..8e68ab2ada --- /dev/null +++ b/packages/adapters/src/adapters/index.ts @@ -0,0 +1,16 @@ +import { CornerstoneSR } from "./Cornerstone"; +import { Cornerstone3DSR, Cornerstone3DSEG } from "./Cornerstone3D"; +import { VTKjsSEG } from "./VTKjs"; + +const adaptersSR = { + Cornerstone: CornerstoneSR, + Cornerstone3D: Cornerstone3DSR +}; + +const adaptersSEG = { + Cornerstone: CornerstoneSR, + Cornerstone3D: Cornerstone3DSEG, + VTKjs: VTKjsSEG +}; + +export { adaptersSR, adaptersSEG }; diff --git a/packages/adapters/src/index.ts b/packages/adapters/src/index.ts index 61aa0800e9..7b553b61e2 100644 --- a/packages/adapters/src/index.ts +++ b/packages/adapters/src/index.ts @@ -1,4 +1,3 @@ -import adaptersSR from "./adapters"; -export { adaptersSR }; +import { adaptersSR, adaptersSEG } from "./adapters"; -export default adaptersSR; +export { adaptersSR, adaptersSEG }; diff --git a/packages/adapters/tsconfig.json b/packages/adapters/tsconfig.json index da70734a4e..d45150d61e 100644 --- a/packages/adapters/tsconfig.json +++ b/packages/adapters/tsconfig.json @@ -1,7 +1,15 @@ { "compilerOptions": { "declaration": true, - "declarationDir": "dts", + "declarationDir": "./dist/types", "emitDeclarationOnly": true - } + }, + "exclude": [ + "node_modules", + "dist", + "examples", + "old-examples" + ] + + } diff --git a/packages/core/package.json b/packages/core/package.json index 43067faff4..740b53c7e4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -17,6 +17,7 @@ "scripts": { "build:cjs": "tsc --project ./tsconfig.cjs.json", "build:esm": "tsc --project ./tsconfig.esm.json", + "build:esm:watch": "tsc --project ./tsconfig.esm.json --watch", "build:umd": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", "build:all": "yarn run build:umd && yarn run build:cjs && yarn run build:esm", "copy-dts": "copyfiles -u 1 \"src/**/*.d.ts\" dist/cjs && copyfiles -u 1 \"src/**/*.d.ts\" dist/esm", diff --git a/packages/dicomImageLoader/package.json b/packages/dicomImageLoader/package.json index 4b0db685e0..e31b4cf118 100644 --- a/packages/dicomImageLoader/package.json +++ b/packages/dicomImageLoader/package.json @@ -27,7 +27,6 @@ "scripts": { "build:umd:dynamic": "cross-env NODE_ENV=production webpack --config .webpack/webpack-dynamic-import.js", "build:umd:bundle": "cross-env NODE_ENV=production webpack --config .webpack/webpack-bundle.js", - "build:esm": "tsc --project ./tsconfig.esm.json", "build:all": "yarn run build:umd:dynamic & yarn run build:umd:bundle", "copy-dts": "echo 'not implemented yet'", "build": "yarn run build:all && yarn run copy-dts", diff --git a/packages/docs/package.json b/packages/docs/package.json index 9df1e12716..eba0ebd550 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -39,7 +39,7 @@ "@mdx-js/react": "^1.6.21", "@svgr/webpack": "^6.2.1", "clsx": "^1.1.1", - "dcmjs": "^0.24.4", + "dcmjs": "^0.29.6", "detect-gpu": "^5.0.22", "dicom-parser": "^1.8.11", "dicomweb-client": "^0.8.4", diff --git a/packages/streaming-image-volume-loader/package.json b/packages/streaming-image-volume-loader/package.json index fa131201e2..2e252b99e0 100644 --- a/packages/streaming-image-volume-loader/package.json +++ b/packages/streaming-image-volume-loader/package.json @@ -16,6 +16,7 @@ "scripts": { "build:cjs": "tsc --project ./tsconfig.cjs.json", "build:esm": "tsc --project ./tsconfig.esm.json", + "build:esm:watch": "tsc --project ./tsconfig.esm.json --watch", "build:umd": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", "build:all": "yarn run build:umd && yarn run build:cjs && yarn run build:esm", "build": "yarn run build:all", @@ -25,7 +26,10 @@ "webpack:watch": "webpack --mode development --progress --watch --config ./.webpack/webpack.dev.js" }, "dependencies": { - "@cornerstonejs/core": "^0.44.2" + "@cornerstonejs/core": "file:../core" + }, + "devDependencies": { + "@cornerstonejs/calculate-suv": "1.0.2" }, "contributors": [ { diff --git a/packages/tools/examples/labelmapRendering/index.ts b/packages/tools/examples/labelmapRendering/index.ts index 5dd1bee01b..610f874da3 100644 --- a/packages/tools/examples/labelmapRendering/index.ts +++ b/packages/tools/examples/labelmapRendering/index.ts @@ -9,14 +9,18 @@ import { initDemo, createImageIdsAndCacheMetaData, setTitleAndDescription, + addButtonToToolbar, } from '../../../../utils/demo/helpers'; import * as cornerstoneTools from '@cornerstonejs/tools'; +import { adaptersSEG } from '@cornerstonejs/adapters'; // This is for debugging purposes console.warn( 'Click on index.ts to open source code for this example --------->' ); +const { VTKjs } = adaptersSEG; + const { SegmentationDisplayTool, ToolGroupManager, @@ -32,6 +36,10 @@ const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id const segmentationId = 'MY_SEGMENTATION_ID'; const toolGroupId = 'MY_TOOLGROUP_ID'; +// Create the viewports +const viewportId1 = 'CT_AXIAL'; +const viewportId2 = 'CT_SAGITTAL'; +const viewportId3 = 'CT_CORONAL'; // ======== Set up page ======== // setTitleAndDescription( @@ -62,9 +70,23 @@ viewportGrid.appendChild(element2); viewportGrid.appendChild(element3); content.appendChild(viewportGrid); - +let renderingEngine; // ============================= // +addButtonToToolbar({ + title: 'Create SEG', + onClick: async () => { + const viewport = renderingEngine.getViewport(viewportId1); + debugger; + + const segBlob = VTKjs.Segmentation.generateSegmentation(); + + //Create a URL for the binary. + const objectUrl = URL.createObjectURL(segBlob); + window.open(objectUrl); + }, +}); + /** * Adds two concentric circles to each axial slice of the demo segmentation. */ @@ -163,12 +185,7 @@ async function run() { // Instantiate a rendering engine const renderingEngineId = 'myRenderingEngine'; - const renderingEngine = new RenderingEngine(renderingEngineId); - - // Create the viewports - const viewportId1 = 'CT_AXIAL'; - const viewportId2 = 'CT_SAGITTAL'; - const viewportId3 = 'CT_CORONAL'; + renderingEngine = new RenderingEngine(renderingEngineId); const viewportInputArray = [ { diff --git a/packages/tools/package.json b/packages/tools/package.json index 3e9b664338..5c146aba6a 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -17,6 +17,7 @@ "scripts": { "build:cjs": "tsc --project ./tsconfig.cjs.json", "build:esm": "tsc --project ./tsconfig.esm.json", + "build:esm:watch": "tsc --project ./tsconfig.esm.json --watch", "build:umd": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", "build:all": "yarn run build:umd && yarn run build:cjs && yarn run build:esm", "build": "yarn run build:all", @@ -26,7 +27,7 @@ "webpack:watch": "webpack --mode development --progress --watch --config ./.webpack/webpack.dev.js" }, "dependencies": { - "@cornerstonejs/core": "^0.44.2", + "@cornerstonejs/core": "file:../core", "lodash.clonedeep": "4.5.0", "lodash.get": "^4.4.2" }, diff --git a/tsconfig.base.json b/tsconfig.base.json index 6643ed3d14..5562fa7fa5 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -37,7 +37,6 @@ "packages/**/dist", "packages/**/lib", "packages/**/lib-esm", - "packages/adapters", "packages/docs", "snippets", "examples" diff --git a/yarn.lock b/yarn.lock index 896b6903a2..195cda5978 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1342,10 +1342,33 @@ resolved "https://registry.npmjs.org/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.2.tgz#e96721d56f6ec96f7f95c16321d88cc8467d8d81" integrity sha512-lgdvBvvNezleY+4pIe2ceUsJzlZe/0PipdeubQ3vZZOz3xxtHHMR1XFCl4fgd8gosR8COHuD7h6q+MwgrwBsng== -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== +"@cornerstonejs/core@file:packages/core": + version "0.44.0" + dependencies: + detect-gpu "^4.0.45" + lodash.clonedeep "4.5.0" + +"@cornerstonejs/streaming-image-volume-loader@file:packages/streaming-image-volume-loader": + version "0.19.0" + dependencies: + "@cornerstonejs/core" "file:../../../../Library/Caches/Yarn/v6/npm-@cornerstonejs-streaming-image-volume-loader-0.19.0-56b3ab26-1faf-4ae4-a47b-7898ee629cce-1682989432248/node_modules/@cornerstonejs/core" + +"@cornerstonejs/tools@file:packages/tools": + version "0.66.0" + dependencies: + "@cornerstonejs/core" "file:../../../../Library/Caches/Yarn/v6/npm-@cornerstonejs-tools-0.66.0-c5173da7-dda7-4e1d-bfda-01f2304936a3-1682989432249/node_modules/@cornerstonejs/core" + lodash.clonedeep "4.5.0" + lodash.get "^4.4.2" + +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== dependencies: "@jridgewell/trace-mapping" "0.3.9" @@ -8193,18 +8216,7 @@ dateformat@^3.0.0: resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -dcmjs@^0.24.4: - version "0.24.10" - resolved "https://registry.npmjs.org/dcmjs/-/dcmjs-0.24.10.tgz#048a12a1905175763274545c17bb38b506cdfd00" - integrity sha512-kugEdBqwwTx7lqUxm4jdklkMJiQ8pZ0aqMw9WkszkDU5lDWHyq2kbRccACtcwqygC7j1d36w8uG/vNLMejeMBg== - dependencies: - "@babel/runtime-corejs2" "^7.17.8" - gl-matrix "^3.1.0" - lodash.clonedeep "^4.5.0" - loglevelnext "^3.0.1" - ndarray "^1.0.19" - -dcmjs@^0.29.5: +dcmjs@^0.29.6: version "0.29.6" resolved "https://registry.npmjs.org/dcmjs/-/dcmjs-0.29.6.tgz#6ca1543e74bae29657d1f7f2273407e23266c011" integrity sha512-xMULkeFpxFNV8JZwWMOW3j1uL3iAe8r3pytlyWnlTxYc/OZ9KSu4wmQMVMBAgmY0GYpeWlji7GMRArVh8ISKhA== From 101e778661722429be5da7ee24a4a788f301c63b Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 3 May 2023 21:01:29 -0400 Subject: [PATCH 02/20] wip --- .../examples/segmentationExport/index.ts | 21 ++++++++++++++----- utils/ExampleRunner/example-info.json | 6 ++++++ utils/ExampleRunner/example-runner-cli.js | 4 ++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/adapters/examples/segmentationExport/index.ts b/packages/adapters/examples/segmentationExport/index.ts index 1a825ff882..796607515e 100644 --- a/packages/adapters/examples/segmentationExport/index.ts +++ b/packages/adapters/examples/segmentationExport/index.ts @@ -3,7 +3,8 @@ import { Types, Enums, setVolumesForViewports, - volumeLoader + volumeLoader, + cache } from "@cornerstonejs/core"; import { initDemo, @@ -45,8 +46,8 @@ const imageIds = wadoURICreateImageIds(); // ======== Set up page ======== // setTitleAndDescription( - "Labelmap Rendering over source data", - "Here we demonstrate rendering of a mock ellipsoid labelmap over source data" + "DICOM SEG Export", + "Here we demonstrate how to export a DICOM SEG from a Cornerstone3D volume." ); const size = "500px"; @@ -79,9 +80,19 @@ addButtonToToolbar({ title: "Create SEG", onClick: async () => { const viewport = renderingEngine.getViewport(viewportId1); - debugger; - const segBlob = Cornerstone3D.Segmentation.generateSegmentation(); + const volume = cache.getVolume(volumeId); + const imageIds = volume.imageIds; + + const images = imageIds.map(imageId => { + return cache.getImageLoadObject(imageId); + }); + + const segBlob = Cornerstone3D.Segmentation.generateSegmentation( + images, + segmentation, + { SeriesInstanceUID: ctSeriesInstanceUID } + ); //Create a URL for the binary. const objectUrl = URL.createObjectURL(segBlob); diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index 8c281adbd7..38dd34f57c 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -269,6 +269,12 @@ "name": "WADO-URI (DICOM P10)", "description": "WADO-URI (DICOM P10 via HTTP GET) with different codecs" } + }, + "adapters": { + "segmentationExport": { + "name": "DICOM SEG export", + "description": "Demonstrates how to export a segmentation to DICOM SEG" + } } } } diff --git a/utils/ExampleRunner/example-runner-cli.js b/utils/ExampleRunner/example-runner-cli.js index e5e6927414..e979d84759 100755 --- a/utils/ExampleRunner/example-runner-cli.js +++ b/utils/ExampleRunner/example-runner-cli.js @@ -77,6 +77,10 @@ const configuration = { path: 'packages/dicomImageLoader/examples', regexp: 'index.ts', }, + { + path: 'packages/adapters/examples', + regexp: 'index.ts', + }, ], }; From 7dc9c67c093aa306991c60512fb7eae6666705fd Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 12 Jul 2023 10:26:21 -0400 Subject: [PATCH 03/20] refactoring --- .../examples/segmentationExport/index.ts | 195 ++++++++++++++---- .../Segmentation/generateLabelMaps2DFrom3D.ts | 52 +++++ .../generateSegmentation.ts} | 92 +++++---- .../Segmentation/generateToolState.ts | 36 ++++ .../Cornerstone3D/Segmentation/index.ts | 5 + .../src/adapters/Cornerstone3D/index.ts | 2 +- packages/adapters/src/adapters/helpers.ts | 19 -- .../src/adapters/helpers/codeMeaningEquals.ts | 9 + .../src/adapters/helpers/graphicTypeEquals.ts | 7 + .../adapters/src/adapters/helpers/index.ts | 5 + .../adapters/src/adapters/helpers/toArray.ts | 5 + yarn.lock | 20 -- 12 files changed, 317 insertions(+), 130 deletions(-) create mode 100644 packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateLabelMaps2DFrom3D.ts rename packages/adapters/src/adapters/Cornerstone3D/{Segmentation.ts => Segmentation/generateSegmentation.ts} (76%) create mode 100644 packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateToolState.ts create mode 100644 packages/adapters/src/adapters/Cornerstone3D/Segmentation/index.ts delete mode 100644 packages/adapters/src/adapters/helpers.ts create mode 100644 packages/adapters/src/adapters/helpers/codeMeaningEquals.ts create mode 100644 packages/adapters/src/adapters/helpers/graphicTypeEquals.ts create mode 100644 packages/adapters/src/adapters/helpers/index.ts create mode 100644 packages/adapters/src/adapters/helpers/toArray.ts diff --git a/packages/adapters/examples/segmentationExport/index.ts b/packages/adapters/examples/segmentationExport/index.ts index 796607515e..cb12623fec 100644 --- a/packages/adapters/examples/segmentationExport/index.ts +++ b/packages/adapters/examples/segmentationExport/index.ts @@ -4,16 +4,18 @@ import { Enums, setVolumesForViewports, volumeLoader, - cache + cache, + metaData } from "@cornerstonejs/core"; import { initDemo, setTitleAndDescription, addButtonToToolbar, - wadoURICreateImageIds + createImageIdsAndCacheMetaData } from "../../../../utils/demo/helpers"; import * as cornerstoneTools from "@cornerstonejs/tools"; import { adaptersSEG } from "@cornerstonejs/adapters"; +import dcmjs from "dcmjs"; // This is for debugging purposes console.warn( @@ -24,6 +26,7 @@ const { Cornerstone3D } = adaptersSEG; const { SegmentationDisplayTool, + StackScrollMouseWheelTool, ToolGroupManager, Enums: csToolsEnums, segmentation @@ -42,8 +45,6 @@ const viewportId1 = "CT_AXIAL"; const viewportId2 = "CT_SAGITTAL"; const viewportId3 = "CT_CORONAL"; -const imageIds = wadoURICreateImageIds(); - // ======== Set up page ======== // setTitleAndDescription( "DICOM SEG Export", @@ -74,60 +75,126 @@ viewportGrid.appendChild(element3); content.appendChild(viewportGrid); let renderingEngine; +let segmentationVolume; +let segmentationRepresentationUID; + +function generateMockMetadata(segmentIndex, color) { + // TODO -> Use colors from the cornerstoneTools LUT. + const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB( + color.slice(0, 3) + ); + + const colorAgain = dcmjs.data.Colors.dicomlab2RGB( + RecommendedDisplayCIELabValue + ); + + return { + SegmentedPropertyCategoryCodeSequence: { + CodeValue: "T-D0050", + CodingSchemeDesignator: "SRT", + CodeMeaning: "Tissue" + }, + SegmentNumber: (segmentIndex + 1).toString(), + SegmentLabel: "Tissue " + (segmentIndex + 1).toString(), + SegmentAlgorithmType: "SEMIAUTOMATIC", + SegmentAlgorithmName: "Slicer Prototype", + RecommendedDisplayCIELabValue, + SegmentedPropertyTypeCodeSequence: { + CodeValue: "T-D0050", + CodingSchemeDesignator: "SRT", + CodeMeaning: "Tissue" + } + }; +} + // ============================= // addButtonToToolbar({ title: "Create SEG", onClick: async () => { - const viewport = renderingEngine.getViewport(viewportId1); - const volume = cache.getVolume(volumeId); - const imageIds = volume.imageIds; - const images = imageIds.map(imageId => { - return cache.getImageLoadObject(imageId); - }); + const volumeImages = volume.convertToCornerstoneImages(); + const imagePromises = volumeImages.map(image => image.promise); + + await Promise.all(imagePromises).then(images => { + const labelmap3D = + Cornerstone3D.Segmentation.generateLabelMaps2DFrom3D( + segmentationVolume + ); - const segBlob = Cornerstone3D.Segmentation.generateSegmentation( - images, - segmentation, - { SeriesInstanceUID: ctSeriesInstanceUID } - ); + const segUID = segmentationRepresentationUID[0]; - //Create a URL for the binary. - const objectUrl = URL.createObjectURL(segBlob); - window.open(objectUrl); + labelmap3D.metadata = []; + // labelmap3D.labelmaps2D.forEach(labelmap2D => { + labelmap3D.segmentsOnLabelmap.forEach(segmentIndex => { + const color = segmentation.config.color.getColorForSegmentIndex( + toolGroupId, + segUID, + segmentIndex + ); + + console.debug("color", segmentIndex, color); + const segmentMetadata = generateMockMetadata( + segmentIndex, + color + ); + labelmap3D.metadata[segmentIndex] = segmentMetadata; + }); + + const segBlob = Cornerstone3D.Segmentation.generateSegmentation( + images, + labelmap3D, + metaData + ); + + //Create a URL for the binary. + const objectUrl = URL.createObjectURL(segBlob); + window.open(objectUrl); + }); } }); /** * Adds two concentric circles to each axial slice of the demo segmentation. */ -function createMockEllipsoidSegmentation(segmentationVolume) { - const scalarData = segmentationVolume.scalarData; - const { dimensions } = segmentationVolume; - - const center = [dimensions[0] / 2, dimensions[1] / 2, dimensions[2] / 2]; - const outerRadius = 50; - const innerRadius = 10; +function createMockEllipsoidSegmentation( + segmentationVolume, + outerRadius = 20, + innerRadius = 10, + center = "center", + labels = [1, 2] + // mode = "first" +) { + const { dimensions, scalarData, imageData } = segmentationVolume; + + const centerToUse = + center === "center" + ? [dimensions[0] / 2, dimensions[1] / 2, dimensions[2] / 2] + : center; let voxelIndex = 0; + // const zToUse = + // mode === "first" ? [0, 1] : [dimensions[2] - 1, dimensions[2]]; + for (let z = 0; z < dimensions[2]; z++) { for (let y = 0; y < dimensions[1]; y++) { for (let x = 0; x < dimensions[0]; x++) { const distanceFromCenter = Math.sqrt( - (x - center[0]) * (x - center[0]) + - (y - center[1]) * (y - center[1]) + - (z - center[2]) * (z - center[2]) + (x - centerToUse[0]) * (x - centerToUse[0]) + + (y - centerToUse[1]) * (y - centerToUse[1]) + + (z - centerToUse[2]) * (z - centerToUse[2]) ); if (distanceFromCenter < innerRadius) { - scalarData[voxelIndex] = 1; + scalarData[voxelIndex] = labels[0]; } else if (distanceFromCenter < outerRadius) { - scalarData[voxelIndex] = 2; + scalarData[voxelIndex] = labels[1]; } voxelIndex++; + // const index = imageData.computeOffsetIndex([x, y, z]); + // scalarData[index] = labels[1]; } } } @@ -136,13 +203,20 @@ function createMockEllipsoidSegmentation(segmentationVolume) { async function addSegmentationsToState() { // Create a segmentation of the same resolution as the source data // using volumeLoader.createAndCacheDerivedVolume. - const segmentationVolume = await volumeLoader.createAndCacheDerivedVolume( + segmentationVolume = await volumeLoader.createAndCacheDerivedVolume( volumeId, { volumeId: segmentationId } ); + // segmentationVolume2 = await volumeLoader.createAndCacheDerivedVolume( + // volumeId, + // { + // volumeId: `${segmentationId}2` + // } + // ); + // Add the segmentations to state segmentation.addSegmentations([ { @@ -158,9 +232,30 @@ async function addSegmentationsToState() { } } ]); + // segmentation.addSegmentations([ + // { + // segmentationId: `${segmentationId}2`, + // representation: { + // // The type of segmentation + // type: csToolsEnums.SegmentationRepresentations.Labelmap, + // // The actual segmentation data, in the case of labelmap this is a + // // reference to the source volume of the segmentation. + // data: { + // volumeId: `${segmentationId}2` + // } + // } + // } + // ]); // Add some data to the segmentations - createMockEllipsoidSegmentation(segmentationVolume); + // createMockEllipsoidSegmentation(segmentationVolume); + createMockEllipsoidSegmentation( + segmentationVolume, + 40, + 20, + [250, 100, 125], + [2, 5] + ); } /** @@ -172,21 +267,24 @@ async function run() { // Add tools to Cornerstone3D cornerstoneTools.addTool(SegmentationDisplayTool); + cornerstoneTools.addTool(StackScrollMouseWheelTool); // Define tool groups to add the segmentation display tool to const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); toolGroup.addTool(SegmentationDisplayTool.toolName); + toolGroup.addTool(StackScrollMouseWheelTool.toolName); toolGroup.setToolEnabled(SegmentationDisplayTool.toolName); + toolGroup.setToolActive(StackScrollMouseWheelTool.toolName); // Get Cornerstone imageIds for the source data and fetch metadata into RAM - // const imageIds = await createImageIdsAndCacheMetaData({ - // StudyInstanceUID: - // '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', - // SeriesInstanceUID: - // '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', - // wadoRsRoot: 'https://d3t6nz73ql33tx.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.226151125820845824875394858561", + wadoRsRoot: "https://d3t6nz73ql33tx.cloudfront.net/dicomweb" + }); // Define a volume in memory const volume = await volumeLoader.createAndCacheVolume(volumeId, { @@ -247,12 +345,19 @@ async function run() { ); // // Add the segmentation representation to the toolgroup - await segmentation.addSegmentationRepresentations(toolGroupId, [ - { - segmentationId, - type: csToolsEnums.SegmentationRepresentations.Labelmap - } - ]); + segmentationRepresentationUID = + await segmentation.addSegmentationRepresentations(toolGroupId, [ + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap + } + ]); + // await segmentation.addSegmentationRepresentations(toolGroupId, [ + // { + // segmentationId: `${segmentationId}2`, + // type: csToolsEnums.SegmentationRepresentations.Labelmap + // } + // ]); // Render the image renderingEngine.renderViewports([viewportId1, viewportId2, viewportId3]); diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateLabelMaps2DFrom3D.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateLabelMaps2DFrom3D.ts new file mode 100644 index 0000000000..1e76d4a60d --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateLabelMaps2DFrom3D.ts @@ -0,0 +1,52 @@ +function generateLabelMaps2DFrom3D(labelmap3D) { + // 1. we need to generate labelmaps2D from labelmaps3D, a labelmap2D is for each + // slice + const { scalarData, dimensions } = labelmap3D; + + // scalarData is a flat array of all the pixels in the volume. + + const labelmaps2D = []; + const segmentsOnLabelmap3D = new Set(); + + for (let z = 0; z < dimensions[2]; z++) { + const pixelData = scalarData.slice( + z * dimensions[0] * dimensions[1], + (z + 1) * dimensions[0] * dimensions[1] + ); + + const segmentsOnLabelmap = []; + + for (let i = 0; i < pixelData.length; i++) { + const segment = pixelData[i]; + if (!segmentsOnLabelmap.includes(segment) && segment !== 0) { + segmentsOnLabelmap.push(segment); + } + } + + const labelmap2D = { + segmentsOnLabelmap, + pixelData, + rows: dimensions[1], + columns: dimensions[0] + }; + + if (segmentsOnLabelmap.length === 0) { + continue; + } + + segmentsOnLabelmap.forEach(segmentIndex => { + segmentsOnLabelmap3D.add(segmentIndex); + }); + + labelmaps2D[dimensions[2] - z] = labelmap2D; + } + + // remove segment 0 from segmentsOnLabelmap3D + labelmap3D.segmentsOnLabelmap = Array.from(segmentsOnLabelmap3D); + + labelmap3D.labelmaps2D = labelmaps2D; + + return labelmap3D; +} + +export { generateLabelMaps2DFrom3D }; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts similarity index 76% rename from packages/adapters/src/adapters/Cornerstone3D/Segmentation.ts rename to packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts index f1c5f5c863..f5523eaa51 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Segmentation.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts @@ -1,16 +1,10 @@ -import { utilities, normalizers, derivations } from "dcmjs"; - -const { datasetToBlob, DicomMessage, DicomMetaDictionary } = utilities; +import { utilities, normalizers, derivations, data as dcmjsData } from "dcmjs"; const { Normalizer } = normalizers; const { Segmentation: SegmentationDerivation } = derivations; const { encode } = utilities.compression; -const Segmentation = { - generateSegmentation -}; - /** * * @typedef {Object} BrushData @@ -18,26 +12,28 @@ const Segmentation = { * @property {Object[]} segments - The cornerstoneTools segment metadata that corresponds to the * seriesInstanceUid. */ - const generateSegmentationDefaultOptions = { includeSliceSpacing: true, - rleEncode: true + rleEncode: false }; /** * generateSegmentation - Generates a DICOM Segmentation object given cornerstoneTools data. * - * @param images - An array of the cornerstone image objects. - * @param labelmaps + * @param images - An array of the cornerstone image objects, which includes imageId and metadata + * @param labelmaps - An array of the 3D Volumes that contain the segmentation data. */ function generateSegmentation( images, labelmaps, + metadata, options = { includeSliceSpacing: true } ) { - const isMultiframe = images[0].imageId.includes("?frame"); - const segmentation = _createSegFromImages(images, isMultiframe, options); - + const segmentation = _createMultiframeSegmentationFromReferencedImages( + images, + metadata, + options + ); return fillSegmentation(segmentation, labelmaps, options); } @@ -70,7 +66,7 @@ function fillSegmentation(segmentation, inputLabelmaps3D, userOptions = {}) { labelmapIndex++ ) { const labelmap3D = labelmaps3D[labelmapIndex]; - const { labelmaps2D, metadata } = labelmap3D; + const { metadata, labelmaps2D } = labelmap3D; const referencedFramesPerSegment = []; @@ -173,7 +169,7 @@ function fillSegmentation(segmentation, inputLabelmaps3D, userOptions = {}) { segmentation.bitPackPixelData(); } - const segBlob = datasetToBlob(segmentation.dataset); + const segBlob = dcmjsData.datasetToBlob(segmentation.dataset); return segBlob; } @@ -196,42 +192,48 @@ function _getLabelmapsFromReferencedFrameIndices( } /** - * _createSegFromImages - description + * _createMultiframeSegmentationFromReferencedImages - description * * @param images - An array of the cornerstone image objects. - * @param isMultiframe - Whether the images are multiframe. + * @param options - the options object for the SegmentationDerivation. * @returns The Seg derived dataSet. */ -function _createSegFromImages(images, isMultiframe, options) { - const datasets = []; - - if (isMultiframe) { - const image = images[0]; - const arrayBuffer = image.data.byteArray.buffer; - - const dicomData = DicomMessage.readFile(arrayBuffer); - const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); - - dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); - - datasets.push(dataset); - } else { - for (let i = 0; i < images.length; i++) { - const image = images[i]; - const arrayBuffer = image.data.byteArray.buffer; - const dicomData = DicomMessage.readFile(arrayBuffer); - const dataset = DicomMetaDictionary.naturalizeDataset( - dicomData.dict - ); - - dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); - datasets.push(dataset); - } - } +function _createMultiframeSegmentationFromReferencedImages( + images, + metadata, + options +) { + // for (let i = 0; i < images.length; i++) { + // const image = images[i]; + // const arrayBuffer = image.data.byteArray.buffer; + // const dicomData = DicomMessage.readFile(arrayBuffer); + // const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); + + // dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); + // datasets.push(dataset); + // } + + const datasets = images.map(image => { + // add the sopClassUID to the dataset + const allMetadataModules = metadata.get("dataset", image.imageId); + + return { + ...image, + ...allMetadataModules, + // Todo: move to dcmjs tag style + SOPClassUID: allMetadataModules.SopClassUID, + SOPInstanceUID: allMetadataModules.SopInstanceUID, + PixelData: image.getPixelData(), + _vrMap: { + PixelData: "OW" + }, + _meta: {} + }; + }); const multiframe = Normalizer.normalizeToDataset(datasets); return new SegmentationDerivation([multiframe], options); } -export default Segmentation; +export { generateSegmentation }; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateToolState.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateToolState.ts new file mode 100644 index 0000000000..4e50a7b29d --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateToolState.ts @@ -0,0 +1,36 @@ +import { CornerstoneSEG } from "../../Cornerstone"; + +const { Segmentation } = CornerstoneSEG; +const { generateToolState: generateToolStateCornerstoneLegacy } = Segmentation; + +/** + * generateToolState - Given a set of cornerstoneTools imageIds and a Segmentation buffer, + * derive cornerstoneTools toolState and brush metadata. + * + * @param imageIds - An array of the imageIds. + * @param arrayBuffer - The SEG arrayBuffer. + * @param skipOverlapping - skip checks for overlapping segs, default value false. + * @param tolerance - default value 1.e-3. + * + * @returns a list of array buffer for each labelMap + * an object from which the segment metadata can be derived + * list containing the track of segments per frame + * list containing the track of segments per frame for each labelMap (available only for the overlapping case). + */ +function generateToolState( + imageIds, + arrayBuffer, + metadataProvider, + skipOverlapping = false, + tolerance = 1e-3 +) { + return generateToolStateCornerstoneLegacy( + imageIds, + arrayBuffer, + metadataProvider, + skipOverlapping, + tolerance + ); +} + +export { generateToolState }; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/index.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/index.ts new file mode 100644 index 0000000000..e124c65c12 --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/index.ts @@ -0,0 +1,5 @@ +import { generateSegmentation } from "./generateSegmentation"; +import { generateLabelMaps2DFrom3D } from "./generateLabelMaps2DFrom3D"; +import { generateToolState } from "./generateToolState"; + +export { generateLabelMaps2DFrom3D, generateSegmentation, generateToolState }; diff --git a/packages/adapters/src/adapters/Cornerstone3D/index.ts b/packages/adapters/src/adapters/Cornerstone3D/index.ts index fbe56d4418..979f9d8519 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/index.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/index.ts @@ -12,7 +12,7 @@ import RectangleROI from "./RectangleROI"; import Length from "./Length"; import PlanarFreehandROI from "./PlanarFreehandROI"; import Probe from "./Probe"; -import Segmentation from "./Segmentation"; +import * as Segmentation from "./Segmentation"; const Cornerstone3DSR = { Bidirectional, diff --git a/packages/adapters/src/adapters/helpers.ts b/packages/adapters/src/adapters/helpers.ts deleted file mode 100644 index 587d9fb259..0000000000 --- a/packages/adapters/src/adapters/helpers.ts +++ /dev/null @@ -1,19 +0,0 @@ -const toArray = function (x) { - return Array.isArray(x) ? x : [x]; -}; - -const codeMeaningEquals = codeMeaningName => { - return contentItem => { - return ( - contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName - ); - }; -}; - -const graphicTypeEquals = graphicType => { - return contentItem => { - return contentItem && contentItem.GraphicType === graphicType; - }; -}; - -export { toArray, codeMeaningEquals, graphicTypeEquals }; diff --git a/packages/adapters/src/adapters/helpers/codeMeaningEquals.ts b/packages/adapters/src/adapters/helpers/codeMeaningEquals.ts new file mode 100644 index 0000000000..c7dfaaffad --- /dev/null +++ b/packages/adapters/src/adapters/helpers/codeMeaningEquals.ts @@ -0,0 +1,9 @@ +const codeMeaningEquals = codeMeaningName => { + return contentItem => { + return ( + contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName + ); + }; +}; + +export { codeMeaningEquals }; diff --git a/packages/adapters/src/adapters/helpers/graphicTypeEquals.ts b/packages/adapters/src/adapters/helpers/graphicTypeEquals.ts new file mode 100644 index 0000000000..f07a0a2953 --- /dev/null +++ b/packages/adapters/src/adapters/helpers/graphicTypeEquals.ts @@ -0,0 +1,7 @@ +const graphicTypeEquals = graphicType => { + return contentItem => { + return contentItem && contentItem.GraphicType === graphicType; + }; +}; + +export { graphicTypeEquals }; diff --git a/packages/adapters/src/adapters/helpers/index.ts b/packages/adapters/src/adapters/helpers/index.ts new file mode 100644 index 0000000000..b852e82fef --- /dev/null +++ b/packages/adapters/src/adapters/helpers/index.ts @@ -0,0 +1,5 @@ +import { toArray } from "./toArray"; +import { codeMeaningEquals } from "./codeMeaningEquals"; +import { graphicTypeEquals } from "./graphicTypeEquals"; + +export { toArray, codeMeaningEquals, graphicTypeEquals }; diff --git a/packages/adapters/src/adapters/helpers/toArray.ts b/packages/adapters/src/adapters/helpers/toArray.ts new file mode 100644 index 0000000000..a9233773bd --- /dev/null +++ b/packages/adapters/src/adapters/helpers/toArray.ts @@ -0,0 +1,5 @@ +const toArray = function (x) { + return Array.isArray(x) ? x : [x]; +}; + +export { toArray }; diff --git a/yarn.lock b/yarn.lock index 42b464c7ca..75f13e923d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1342,26 +1342,6 @@ resolved "https://registry.npmjs.org/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.2.tgz#e96721d56f6ec96f7f95c16321d88cc8467d8d81" integrity sha512-lgdvBvvNezleY+4pIe2ceUsJzlZe/0PipdeubQ3vZZOz3xxtHHMR1XFCl4fgd8gosR8COHuD7h6q+MwgrwBsng== -"@cornerstonejs/core@file:packages/core": - version "1.2.6" - dependencies: - "@kitware/vtk.js" "27.3.1" - detect-gpu "^5.0.22" - gl-matrix "^3.4.3" - lodash.clonedeep "4.5.0" - -"@cornerstonejs/streaming-image-volume-loader@file:packages/streaming-image-volume-loader": - version "1.2.6" - dependencies: - "@cornerstonejs/core" "^1.2.6" - -"@cornerstonejs/tools@file:packages/tools": - version "1.2.6" - dependencies: - "@cornerstonejs/core" "^1.2.6" - lodash.clonedeep "4.5.0" - lodash.get "^4.4.2" - "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" From b25c911069195435cec738cb96b9520e82ab9148 Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 12 Jul 2023 10:27:10 -0400 Subject: [PATCH 04/20] remove bound ijk which is unnecessary since we moved to world --- .../src/BaseStreamingImageVolume.ts | 10 ++++++++++ .../tools/segmentation/strategies/eraseRectangle.ts | 4 ---- .../src/tools/segmentation/strategies/fillCircle.ts | 4 ---- .../src/tools/segmentation/strategies/fillRectangle.ts | 4 ---- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts b/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts index 7883b3985b..4be1a04b54 100644 --- a/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts +++ b/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts @@ -885,6 +885,16 @@ export default class BaseStreamingImageVolume extends ImageVolume { return imageLoadObject; } + public convertToCornerstoneImages(): Types.IImageLoadObject[] { + const { imageIds } = this; + + const imageLoadObjects = imageIds.map((imageId, imageIdIndex) => { + return this.convertToCornerstoneImage(imageId, imageIdIndex); + }); + + return imageLoadObjects; + } + /** * Converts all the volume images (imageIds) to cornerstoneImages and caches them. * It iterates over all the imageIds and convert them until there is no diff --git a/packages/tools/src/tools/segmentation/strategies/eraseRectangle.ts b/packages/tools/src/tools/segmentation/strategies/eraseRectangle.ts index a3df8236ed..2b59d8f579 100644 --- a/packages/tools/src/tools/segmentation/strategies/eraseRectangle.ts +++ b/packages/tools/src/tools/segmentation/strategies/eraseRectangle.ts @@ -35,10 +35,6 @@ function eraseRectangle( const boundsIJK = getBoundingBoxAroundShape(rectangleCornersIJK, dimensions); - if (boundsIJK.every(([min, max]) => min !== max)) { - throw new Error('Oblique segmentation tools are not supported yet'); - } - // Since always all points inside the boundsIJK is inside the rectangle... const pointInShape = () => true; diff --git a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts index 691023fd20..1eb997e649 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts @@ -68,10 +68,6 @@ function fillCircle( const boundsIJK = getBoundingBoxAroundShape(ellipsoidCornersIJK, dimensions); - if (boundsIJK.every(([min, max]) => min !== max)) { - throw new Error('Oblique segmentation tools are not supported yet'); - } - // using circle as a form of ellipse const ellipseObj = { center: center as Types.Point3, diff --git a/packages/tools/src/tools/segmentation/strategies/fillRectangle.ts b/packages/tools/src/tools/segmentation/strategies/fillRectangle.ts index 27762595ef..de5428a279 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillRectangle.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillRectangle.ts @@ -54,10 +54,6 @@ function fillRectangle( const boundsIJK = getBoundingBoxAroundShape(rectangleCornersIJK, dimensions); - if (boundsIJK.every(([min, max]) => min !== max)) { - throw new Error('Oblique segmentation tools are not supported yet'); - } - // Since always all points inside the boundsIJK is inside the rectangle... const pointInRectangle = () => true; From f310dec8e31253a78825a0823a3aa3edd4493f76 Mon Sep 17 00:00:00 2001 From: Alireza Date: Fri, 14 Jul 2023 12:11:54 -0400 Subject: [PATCH 05/20] add data set module to the metadata providers --- packages/adapters/package.json | 14 +++--- .../core/src/RenderingEngine/StackViewport.ts | 5 ++- .../src/imageLoader/getDatasetModule.ts | 44 +++++++++++++++++++ .../wadors/metaData/metaDataProvider.ts | 23 ++++++++++ .../wadouri/metaData/metaDataProvider.ts | 22 ++++++++++ 5 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 packages/dicomImageLoader/src/imageLoader/getDatasetModule.ts diff --git a/packages/adapters/package.json b/packages/adapters/package.json index 16459514c9..fd8b671744 100644 --- a/packages/adapters/package.json +++ b/packages/adapters/package.json @@ -19,11 +19,11 @@ "test": "jest --testTimeout 60000", "build": "rollup -c rollup.config.mjs", "build:esm": "rollup -c rollup.config.mjs", - "build:esm:watch": "rollup -c -w rollup.config.mjs", - "dev": "rollup -c -w rollup.config.mjs", + "build:esm:watch": "rollup --watch -c rollup.config.mjs", + "dev": "rollup --watch -c rollup.config.mjs", "build:all": "yarn build", "build:update-api": "yarn run build && api-extractor run --local", - "start": "rollup -c -w", + "start": "rollup --watch -c rollup.config.mjs", "format": "prettier --write 'src/**/*.js' 'test/**/*.js'", "lint": "eslint --fix ." }, @@ -39,14 +39,14 @@ "homepage": "https://github.com/cornerstonejs/cornerstone3D/blob/main/packages/adapters/README.md", "dependencies": { "@babel/runtime-corejs2": "^7.17.8", - "dcmjs": "^0.29.6", + "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "lodash.clonedeep": "^4.5.0", "ndarray": "^1.0.19" }, "devDependencies": { - "@cornerstonejs/core": "file:../core", - "@cornerstonejs/tools": "file:../tools", - "@cornerstonejs/streaming-image-volume-loader": "file:../streaming-image-volume-loader" + "@cornerstonejs/core": "^1.2.6", + "@cornerstonejs/tools": "^1.2.6", + "@cornerstonejs/streaming-image-volume-loader": "^1.2.6" } } diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index be3e6ccf03..86f3039cdb 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -1810,7 +1810,10 @@ class StackViewport extends Viewport implements IStackViewport { } //If Photometric Interpretation is not the same for the next image we are trying to load, invalidate the stack to recreate the VTK imageData - if (this.csImage?.imageFrame.photometricInterpretation !== image.imageFrame.photometricInterpretation) { + if ( + this.csImage?.imageFrame.photometricInterpretation !== + image?.imageFrame?.photometricInterpretation + ) { this.stackInvalidated = true; } diff --git a/packages/dicomImageLoader/src/imageLoader/getDatasetModule.ts b/packages/dicomImageLoader/src/imageLoader/getDatasetModule.ts new file mode 100644 index 0000000000..2522aec87b --- /dev/null +++ b/packages/dicomImageLoader/src/imageLoader/getDatasetModule.ts @@ -0,0 +1,44 @@ +/** + * Retrieves metadata from a DICOM image and returns it as an object with capitalized keys. + * @param imageId - the imageId + * @param metaDataProvider - The metadata provider either wadors or wadouri + * @param types - An array of metadata types to retrieve. + * @returns An object containing the retrieved metadata with capitalized keys. + */ +function getDatasetModule( + imageId: string, + metaDataProvider: any, + types: string[] +): object; +function getDatasetModule(imageId, metaDataProvider, types) { + const result = {}; + for (const t of types) { + try { + const data = metaDataProvider(t, imageId); + if (data) { + const capitalizedData = {}; + for (const key in data) { + if (key in data) { + // each tag should get capitalized to match dcmjs style. Todo: move all of the tags to dcmjs style + const capitalizedKey = capitalizeTag(key); + capitalizedData[capitalizedKey] = data[key]; + } + } + Object.assign(result, capitalizedData); + } + } catch (error) { + console.error(`Error retrieving ${t} data:`, error); + } + } + + return result; +} + +function capitalizeTag(tag) { + const parts = tag.split(/([0-9]+)/); + return parts + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(''); +} + +export { getDatasetModule }; diff --git a/packages/dicomImageLoader/src/imageLoader/wadors/metaData/metaDataProvider.ts b/packages/dicomImageLoader/src/imageLoader/wadors/metaData/metaDataProvider.ts index 9de5bdd538..c4c8fdf845 100644 --- a/packages/dicomImageLoader/src/imageLoader/wadors/metaData/metaDataProvider.ts +++ b/packages/dicomImageLoader/src/imageLoader/wadors/metaData/metaDataProvider.ts @@ -15,6 +15,7 @@ import { } from './extractPositioningFromMetadata'; import { getImageTypeSubItemFromMetadata } from './NMHelpers'; import isNMReconstructable from '../../isNMReconstructable'; +import { getDatasetModule } from '../../getDatasetModule'; function metaDataProvider(type, imageId) { if (type === 'multiframeModule') { @@ -266,6 +267,28 @@ function metaDataProvider(type, imageId) { actualFrameDuration: getNumberValue(metaData['00181242']), }; } + + // This is used for gathering all the metadata for export + if (type === 'dataset') { + const types = [ + 'multiframeModule', + 'generalSeriesModule', + 'patientStudyModule', + 'nmMultiframeGeometryModule', + 'imagePlaneModule', + 'imagePixelModule', + 'voiLutModule', + 'modalityLutModule', + 'sopCommonModule', + 'petIsotopeModule', + 'overlayPlaneModule', + 'transferSyntax', + 'petSeriesModule', + 'petImageModule', + ]; + + return getDatasetModule(imageId, metaDataProvider, types); + } } export default metaDataProvider; diff --git a/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts b/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts index d63d35fc41..dbe74b49df 100644 --- a/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts +++ b/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts @@ -16,6 +16,7 @@ import { extractSliceThicknessFromDataset, } from './extractPositioningFromDataset'; import isNMReconstructable from '../../isNMReconstructable'; +import { getDatasetModule } from '../../getDatasetModule'; function metaDataProvider(type, imageId) { const { dicomParser } = external; @@ -245,6 +246,27 @@ function metaDataProvider(type, imageId) { actualFrameDuration: dicomParser.intString(dataSet.string('x00181242')), }; } + + if (type === 'dataset') { + const types = [ + 'multiframeModule', + 'generalSeriesModule', + 'patientStudyModule', + 'imagePlaneModule', + 'nmMultiframeGeometryModule', + 'imagePixelModule', + 'modalityLutModule', + 'voiLutModule', + 'sopCommonModule', + 'petIsotopeModule', + 'overlayPlaneModule', + 'transferSyntax', + 'petSeriesModule', + 'petImageModule', + ]; + + return getDatasetModule(imageId, metaDataProvider, types); + } } export default metaDataProvider; From 2750e8dd2d6f3702d7a46fcc5e6f0fe5f832906a Mon Sep 17 00:00:00 2001 From: Alireza Date: Fri, 14 Jul 2023 12:12:50 -0400 Subject: [PATCH 06/20] add a new method to segmentation state --- .../segmentation/SegmentationStateManager.ts | 19 +++++++++++++++++++ .../segmentation/segmentationState.ts | 17 +++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/tools/src/stateManagement/segmentation/SegmentationStateManager.ts b/packages/tools/src/stateManagement/segmentation/SegmentationStateManager.ts index 787fab2d68..04156c5804 100644 --- a/packages/tools/src/stateManagement/segmentation/SegmentationStateManager.ts +++ b/packages/tools/src/stateManagement/segmentation/SegmentationStateManager.ts @@ -134,6 +134,25 @@ export default class SegmentationStateManager { return toolGroupSegRepresentationsWithConfig.segmentationRepresentations; } + /** + * Returns an array of all segmentation representations for all tool groups. + * @returns An array of ToolGroupSpecificRepresentations. + */ + getAllSegmentationRepresentations(): Record< + string, + ToolGroupSpecificRepresentation[] + > { + const toolGroupSegReps: Record = + {}; + Object.entries(this.state.toolGroups).forEach( + ([toolGroupId, toolGroupSegRepresentationsWithConfig]) => { + toolGroupSegReps[toolGroupId] = + toolGroupSegRepresentationsWithConfig.segmentationRepresentations; + } + ); + return toolGroupSegReps; + } + /** * Add a new segmentation representation to the toolGroup's segmentation representations. * @param toolGroupId - The Id of the tool group . diff --git a/packages/tools/src/stateManagement/segmentation/segmentationState.ts b/packages/tools/src/stateManagement/segmentation/segmentationState.ts index 6d458abec4..5093cdaf86 100644 --- a/packages/tools/src/stateManagement/segmentation/segmentationState.ts +++ b/packages/tools/src/stateManagement/segmentation/segmentationState.ts @@ -85,6 +85,18 @@ function getSegmentationRepresentations( return segmentationStateManager.getSegmentationRepresentations(toolGroupId); } +/** + * Get all segmentation representations in the state + * @returns An array of segmentation representation objects. + */ +function getAllSegmentationRepresentations(): Record< + string, + ToolGroupSpecificRepresentation[] +> { + const segmentationStateManager = getDefaultSegmentationStateManager(); + return segmentationStateManager.getAllSegmentationRepresentations(); +} + /** * Get the tool group IDs that have a segmentation representation with the given * segmentationId @@ -92,6 +104,10 @@ function getSegmentationRepresentations( * @returns An array of tool group IDs. */ function getToolGroupIdsWithSegmentation(segmentationId: string): string[] { + if (!segmentationId) { + throw new Error('getToolGroupIdsWithSegmentation: segmentationId is empty'); + } + const segmentationStateManager = getDefaultSegmentationStateManager(); const state = segmentationStateManager.getState(); const toolGroupIds = Object.keys(state.toolGroups); @@ -405,6 +421,7 @@ export { setSegmentSpecificRepresentationConfig, // helpers s getToolGroupIdsWithSegmentation, + getAllSegmentationRepresentations, getSegmentationRepresentationByUID, // color addColorLUT, From 454da6ef934b66e8406b47a470e3d105449cd960 Mon Sep 17 00:00:00 2001 From: Alireza Date: Fri, 14 Jul 2023 17:04:49 -0400 Subject: [PATCH 07/20] add the new parser --- packages/adapters/package.json | 3 +- .../adapters/Cornerstone/Segmentation_4X.js | 318 +++++++++++------- .../Segmentation/generateSegmentation.ts | 6 +- yarn.lock | 31 ++ 4 files changed, 243 insertions(+), 115 deletions(-) diff --git a/packages/adapters/package.json b/packages/adapters/package.json index fd8b671744..f0403cf5ce 100644 --- a/packages/adapters/package.json +++ b/packages/adapters/package.json @@ -42,7 +42,8 @@ "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "lodash.clonedeep": "^4.5.0", - "ndarray": "^1.0.19" + "ndarray": "^1.0.19", + "buffer": "^6.0.3" }, "devDependencies": { "@cornerstonejs/core": "^1.2.6", diff --git a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js index e4920c5f18..ed48f0d593 100644 --- a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js +++ b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js @@ -1,4 +1,10 @@ -import { log, utilities, normalizers, derivations } from "dcmjs"; +import { + log, + data as dcmjsData, + utilities, + normalizers, + derivations +} from "dcmjs"; import ndarray from "ndarray"; import cloneDeep from "lodash.clonedeep"; @@ -11,7 +17,7 @@ const { } = utilities.orientation; const { datasetToBlob, BitArray, DicomMessage, DicomMetaDictionary } = - utilities; + dcmjsData; const { Normalizer } = normalizers; const { Segmentation: SegmentationDerivation } = derivations; @@ -146,7 +152,7 @@ function fillSegmentation(segmentation, inputLabelmaps3D, userOptions = {}) { } ); const segmentMetadata = metadata[segmentIndex]; - const labelmaps = _getLabelmapsFromRefernecedFrameIndicies( + const labelmaps = _getLabelmapsFromReferencedFrameIndicies( labelmap3D, referencedFrameIndicies ); @@ -160,7 +166,6 @@ function fillSegmentation(segmentation, inputLabelmaps3D, userOptions = {}) { } } } - if (options.rleEncode) { const rleEncodedFrames = encode( segmentation.dataset.PixelData, @@ -197,7 +202,7 @@ function fillSegmentation(segmentation, inputLabelmaps3D, userOptions = {}) { return segBlob; } -function _getLabelmapsFromRefernecedFrameIndicies( +function _getLabelmapsFromReferencedFrameIndicies( labelmap3D, referencedFrameIndicies ) { @@ -260,8 +265,7 @@ function _createSegFromImages(images, isMultiframe, options) { * @param {string[]} imageIds - An array of the imageIds. * @param {ArrayBuffer} arrayBuffer - The SEG arrayBuffer. * @param {*} metadataProvider. - * @param {bool} skipOverlapping - skip checks for overlapping segs, default value false. - * @param {number} tolerance - default value 1.e-3. + * @param {obj} options - Options object. * * @return {[]ArrayBuffer}a list of array buffer for each labelMap * @return {Object} an object from which the segment metadata can be derived @@ -269,13 +273,12 @@ function _createSegFromImages(images, isMultiframe, options) { * @return {[][][]} 3D list containing the track of segments per frame for each labelMap * (available only for the overlapping case). */ -function generateToolState( - imageIds, - arrayBuffer, - metadataProvider, - skipOverlapping = false, - tolerance = 1e-3 -) { +function generateToolState(imageIds, arrayBuffer, metadataProvider, options) { + const { + skipOverlapping = false, + tolerance = 1e-3, + TypedArrayConstructor = Uint8Array + } = options; const dicomData = DicomMessage.readFile(arrayBuffer); const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); @@ -317,6 +320,7 @@ function generateToolState( const TransferSyntaxUID = multiframe._meta.TransferSyntaxUID.Value[0]; let pixelData; + let pixelDataChunks; if (TransferSyntaxUID === "1.2.840.10008.1.2.5") { const rleEncodedFrames = Array.isArray(multiframe.PixelData) @@ -334,10 +338,13 @@ function generateToolState( return; } + + // Todo: need to test this with rle data + pixelDataChunks = [pixelData]; } else { - pixelData = unpackPixelData(multiframe); + pixelDataChunks = unpackPixelData(multiframe); - if (!pixelData) { + if (!pixelDataChunks) { throw new Error("Fractional segmentations are not yet supported"); } } @@ -349,15 +356,29 @@ function generateToolState( tolerance ); + // Pre-compute the sop UID to imageId index map so that in the for loop + // we don't have to call metadataProvider.get() for each imageId over + // and over again. + const sopUIDImageIdIndexMap = imageIds.reduce((acc, imageId) => { + const { sopInstanceUid } = metadataProvider.get( + "generalImageModule", + imageId + ); + acc[sopInstanceUid] = imageId; + return acc; + }, {}); + let overlapping = false; if (!skipOverlapping) { overlapping = checkSEGsOverlapping( - pixelData, + pixelDataChunks, multiframe, imageIds, validOrientations, metadataProvider, - tolerance + sopUIDImageIdIndexMap, + tolerance, + TypedArrayConstructor ); } @@ -392,7 +413,8 @@ function generateToolState( segmentsOnFrameArray[0] = []; const segmentsOnFrame = []; - const arrayBufferLength = sliceLength * imageIds.length * 2; // 2 bytes per label voxel in cst4. + const arrayBufferLength = + sliceLength * imageIds.length * TypedArrayConstructor.BYTES_PER_ELEMENT; const labelmapBufferArray = []; labelmapBufferArray[0] = new ArrayBuffer(arrayBufferLength); @@ -400,12 +422,14 @@ function generateToolState( segmentsOnFrame, segmentsOnFrameArray, labelmapBufferArray, - pixelData, + pixelDataChunks, multiframe, imageIds, validOrientations, metadataProvider, - tolerance + sopUIDImageIdIndexMap, + tolerance, + TypedArrayConstructor ); return { @@ -593,8 +617,7 @@ function generateToolState( * @param {Object} multiframe dicom metadata * @param {Int} frameSegment frame dicom index * @param {String[]} imageIds A list of imageIds. - * @param {Object} metadataProvider A Cornerstone metadataProvider to query - * metadata from imageIds. + * @param {Object} sopUIDImageIdIndexMap A map of SOPInstanceUID to imageId * @param {Float} tolerance The tolerance parameter * * @returns {String} Returns the imageId @@ -604,6 +627,7 @@ function findReferenceSourceImageId( frameSegment, imageIds, metadataProvider, + sopUIDImageIdIndexMap, tolerance ) { let imageId = undefined; @@ -661,10 +685,9 @@ function findReferenceSourceImageId( } if (frameSourceImageSequence) { - imageId = getImageIdOfSourceImagebySourceImageSequence( + imageId = getImageIdOfSourceImageBySourceImageSequence( frameSourceImageSequence, - imageIds, - metadataProvider + sopUIDImageIdIndexMap ); } @@ -699,7 +722,9 @@ function checkSEGsOverlapping( imageIds, validOrientations, metadataProvider, - tolerance + sopUIDImageIdIndexMap, + tolerance, + TypedArrayConstructor ) { const { SharedFunctionalGroupsSequence, @@ -745,6 +770,7 @@ function checkSEGsOverlapping( frameSegment, imageIds, metadataProvider, + sopUIDImageIdIndexMap, tolerance ); @@ -771,7 +797,7 @@ function checkSEGsOverlapping( } for (let [, role] of frameSegmentsMapping.entries()) { - let temp2DArray = new Uint16Array(sliceLength).fill(0); + let temp2DArray = new TypedArrayConstructor(sliceLength).fill(0); for (let i = 0; i < role.length; ++i) { const frameSegment = role[i]; @@ -784,15 +810,14 @@ function checkSEGsOverlapping( PerFrameFunctionalGroups.PlaneOrientationSequence .ImageOrientationPatient; - const pixelDataI2D = ndarray( - new Uint8Array( - pixelData.buffer, - frameSegment * sliceLength, - sliceLength - ), - [Rows, Columns] + const view = readFromUnpackedChunks( + pixelData, + frameSegment * sliceLength, + sliceLength ); + const pixelDataI2D = ndarray(view, [Rows, Columns]); + const alignedPixelDataI = alignPixelDataWithSourceData( pixelDataI2D, ImageOrientationPatientI, @@ -831,7 +856,9 @@ function insertOverlappingPixelDataPlanar( imageIds, validOrientations, metadataProvider, - tolerance + sopUIDImageIdIndexMap, + tolerance, + TypedArrayConstructor ) { const { SharedFunctionalGroupsSequence, @@ -846,8 +873,8 @@ function insertOverlappingPixelDataPlanar( .ImageOrientationPatient : undefined; const sliceLength = Columns * Rows; - const arrayBufferLength = sliceLength * imageIds.length * 2; // 2 bytes per label voxel in cst4. - + const arrayBufferLength = + sliceLength * imageIds.length * TypedArrayConstructor.BYTES_PER_ELEMENT; // indicate the number of labelMaps let M = 1; @@ -897,11 +924,17 @@ function insertOverlappingPixelDataPlanar( PerFrameFunctionalGroups.PlaneOrientationSequence .ImageOrientationPatient; - const pixelDataI2D = ndarray( - new Uint8Array(pixelData.buffer, i * sliceLength, sliceLength), - [Rows, Columns] + // Since we moved to the chunks approach, we need to read the data + // and handle scenarios where the portion of data is in one chunk + // and the other portion is in another chunk + const view = readFromUnpackedChunks( + pixelData, + i * sliceLength, + sliceLength ); + const pixelDataI2D = ndarray(view, [Rows, Columns]); + const alignedPixelDataI = alignPixelDataWithSourceData( pixelDataI2D, ImageOrientationPatientI, @@ -921,6 +954,7 @@ function insertOverlappingPixelDataPlanar( i, imageIds, metadataProvider, + sopUIDImageIdIndexMap, tolerance ); @@ -951,9 +985,12 @@ function insertOverlappingPixelDataPlanar( const imageIdIndex = imageIds.findIndex( element => element === imageId ); - const byteOffset = sliceLength * 2 * imageIdIndex; // 2 bytes/pixel + const byteOffset = + sliceLength * + imageIdIndex * + TypedArrayConstructor.BYTES_PER_ELEMENT; - const labelmap2DView = new Uint16Array( + const labelmap2DView = new TypedArrayConstructor( tempBuffer, byteOffset, sliceLength @@ -1035,7 +1072,9 @@ function insertPixelDataPlanar( imageIds, validOrientations, metadataProvider, - tolerance + sopUIDImageIdIndexMap, + tolerance, + TypedArrayConstructor ) { const { SharedFunctionalGroupsSequence, @@ -1063,11 +1102,14 @@ function insertPixelDataPlanar( PerFrameFunctionalGroups.PlaneOrientationSequence .ImageOrientationPatient; - const pixelDataI2D = ndarray( - new Uint8Array(pixelData.buffer, i * sliceLength, sliceLength), - [Rows, Columns] + const view = readFromUnpackedChunks( + pixelData, + i * sliceLength, + sliceLength ); + const pixelDataI2D = ndarray(view, [Rows, Columns]); + const alignedPixelDataI = alignPixelDataWithSourceData( pixelDataI2D, ImageOrientationPatientI, @@ -1094,6 +1136,7 @@ function insertPixelDataPlanar( i, imageIds, metadataProvider, + sopUIDImageIdIndexMap, tolerance ); @@ -1117,9 +1160,12 @@ function insertPixelDataPlanar( } const imageIdIndex = imageIds.findIndex(element => element === imageId); - const byteOffset = sliceLength * 2 * imageIdIndex; // 2 bytes/pixel + const byteOffset = + sliceLength * + imageIdIndex * + TypedArrayConstructor.BYTES_PER_ELEMENT; - const labelmap2DView = new Uint16Array( + const labelmap2DView = new TypedArrayConstructor( labelmapBufferArray[0], byteOffset, sliceLength @@ -1215,7 +1261,7 @@ function checkIfPerpendicular(iop1, iop2, tolerance) { } /** - * unpackPixelData - Unpacks bitpacked pixelData if the Segmentation is BINARY. + * unpackPixelData - Unpacks bit packed pixelData if the Segmentation is BINARY. * * @param {Object} multiframe The multiframe dataset. * @return {Uint8Array} The unpacked pixelData. @@ -1235,7 +1281,10 @@ function unpackPixelData(multiframe) { } if (segType === "BINARY") { - return BitArray.unpack(data); + // For extreme big data, we can't unpack the data at once and we need to + // chunk it and unpack each chunk separately. + // MAX 2GB is the limit right now to allocate a buffer + return getUnpackedChunks(data, 199000000); } const pixelData = new Uint8Array(data); @@ -1257,19 +1306,40 @@ function unpackPixelData(multiframe) { return pixelData; } +function getUnpackedChunks(data, maxBytesPerChunk) { + var bitArray = new Uint8Array(data); + var chunks = []; + + var maxBitsPerChunk = maxBytesPerChunk * 8; + var numberOfChunks = Math.ceil((bitArray.length * 8) / maxBitsPerChunk); + + for (var i = 0; i < numberOfChunks; i++) { + var startBit = i * maxBitsPerChunk; + var endBit = Math.min(startBit + maxBitsPerChunk, bitArray.length * 8); + + var startByte = Math.floor(startBit / 8); + var endByte = Math.ceil(endBit / 8); + + var chunk = bitArray.slice(startByte, endByte); + var unpackedChunk = BitArray.unpack(chunk); + + chunks.push(unpackedChunk); + } + + return chunks; +} + /** - * getImageIdOfSourceImagebySourceImageSequence - Returns the Cornerstone imageId of the source image. + * getImageIdOfSourceImageBySourceImageSequence - Returns the Cornerstone imageId of the source image. * * @param {Object} SourceImageSequence Sequence describing the source image. * @param {String[]} imageIds A list of imageIds. - * @param {Object} metadataProvider A Cornerstone metadataProvider to query - * metadata from imageIds. + * @param {Object} sopUIDImageIdIndexMap A map of SOPInstanceUIDs to imageIds. * @return {String} The corresponding imageId. */ -function getImageIdOfSourceImagebySourceImageSequence( +function getImageIdOfSourceImageBySourceImageSequence( SourceImageSequence, - imageIds, - metadataProvider + sopUIDImageIdIndexMap ) { const { ReferencedSOPInstanceUID, ReferencedFrameNumber } = SourceImageSequence; @@ -1278,14 +1348,9 @@ function getImageIdOfSourceImagebySourceImageSequence( ? getImageIdOfReferencedFrame( ReferencedSOPInstanceUID, ReferencedFrameNumber, - imageIds, - metadataProvider + sopUIDImageIdIndexMap ) - : getImageIdOfReferencedSingleFramedSOPInstance( - ReferencedSOPInstanceUID, - imageIds, - metadataProvider - ); + : sopUIDImageIdIndexMap[ReferencedSOPInstanceUID]; } /** @@ -1295,7 +1360,7 @@ function getImageIdOfSourceImagebySourceImageSequence( * @param {String} FrameOfReferenceUID Frame of reference. * @param {Object} PerFrameFunctionalGroup Sequence describing segmentation reference attributes per frame. * @param {String[]} imageIds A list of imageIds. - * @param {Object} metadataProvider A Cornerstone metadataProvider to query + * @param {Object} sopUIDImageIdIndexMap A map of SOPInstanceUIDs to imageIds. * @param {Float} tolerance The tolerance parameter * * @return {String} The corresponding imageId. @@ -1351,34 +1416,6 @@ function getImageIdOfSourceImagebyGeometry( } } -/** - * getImageIdOfReferencedSingleFramedSOPInstance - Returns the imageId - * corresponding to the specified sopInstanceUid for single-frame images. - * - * @param {String} sopInstanceUid The sopInstanceUid of the desired image. - * @param {String[]} imageIds The list of imageIds. - * @param {Object} metadataProvider The metadataProvider to obtain sopInstanceUids - * from the cornerstone imageIds. - * @return {String} The imageId that corresponds to the sopInstanceUid. - */ -function getImageIdOfReferencedSingleFramedSOPInstance( - sopInstanceUid, - imageIds, - metadataProvider -) { - return imageIds.find(imageId => { - const sopCommonModule = metadataProvider.get( - "sopCommonModule", - imageId - ); - if (!sopCommonModule) { - return; - } - - return sopCommonModule.sopInstanceUID === sopInstanceUid; - }); -} - /** * getImageIdOfReferencedFrame - Returns the imageId corresponding to the * specified sopInstanceUid and frameNumber for multi-frame images. @@ -1386,35 +1423,23 @@ function getImageIdOfReferencedSingleFramedSOPInstance( * @param {String} sopInstanceUid The sopInstanceUid of the desired image. * @param {Number} frameNumber The frame number. * @param {String} imageIds The list of imageIds. - * @param {Object} metadataProvider The metadataProvider to obtain sopInstanceUids - * from the cornerstone imageIds. + * @param {Object} sopUIDImageIdIndexMap A map of SOPInstanceUIDs to imageIds. * @return {String} The imageId that corresponds to the sopInstanceUid. */ function getImageIdOfReferencedFrame( sopInstanceUid, frameNumber, - imageIds, - metadataProvider + sopUIDImageIdIndexMap ) { - const imageId = imageIds.find(imageId => { - const sopCommonModule = metadataProvider.get( - "sopCommonModule", - imageId - ); - if (!sopCommonModule) { - return; - } + const imageId = sopUIDImageIdIndexMap[sopInstanceUid]; - const imageIdFrameNumber = Number(imageId.split("frame=")[1]); + if (!imageId) { + return; + } - return ( - //frameNumber is zero indexed for cornerstoneDICOMImageLoader image Ids. - sopCommonModule.sopInstanceUID === sopInstanceUid && - imageIdFrameNumber === frameNumber - 1 - ); - }); + const imageIdFrameNumber = Number(imageId.split("frame=")[1]); - return imageId; + return imageIdFrameNumber === frameNumber - 1 ? imageId : undefined; } /** @@ -1541,3 +1566,70 @@ function getSegmentMetadata(multiframe, seriesInstanceUid) { data }; } + +function readFromUnpackedChunks(chunks, offset, length) { + const mapping = getUnpackedOffsetAndLength(chunks, offset, length); + + // If all the data is in one chunk, we can just slice that chunk + if (mapping.start.chunkIndex === mapping.end.chunkIndex) { + return new Uint8Array( + chunks[mapping.start.chunkIndex].buffer, + mapping.start.offset, + length + ); + } else { + // If the data spans multiple chunks, we need to create a new Uint8Array and copy the data from each chunk + let result = new Uint8Array(length); + let resultOffset = 0; + + for ( + let i = mapping.start.chunkIndex; + i <= mapping.end.chunkIndex; + i++ + ) { + let start = + i === mapping.start.chunkIndex ? mapping.start.offset : 0; + let end = + i === mapping.end.chunkIndex + ? mapping.end.offset + : chunks[i].length; + + result.set( + new Uint8Array(chunks[i].buffer, start, end - start), + resultOffset + ); + resultOffset += end - start; + } + + return result; + } +} + +function getUnpackedOffsetAndLength(chunks, offset, length) { + var totalBytes = chunks.reduce((total, chunk) => total + chunk.length, 0); + + if (offset < 0 || offset + length > totalBytes) { + throw new Error("Offset and length out of bounds"); + } + + var startChunkIndex = 0; + var startOffsetInChunk = offset; + + while (startOffsetInChunk >= chunks[startChunkIndex].length) { + startOffsetInChunk -= chunks[startChunkIndex].length; + startChunkIndex++; + } + + var endChunkIndex = startChunkIndex; + var endOffsetInChunk = startOffsetInChunk + length; + + while (endOffsetInChunk > chunks[endChunkIndex].length) { + endOffsetInChunk -= chunks[endChunkIndex].length; + endChunkIndex++; + } + + return { + start: { chunkIndex: startChunkIndex, offset: startOffsetInChunk }, + end: { chunkIndex: endChunkIndex, offset: endOffsetInChunk } + }; +} diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts index f5523eaa51..472acf017c 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts @@ -1,7 +1,9 @@ import { utilities, normalizers, derivations, data as dcmjsData } from "dcmjs"; +import { Buffer } from "buffer"; const { Normalizer } = normalizers; const { Segmentation: SegmentationDerivation } = derivations; +const { datasetToDict } = dcmjsData; const { encode } = utilities.compression; @@ -169,7 +171,9 @@ function fillSegmentation(segmentation, inputLabelmaps3D, userOptions = {}) { segmentation.bitPackPixelData(); } - const segBlob = dcmjsData.datasetToBlob(segmentation.dataset); + // const segBlob = dcmjsData.datasetToBlob(segmentation.dataset); + const buffer = Buffer.from(datasetToDict(segmentation.dataset).write()); + const segBlob = new Blob([buffer], { type: "application/dicom" }); return segBlob; } diff --git a/yarn.lock b/yarn.lock index 75f13e923d..ff17cdd4f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1211,6 +1211,14 @@ core-js-pure "^3.25.1" regenerator-runtime "^0.13.11" +"@babel/runtime-corejs3@^7.22.5": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.22.6.tgz#e8e625eb3db29491e0326b3aeb9929c43b270ae4" + integrity sha512-M+37LLIRBTEVjktoJjbw4KVhupF0U/3PYUCbBwgAd9k17hoKhRu1n935QiG7Tuxv0LJOMrb2vuKEeYUlv0iyiw== + dependencies: + core-js-pure "^3.30.2" + regenerator-runtime "^0.13.11" + "@babel/runtime@7.17.9": version "7.17.9" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" @@ -5226,6 +5234,11 @@ address@^1.0.1, address@^1.1.2: resolved "https://registry.npmjs.org/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== +adm-zip@^0.5.10: + version "0.5.10" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.10.tgz#4a51d5ab544b1f5ce51e1b9043139b639afff45b" + integrity sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ== + agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -7661,6 +7674,11 @@ core-js-pure@^3.25.1: resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.30.1.tgz#7d93dc89e7d47b8ef05d7e79f507b0e99ea77eec" integrity sha512-nXBEVpmUnNRhz83cHd9JRQC52cTMcuXAmR56+9dSMpRdpeA4I1PX6yjmhd71Eyc/wXNsdBdUDIj1QTIeZpU5Tg== +core-js-pure@^3.30.2: + version "3.31.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.31.1.tgz#73d154958881873bc19381df80bddb20c8d0cdb5" + integrity sha512-w+C62kvWti0EPs4KPMCMVv9DriHSXfQOCQ94bGGBiEW5rrbtt/Rz8n5Krhfw9cpFyzXBjf3DB3QnPdEzGDY4Fw== + core-js@^2.4.0, core-js@^2.6.12: version "2.6.12" resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" @@ -8205,6 +8223,19 @@ dcmjs@^0.29.6: ndarray "^1.0.19" pako "^2.0.4" +dcmjs@^0.29.8: + version "0.29.8" + resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.29.8.tgz#3daa5224f8e75b2e5b069590bef26a272741fa44" + integrity sha512-Y0/KZAmT1siVo7eH3KK4ZflEbNi61soUpD0N7lsXMVVJQ6IZkHlaSzb9DtqnEpMs7RJDfvZGr1uXpv1vBBIypQ== + dependencies: + "@babel/runtime-corejs3" "^7.22.5" + adm-zip "^0.5.10" + gl-matrix "^3.1.0" + lodash.clonedeep "^4.5.0" + loglevelnext "^3.0.1" + ndarray "^1.0.19" + pako "^2.0.4" + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" From 9d4ad01235320775f6f8f96c1266a863177d1b0e Mon Sep 17 00:00:00 2001 From: Alireza Date: Mon, 17 Jul 2023 08:32:43 -0400 Subject: [PATCH 08/20] work without rle encode --- .../adapters/Cornerstone/Segmentation_4X.js | 26 +-- .../Segmentation/generateSegmentation.ts | 192 +----------------- .../tools/examples/labelmapRendering/index.ts | 31 +-- 3 files changed, 24 insertions(+), 225 deletions(-) diff --git a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js index ed48f0d593..cadfe731d2 100644 --- a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js +++ b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js @@ -7,6 +7,7 @@ import { } from "dcmjs"; import ndarray from "ndarray"; import cloneDeep from "lodash.clonedeep"; +import { Buffer } from "buffer"; const { rotateDirectionCosinesInPlane, @@ -16,7 +17,7 @@ const { nearlyEqual } = utilities.orientation; -const { datasetToBlob, BitArray, DicomMessage, DicomMetaDictionary } = +const { datasetToDict, BitArray, DicomMessage, DicomMetaDictionary } = dcmjsData; const { Normalizer } = normalizers; @@ -24,14 +25,6 @@ const { Segmentation: SegmentationDerivation } = derivations; const { encode, decode } = utilities.compression; -const Segmentation = { - generateSegmentation, - generateToolState, - fillSegmentation -}; - -export default Segmentation; - /** * * @typedef {Object} BrushData @@ -39,10 +32,9 @@ export default Segmentation; * @property {Object[]} segments - The cornerstoneTools segment metadata that corresponds to the * seriesInstanceUid. */ - const generateSegmentationDefaultOptions = { includeSliceSpacing: true, - rleEncode: true + rleEncode: false }; /** @@ -197,7 +189,8 @@ function fillSegmentation(segmentation, inputLabelmaps3D, userOptions = {}) { segmentation.bitPackPixelData(); } - const segBlob = datasetToBlob(segmentation.dataset); + const buffer = Buffer.from(datasetToDict(segmentation.dataset).write()); + const segBlob = new Blob([buffer], { type: "application/dicom" }); return segBlob; } @@ -1633,3 +1626,12 @@ function getUnpackedOffsetAndLength(chunks, offset, length) { end: { chunkIndex: endChunkIndex, offset: endOffsetInChunk } }; } + +const Segmentation = { + generateSegmentation, + generateToolState, + fillSegmentation +}; + +export default Segmentation; +export { fillSegmentation, generateSegmentation, generateToolState }; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts index 472acf017c..3272102144 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts @@ -1,23 +1,8 @@ -import { utilities, normalizers, derivations, data as dcmjsData } from "dcmjs"; -import { Buffer } from "buffer"; +import { , normalizers, derivations } from "dcmjs"; +import { fillSegmentation } from "../../Cornerstone/Segmentation_4X"; const { Normalizer } = normalizers; const { Segmentation: SegmentationDerivation } = derivations; -const { datasetToDict } = dcmjsData; - -const { encode } = utilities.compression; - -/** - * - * @typedef {Object} BrushData - * @property {Object} toolState - The cornerstoneTools global toolState. - * @property {Object[]} segments - The cornerstoneTools segment metadata that corresponds to the - * seriesInstanceUid. - */ -const generateSegmentationDefaultOptions = { - includeSliceSpacing: true, - rleEncode: false -}; /** * generateSegmentation - Generates a DICOM Segmentation object given cornerstoneTools data. @@ -25,12 +10,7 @@ const generateSegmentationDefaultOptions = { * @param images - An array of the cornerstone image objects, which includes imageId and metadata * @param labelmaps - An array of the 3D Volumes that contain the segmentation data. */ -function generateSegmentation( - images, - labelmaps, - metadata, - options = { includeSliceSpacing: true } -) { +function generateSegmentation(images, labelmaps, metadata, options) { const segmentation = _createMultiframeSegmentationFromReferencedImages( images, metadata, @@ -39,162 +19,6 @@ function generateSegmentation( return fillSegmentation(segmentation, labelmaps, options); } -/** - * fillSegmentation - Fills a derived segmentation dataset with cornerstoneTools `LabelMap3D` data. - * - * @param {object[]} segmentation An empty segmentation derived dataset. - * @param {Object|Object[]} inputLabelmaps3D The cornerstone `Labelmap3D` object, or an array of objects. - * @param {Object} userOptions Options object to override default options. - * @returns {Blob} description - */ -function fillSegmentation(segmentation, inputLabelmaps3D, userOptions = {}) { - const options = Object.assign( - {}, - generateSegmentationDefaultOptions, - userOptions - ); - - // Use another variable so we don't redefine labelmaps3D. - const labelmaps3D = Array.isArray(inputLabelmaps3D) - ? inputLabelmaps3D - : [inputLabelmaps3D]; - - let numberOfFrames = 0; - const referencedFramesPerLabelmap = []; - - for ( - let labelmapIndex = 0; - labelmapIndex < labelmaps3D.length; - labelmapIndex++ - ) { - const labelmap3D = labelmaps3D[labelmapIndex]; - const { metadata, labelmaps2D } = labelmap3D; - - const referencedFramesPerSegment = []; - - for (let i = 1; i < metadata.length; i++) { - if (metadata[i]) { - referencedFramesPerSegment[i] = []; - } - } - - for (let i = 0; i < labelmaps2D.length; i++) { - const labelmap2D = labelmaps2D[i]; - - if (labelmaps2D[i]) { - const { segmentsOnLabelmap } = labelmap2D; - - segmentsOnLabelmap.forEach(segmentIndex => { - if (segmentIndex !== 0) { - referencedFramesPerSegment[segmentIndex].push(i); - numberOfFrames++; - } - }); - } - } - - referencedFramesPerLabelmap[labelmapIndex] = referencedFramesPerSegment; - } - - segmentation.setNumberOfFrames(numberOfFrames); - - for ( - let labelmapIndex = 0; - labelmapIndex < labelmaps3D.length; - labelmapIndex++ - ) { - const referencedFramesPerSegment = - referencedFramesPerLabelmap[labelmapIndex]; - - const labelmap3D = labelmaps3D[labelmapIndex]; - const { metadata } = labelmap3D; - - for ( - let segmentIndex = 1; - segmentIndex < referencedFramesPerSegment.length; - segmentIndex++ - ) { - const referencedFrameIndices = - referencedFramesPerSegment[segmentIndex]; - - if (referencedFrameIndices) { - // Frame numbers start from 1. - const referencedFrameNumbers = referencedFrameIndices.map( - element => { - return element + 1; - } - ); - const segmentMetadata = metadata[segmentIndex]; - const labelmaps = _getLabelmapsFromReferencedFrameIndices( - labelmap3D, - referencedFrameIndices - ); - - segmentation.addSegmentFromLabelmap( - segmentMetadata, - labelmaps, - segmentIndex, - referencedFrameNumbers - ); - } - } - } - - if (options.rleEncode) { - const rleEncodedFrames = encode( - segmentation.dataset.PixelData, - numberOfFrames, - segmentation.dataset.Rows, - segmentation.dataset.Columns - ); - - // Must use fractional now to RLE encode, as the DICOM standard only allows BitStored && BitsAllocated - // to be 1 for BINARY. This is not ideal and there should be a better format for compression in this manner - // added to the standard. - segmentation.assignToDataset({ - BitsAllocated: "8", - BitsStored: "8", - HighBit: "7", - SegmentationType: "FRACTIONAL", - SegmentationFractionalType: "PROBABILITY", - MaximumFractionalValue: "255" - }); - - segmentation.dataset._meta.TransferSyntaxUID = { - Value: ["1.2.840.10008.1.2.5"], - vr: "UI" - }; - segmentation.dataset._vrMap.PixelData = "OB"; - segmentation.dataset.PixelData = rleEncodedFrames; - } else { - // If no rleEncoding, at least bitpack the data. - segmentation.bitPackPixelData(); - } - - // const segBlob = dcmjsData.datasetToBlob(segmentation.dataset); - const buffer = Buffer.from(datasetToDict(segmentation.dataset).write()); - const segBlob = new Blob([buffer], { type: "application/dicom" }); - - return segBlob; -} - -function _getLabelmapsFromReferencedFrameIndices( - labelmap3D, - referencedFrameIndices -) { - const { labelmaps2D } = labelmap3D; - - const labelmaps = []; - - for (let i = 0; i < referencedFrameIndices.length; i++) { - const frame = referencedFrameIndices[i]; - - labelmaps.push(labelmaps2D[frame].pixelData); - } - - return labelmaps; -} - /** * _createMultiframeSegmentationFromReferencedImages - description * @@ -207,16 +31,6 @@ function _createMultiframeSegmentationFromReferencedImages( metadata, options ) { - // for (let i = 0; i < images.length; i++) { - // const image = images[i]; - // const arrayBuffer = image.data.byteArray.buffer; - // const dicomData = DicomMessage.readFile(arrayBuffer); - // const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); - - // dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); - // datasets.push(dataset); - // } - const datasets = images.map(image => { // add the sopClassUID to the dataset const allMetadataModules = metadata.get("dataset", image.imageId); diff --git a/packages/tools/examples/labelmapRendering/index.ts b/packages/tools/examples/labelmapRendering/index.ts index 610f874da3..5dd1bee01b 100644 --- a/packages/tools/examples/labelmapRendering/index.ts +++ b/packages/tools/examples/labelmapRendering/index.ts @@ -9,18 +9,14 @@ import { initDemo, createImageIdsAndCacheMetaData, setTitleAndDescription, - addButtonToToolbar, } from '../../../../utils/demo/helpers'; import * as cornerstoneTools from '@cornerstonejs/tools'; -import { adaptersSEG } from '@cornerstonejs/adapters'; // This is for debugging purposes console.warn( 'Click on index.ts to open source code for this example --------->' ); -const { VTKjs } = adaptersSEG; - const { SegmentationDisplayTool, ToolGroupManager, @@ -36,10 +32,6 @@ const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id const segmentationId = 'MY_SEGMENTATION_ID'; const toolGroupId = 'MY_TOOLGROUP_ID'; -// Create the viewports -const viewportId1 = 'CT_AXIAL'; -const viewportId2 = 'CT_SAGITTAL'; -const viewportId3 = 'CT_CORONAL'; // ======== Set up page ======== // setTitleAndDescription( @@ -70,22 +62,8 @@ viewportGrid.appendChild(element2); viewportGrid.appendChild(element3); content.appendChild(viewportGrid); -let renderingEngine; -// ============================= // - -addButtonToToolbar({ - title: 'Create SEG', - onClick: async () => { - const viewport = renderingEngine.getViewport(viewportId1); - debugger; - const segBlob = VTKjs.Segmentation.generateSegmentation(); - - //Create a URL for the binary. - const objectUrl = URL.createObjectURL(segBlob); - window.open(objectUrl); - }, -}); +// ============================= // /** * Adds two concentric circles to each axial slice of the demo segmentation. @@ -185,7 +163,12 @@ async function run() { // Instantiate a rendering engine const renderingEngineId = 'myRenderingEngine'; - renderingEngine = new RenderingEngine(renderingEngineId); + const renderingEngine = new RenderingEngine(renderingEngineId); + + // Create the viewports + const viewportId1 = 'CT_AXIAL'; + const viewportId2 = 'CT_SAGITTAL'; + const viewportId3 = 'CT_CORONAL'; const viewportInputArray = [ { From 27b0c19b9c4b3f2d126f17a09cabf33726ed43ca Mon Sep 17 00:00:00 2001 From: Alireza Date: Tue, 18 Jul 2023 14:39:29 -0400 Subject: [PATCH 09/20] fix demo for export color --- package.json | 1 - .../examples/segmentationExport/index.ts | 52 +++---------------- .../adapters/Cornerstone/Segmentation_4X.js | 22 ++++++-- yarn.lock | 12 ----- 4 files changed, 25 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index e2ab9e58a7..398c4675bc 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "build": "npx lerna run build --stream", "build:esm": "npx lerna run build:esm --stream", "build:umd": "npx lerna run build:umd --stream", - "build:esm": "npx lerna run build:esm --stream", "watch": "npx lerna watch -- lerna run build --scope=$LERNA_PACKAGE_NAME --include-dependents", "build:update-api:core": "cd packages/core && npm run build:update-api", "build:update-api:tools": "cd packages/tools && npm run build:update-api", diff --git a/packages/adapters/examples/segmentationExport/index.ts b/packages/adapters/examples/segmentationExport/index.ts index cb12623fec..b4bc70bbdf 100644 --- a/packages/adapters/examples/segmentationExport/index.ts +++ b/packages/adapters/examples/segmentationExport/index.ts @@ -79,14 +79,9 @@ let segmentationVolume; let segmentationRepresentationUID; function generateMockMetadata(segmentIndex, color) { - // TODO -> Use colors from the cornerstoneTools LUT. const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB( - color.slice(0, 3) - ); - - const colorAgain = dcmjs.data.Colors.dicomlab2RGB( - RecommendedDisplayCIELabValue - ); + color.slice(0, 3).map(value => value / 255) + ).map(value => Math.round(value)); return { SegmentedPropertyCategoryCodeSequence: { @@ -94,8 +89,8 @@ function generateMockMetadata(segmentIndex, color) { CodingSchemeDesignator: "SRT", CodeMeaning: "Tissue" }, - SegmentNumber: (segmentIndex + 1).toString(), - SegmentLabel: "Tissue " + (segmentIndex + 1).toString(), + SegmentNumber: segmentIndex.toString(), + SegmentLabel: "Tissue " + segmentIndex.toString(), SegmentAlgorithmType: "SEMIAUTOMATIC", SegmentAlgorithmName: "Slicer Prototype", RecommendedDisplayCIELabValue, @@ -162,11 +157,10 @@ function createMockEllipsoidSegmentation( segmentationVolume, outerRadius = 20, innerRadius = 10, - center = "center", + center, labels = [1, 2] - // mode = "first" ) { - const { dimensions, scalarData, imageData } = segmentationVolume; + const { dimensions, scalarData } = segmentationVolume; const centerToUse = center === "center" @@ -175,9 +169,6 @@ function createMockEllipsoidSegmentation( let voxelIndex = 0; - // const zToUse = - // mode === "first" ? [0, 1] : [dimensions[2] - 1, dimensions[2]]; - for (let z = 0; z < dimensions[2]; z++) { for (let y = 0; y < dimensions[1]; y++) { for (let x = 0; x < dimensions[0]; x++) { @@ -193,8 +184,6 @@ function createMockEllipsoidSegmentation( } voxelIndex++; - // const index = imageData.computeOffsetIndex([x, y, z]); - // scalarData[index] = labels[1]; } } } @@ -210,13 +199,6 @@ async function addSegmentationsToState() { } ); - // segmentationVolume2 = await volumeLoader.createAndCacheDerivedVolume( - // volumeId, - // { - // volumeId: `${segmentationId}2` - // } - // ); - // Add the segmentations to state segmentation.addSegmentations([ { @@ -232,20 +214,6 @@ async function addSegmentationsToState() { } } ]); - // segmentation.addSegmentations([ - // { - // segmentationId: `${segmentationId}2`, - // representation: { - // // The type of segmentation - // type: csToolsEnums.SegmentationRepresentations.Labelmap, - // // The actual segmentation data, in the case of labelmap this is a - // // reference to the source volume of the segmentation. - // data: { - // volumeId: `${segmentationId}2` - // } - // } - // } - // ]); // Add some data to the segmentations // createMockEllipsoidSegmentation(segmentationVolume); @@ -254,7 +222,7 @@ async function addSegmentationsToState() { 40, 20, [250, 100, 125], - [2, 5] + [1, 2] ); } @@ -352,12 +320,6 @@ async function run() { type: csToolsEnums.SegmentationRepresentations.Labelmap } ]); - // await segmentation.addSegmentationRepresentations(toolGroupId, [ - // { - // segmentationId: `${segmentationId}2`, - // type: csToolsEnums.SegmentationRepresentations.Labelmap - // } - // ]); // Render the image renderingEngine.renderViewports([viewportId1, viewportId2, viewportId3]); diff --git a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js index cadfe731d2..649d6d0704 100644 --- a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js +++ b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js @@ -411,6 +411,17 @@ function generateToolState(imageIds, arrayBuffer, metadataProvider, options) { const labelmapBufferArray = []; labelmapBufferArray[0] = new ArrayBuffer(arrayBufferLength); + // Precompute the indices and metadata so that we don't have to call + // a function for each imageId in the for loop. + const imageIdMaps = imageIds.reduce( + (acc, curr, index) => { + acc.indices[curr] = index; + acc.metadata[curr] = metadataProvider.get("instance", curr); + return acc; + }, + { indices: {}, metadata: {} } + ); + insertFunction( segmentsOnFrame, segmentsOnFrameArray, @@ -422,7 +433,8 @@ function generateToolState(imageIds, arrayBuffer, metadataProvider, options) { metadataProvider, sopUIDImageIdIndexMap, tolerance, - TypedArrayConstructor + TypedArrayConstructor, + imageIdMaps ); return { @@ -1067,7 +1079,8 @@ function insertPixelDataPlanar( metadataProvider, sopUIDImageIdIndexMap, tolerance, - TypedArrayConstructor + TypedArrayConstructor, + imageIdMaps ) { const { SharedFunctionalGroupsSequence, @@ -1140,7 +1153,7 @@ function insertPixelDataPlanar( continue; } - const sourceImageMetadata = metadataProvider.get("instance", imageId); + const sourceImageMetadata = imageIdMaps.metadata[imageId]; if ( Rows !== sourceImageMetadata.Rows || Columns !== sourceImageMetadata.Columns @@ -1152,7 +1165,8 @@ function insertPixelDataPlanar( ); } - const imageIdIndex = imageIds.findIndex(element => element === imageId); + const imageIdIndex = imageIdMaps.indices[imageId]; + const byteOffset = sliceLength * imageIdIndex * diff --git a/yarn.lock b/yarn.lock index ff17cdd4f6..bb6b4a2ebb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8211,18 +8211,6 @@ dateformat@^3.0.0: resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -dcmjs@^0.29.6: - version "0.29.6" - resolved "https://registry.npmjs.org/dcmjs/-/dcmjs-0.29.6.tgz#6ca1543e74bae29657d1f7f2273407e23266c011" - integrity sha512-xMULkeFpxFNV8JZwWMOW3j1uL3iAe8r3pytlyWnlTxYc/OZ9KSu4wmQMVMBAgmY0GYpeWlji7GMRArVh8ISKhA== - dependencies: - "@babel/runtime-corejs2" "^7.17.8" - gl-matrix "^3.1.0" - lodash.clonedeep "^4.5.0" - loglevelnext "^3.0.1" - ndarray "^1.0.19" - pako "^2.0.4" - dcmjs@^0.29.8: version "0.29.8" resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.29.8.tgz#3daa5224f8e75b2e5b069590bef26a272741fa44" From 8917dd09ba03e6f6a9f80e30a8d6410d3119dc0e Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 19 Jul 2023 14:38:16 -0400 Subject: [PATCH 10/20] add centroid calculations --- .../adapters/Cornerstone/Segmentation_4X.js | 103 +++++++++++++++--- 1 file changed, 86 insertions(+), 17 deletions(-) diff --git a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js index 649d6d0704..bf5cd5301d 100644 --- a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js +++ b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js @@ -369,9 +369,9 @@ function generateToolState(imageIds, arrayBuffer, metadataProvider, options) { imageIds, validOrientations, metadataProvider, - sopUIDImageIdIndexMap, tolerance, - TypedArrayConstructor + TypedArrayConstructor, + sopUIDImageIdIndexMap ); } @@ -422,6 +422,12 @@ function generateToolState(imageIds, arrayBuffer, metadataProvider, options) { { indices: {}, metadata: {} } ); + // This is the centroid calculation for each segment Index, the data structure + // is a Map with key = segmentIndex and value = {imageIdIndex: centroid, ...} + // later on we will use this data structure to calculate the centroid of the + // segment in the labelmapBuffer + const segmentsPixelIndices = new Map(); + insertFunction( segmentsOnFrame, segmentsOnFrameArray, @@ -431,17 +437,35 @@ function generateToolState(imageIds, arrayBuffer, metadataProvider, options) { imageIds, validOrientations, metadataProvider, - sopUIDImageIdIndexMap, tolerance, TypedArrayConstructor, + segmentsPixelIndices, + sopUIDImageIdIndexMap, imageIdMaps ); + // calculate the centroid of each segment + const centroidXYZ = new Map(); + + segmentsPixelIndices.forEach((imageIdIndexBufferIndex, segmentIndex) => { + const { xAcc, yAcc, zAcc, count } = calculateCentroid( + imageIdIndexBufferIndex, + multiframe + ); + + centroidXYZ.set(segmentIndex, { + x: Math.floor(xAcc / count), + y: Math.floor(yAcc / count), + z: Math.floor(zAcc / count) + }); + }); + return { labelmapBufferArray, segMetadata, segmentsOnFrame, - segmentsOnFrameArray + segmentsOnFrameArray, + centroids: centroidXYZ }; } @@ -632,8 +656,8 @@ function findReferenceSourceImageId( frameSegment, imageIds, metadataProvider, - sopUIDImageIdIndexMap, - tolerance + tolerance, + sopUIDImageIdIndexMap ) { let imageId = undefined; @@ -727,9 +751,9 @@ function checkSEGsOverlapping( imageIds, validOrientations, metadataProvider, - sopUIDImageIdIndexMap, tolerance, - TypedArrayConstructor + TypedArrayConstructor, + sopUIDImageIdIndexMap ) { const { SharedFunctionalGroupsSequence, @@ -775,8 +799,8 @@ function checkSEGsOverlapping( frameSegment, imageIds, metadataProvider, - sopUIDImageIdIndexMap, - tolerance + tolerance, + sopUIDImageIdIndexMap ); if (!imageId) { @@ -861,9 +885,10 @@ function insertOverlappingPixelDataPlanar( imageIds, validOrientations, metadataProvider, - sopUIDImageIdIndexMap, tolerance, - TypedArrayConstructor + TypedArrayConstructor, + segmentsPixelIndices, + sopUIDImageIdIndexMap ) { const { SharedFunctionalGroupsSequence, @@ -959,8 +984,8 @@ function insertOverlappingPixelDataPlanar( i, imageIds, metadataProvider, - sopUIDImageIdIndexMap, - tolerance + tolerance, + sopUIDImageIdIndexMap ); if (!imageId) { @@ -1077,9 +1102,10 @@ function insertPixelDataPlanar( imageIds, validOrientations, metadataProvider, - sopUIDImageIdIndexMap, tolerance, TypedArrayConstructor, + segmentsPixelIndices, + sopUIDImageIdIndexMap, imageIdMaps ) { const { @@ -1137,13 +1163,17 @@ function insertPixelDataPlanar( ); } + if (segmentsPixelIndices.get(segmentIndex) === undefined) { + segmentsPixelIndices.set(segmentIndex, []); + } + const imageId = findReferenceSourceImageId( multiframe, i, imageIds, metadataProvider, - sopUIDImageIdIndexMap, - tolerance + tolerance, + sopUIDImageIdIndexMap ); if (!imageId) { @@ -1167,6 +1197,10 @@ function insertPixelDataPlanar( const imageIdIndex = imageIdMaps.indices[imageId]; + if (segmentsPixelIndices.get(segmentIndex).length === 0) { + segmentsPixelIndices.set(segmentIndex, {}); + } + const byteOffset = sliceLength * imageIdIndex * @@ -1179,11 +1213,14 @@ function insertPixelDataPlanar( ); const data = alignedPixelDataI.data; + + const indexCache = []; for (let j = 0, len = alignedPixelDataI.data.length; j < len; ++j) { if (data[j]) { for (let x = j; x < len; ++x) { if (data[x]) { labelmap2DView[x] = segmentIndex; + indexCache.push(x); } } @@ -1196,6 +1233,8 @@ function insertPixelDataPlanar( break; } } + + segmentsPixelIndices.get(segmentIndex)[imageIdIndex] = indexCache; } } @@ -1641,6 +1680,36 @@ function getUnpackedOffsetAndLength(chunks, offset, length) { }; } +function calculateCentroid(imageIdIndexBufferIndex, multiframe) { + let xAcc = 0; + let yAcc = 0; + let zAcc = 0; + let count = 0; + + for (const [imageIdIndex, bufferIndices] of Object.entries( + imageIdIndexBufferIndex + )) { + const z = Number(imageIdIndex); + + if (!bufferIndices || bufferIndices.length === 0) { + continue; + } + + for (const bufferIndex of bufferIndices) { + const y = Math.floor(bufferIndex / multiframe.Rows); + const x = bufferIndex % multiframe.Rows; + + xAcc += x; + yAcc += y; + zAcc += z; + + count++; + } + } + + return { xAcc, yAcc, zAcc, count }; +} + const Segmentation = { generateSegmentation, generateToolState, From 2f6655ff54a0d0afa02014c0308e44c2d578ae1b Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 19 Jul 2023 17:09:49 -0400 Subject: [PATCH 11/20] add trigger event for segmentation load progress --- .../adapters/Cornerstone/Segmentation_4X.js | 238 +++++++++++------- .../Segmentation/generateSegmentation.ts | 2 +- .../adapters/src/adapters/enums/Events.ts | 11 + packages/adapters/src/adapters/enums/index.ts | 3 + packages/adapters/src/adapters/index.ts | 3 +- packages/adapters/src/index.ts | 4 +- 6 files changed, 162 insertions(+), 99 deletions(-) create mode 100644 packages/adapters/src/adapters/enums/Events.ts create mode 100644 packages/adapters/src/adapters/enums/index.ts diff --git a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js index bf5cd5301d..a7d795f855 100644 --- a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js +++ b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js @@ -9,6 +9,8 @@ import ndarray from "ndarray"; import cloneDeep from "lodash.clonedeep"; import { Buffer } from "buffer"; +import { Events } from "../enums"; + const { rotateDirectionCosinesInPlane, flipImageOrientationPatient: flipIOP, @@ -266,11 +268,18 @@ function _createSegFromImages(images, isMultiframe, options) { * @return {[][][]} 3D list containing the track of segments per frame for each labelMap * (available only for the overlapping case). */ -function generateToolState(imageIds, arrayBuffer, metadataProvider, options) { +async function generateToolState( + imageIds, + arrayBuffer, + metadataProvider, + options +) { const { skipOverlapping = false, tolerance = 1e-3, - TypedArrayConstructor = Uint8Array + TypedArrayConstructor = Uint8Array, + eventTarget, + triggerEvent } = options; const dicomData = DicomMessage.readFile(arrayBuffer); const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); @@ -428,7 +437,7 @@ function generateToolState(imageIds, arrayBuffer, metadataProvider, options) { // segment in the labelmapBuffer const segmentsPixelIndices = new Map(); - insertFunction( + await insertFunction( segmentsOnFrame, segmentsOnFrameArray, labelmapBufferArray, @@ -441,7 +450,9 @@ function generateToolState(imageIds, arrayBuffer, metadataProvider, options) { TypedArrayConstructor, segmentsPixelIndices, sopUIDImageIdIndexMap, - imageIdMaps + imageIdMaps, + eventTarget, + triggerEvent ); // calculate the centroid of each segment @@ -1106,7 +1117,9 @@ function insertPixelDataPlanar( TypedArrayConstructor, segmentsPixelIndices, sopUIDImageIdIndexMap, - imageIdMaps + imageIdMaps, + eventTarget, + triggerEvent ) { const { SharedFunctionalGroupsSequence, @@ -1122,120 +1135,155 @@ function insertPixelDataPlanar( : undefined; const sliceLength = Columns * Rows; - for ( - let i = 0, groupsLen = PerFrameFunctionalGroupsSequence.length; - i < groupsLen; - ++i - ) { - const PerFrameFunctionalGroups = PerFrameFunctionalGroupsSequence[i]; + let i = 0; + const groupsLen = PerFrameFunctionalGroupsSequence.length; + const chunkSize = Math.ceil(groupsLen / 10); // 10% of total length + + const shouldTriggerEvent = triggerEvent && eventTarget; + + // Below, we chunk the processing of the frames to avoid blocking the main thread + // if the segmentation is large. We also use a promise to allow the caller to + // wait for the processing to finish. + return new Promise(resolve => { + function processInChunks() { + // process one chunk + for (let end = Math.min(i + chunkSize, groupsLen); i < end; ++i) { + const PerFrameFunctionalGroups = + PerFrameFunctionalGroupsSequence[i]; + + const ImageOrientationPatientI = + sharedImageOrientationPatient || + PerFrameFunctionalGroups.PlaneOrientationSequence + .ImageOrientationPatient; + + const view = readFromUnpackedChunks( + pixelData, + i * sliceLength, + sliceLength + ); - const ImageOrientationPatientI = - sharedImageOrientationPatient || - PerFrameFunctionalGroups.PlaneOrientationSequence - .ImageOrientationPatient; + const pixelDataI2D = ndarray(view, [Rows, Columns]); - const view = readFromUnpackedChunks( - pixelData, - i * sliceLength, - sliceLength - ); + const alignedPixelDataI = alignPixelDataWithSourceData( + pixelDataI2D, + ImageOrientationPatientI, + validOrientations, + tolerance + ); - const pixelDataI2D = ndarray(view, [Rows, Columns]); + if (!alignedPixelDataI) { + throw new Error( + "Individual SEG frames are out of plane with respect to the first SEG frame. " + + "This is not yet supported. Aborting segmentation loading." + ); + } - const alignedPixelDataI = alignPixelDataWithSourceData( - pixelDataI2D, - ImageOrientationPatientI, - validOrientations, - tolerance - ); + const segmentIndex = getSegmentIndex(multiframe, i); - if (!alignedPixelDataI) { - throw new Error( - "Individual SEG frames are out of plane with respect to the first SEG frame. " + - "This is not yet supported. Aborting segmentation loading." - ); - } + if (segmentIndex === undefined) { + throw new Error( + "Could not retrieve the segment index. Aborting segmentation loading." + ); + } - const segmentIndex = getSegmentIndex(multiframe, i); - if (segmentIndex === undefined) { - throw new Error( - "Could not retrieve the segment index. Aborting segmentation loading." - ); - } + if (!segmentsPixelIndices.has(segmentIndex)) { + segmentsPixelIndices.set(segmentIndex, {}); + } - if (segmentsPixelIndices.get(segmentIndex) === undefined) { - segmentsPixelIndices.set(segmentIndex, []); - } + const imageId = findReferenceSourceImageId( + multiframe, + i, + imageIds, + metadataProvider, + tolerance, + sopUIDImageIdIndexMap + ); - const imageId = findReferenceSourceImageId( - multiframe, - i, - imageIds, - metadataProvider, - tolerance, - sopUIDImageIdIndexMap - ); + if (!imageId) { + console.warn( + "Image not present in stack, can't import frame : " + + i + + "." + ); + continue; + } - if (!imageId) { - console.warn( - "Image not present in stack, can't import frame : " + i + "." - ); - continue; - } + const sourceImageMetadata = imageIdMaps.metadata[imageId]; + if ( + Rows !== sourceImageMetadata.Rows || + Columns !== sourceImageMetadata.Columns + ) { + throw new Error( + "Individual SEG frames have different geometry dimensions (Rows and Columns) " + + "respect to the source image reference frame. This is not yet supported. " + + "Aborting segmentation loading. " + ); + } - const sourceImageMetadata = imageIdMaps.metadata[imageId]; - if ( - Rows !== sourceImageMetadata.Rows || - Columns !== sourceImageMetadata.Columns - ) { - throw new Error( - "Individual SEG frames have different geometry dimensions (Rows and Columns) " + - "respect to the source image reference frame. This is not yet supported. " + - "Aborting segmentation loading. " - ); - } + const imageIdIndex = imageIdMaps.indices[imageId]; - const imageIdIndex = imageIdMaps.indices[imageId]; + const byteOffset = + sliceLength * + imageIdIndex * + TypedArrayConstructor.BYTES_PER_ELEMENT; - if (segmentsPixelIndices.get(segmentIndex).length === 0) { - segmentsPixelIndices.set(segmentIndex, {}); - } + const labelmap2DView = new TypedArrayConstructor( + labelmapBufferArray[0], + byteOffset, + sliceLength + ); - const byteOffset = - sliceLength * - imageIdIndex * - TypedArrayConstructor.BYTES_PER_ELEMENT; + const data = alignedPixelDataI.data; + + const indexCache = []; + for ( + let j = 0, len = alignedPixelDataI.data.length; + j < len; + ++j + ) { + if (data[j]) { + for (let x = j; x < len; ++x) { + if (data[x]) { + labelmap2DView[x] = segmentIndex; + indexCache.push(x); + } + } - const labelmap2DView = new TypedArrayConstructor( - labelmapBufferArray[0], - byteOffset, - sliceLength - ); + if (!segmentsOnFrame[imageIdIndex]) { + segmentsOnFrame[imageIdIndex] = []; + } - const data = alignedPixelDataI.data; + segmentsOnFrame[imageIdIndex].push(segmentIndex); - const indexCache = []; - for (let j = 0, len = alignedPixelDataI.data.length; j < len; ++j) { - if (data[j]) { - for (let x = j; x < len; ++x) { - if (data[x]) { - labelmap2DView[x] = segmentIndex; - indexCache.push(x); + break; } } - if (!segmentsOnFrame[imageIdIndex]) { - segmentsOnFrame[imageIdIndex] = []; - } + const segmentIndexObject = + segmentsPixelIndices.get(segmentIndex); + segmentIndexObject[imageIdIndex] = indexCache; + segmentsPixelIndices.set(segmentIndex, segmentIndexObject); + } - segmentsOnFrame[imageIdIndex].push(segmentIndex); + // trigger an event after each chunk + if (shouldTriggerEvent) { + const percentComplete = Math.round((i / groupsLen) * 100); + triggerEvent(eventTarget, Events.SEGMENTATION_LOAD_PROGRESS, { + percentComplete + }); + } - break; + // schedule next chunk + if (i < groupsLen) { + setTimeout(processInChunks, 0); + } else { + // resolve the Promise when all chunks have been processed + resolve(); } } - segmentsPixelIndices.get(segmentIndex)[imageIdIndex] = indexCache; - } + processInChunks(); + }); } function checkOrientation( diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts index 3272102144..c5fbc2c8bd 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts @@ -1,4 +1,4 @@ -import { , normalizers, derivations } from "dcmjs"; +import { normalizers, derivations } from "dcmjs"; import { fillSegmentation } from "../../Cornerstone/Segmentation_4X"; const { Normalizer } = normalizers; diff --git a/packages/adapters/src/adapters/enums/Events.ts b/packages/adapters/src/adapters/enums/Events.ts new file mode 100644 index 0000000000..82d86437e2 --- /dev/null +++ b/packages/adapters/src/adapters/enums/Events.ts @@ -0,0 +1,11 @@ +/** + * Cornerstone adapters events + */ +enum Events { + /** + * Cornerstone segmentation load progress event + */ + SEGMENTATION_LOAD_PROGRESS = "CORNERSTONE_ADAPTER_SEGMENTATION_LOAD_PROGRESS" +} + +export default Events; diff --git a/packages/adapters/src/adapters/enums/index.ts b/packages/adapters/src/adapters/enums/index.ts new file mode 100644 index 0000000000..f5b115b416 --- /dev/null +++ b/packages/adapters/src/adapters/enums/index.ts @@ -0,0 +1,3 @@ +import Events from "./Events"; + +export { Events }; diff --git a/packages/adapters/src/adapters/index.ts b/packages/adapters/src/adapters/index.ts index 8e68ab2ada..c497cc706f 100644 --- a/packages/adapters/src/adapters/index.ts +++ b/packages/adapters/src/adapters/index.ts @@ -1,6 +1,7 @@ import { CornerstoneSR } from "./Cornerstone"; import { Cornerstone3DSR, Cornerstone3DSEG } from "./Cornerstone3D"; import { VTKjsSEG } from "./VTKjs"; +import * as Enums from "./enums"; const adaptersSR = { Cornerstone: CornerstoneSR, @@ -13,4 +14,4 @@ const adaptersSEG = { VTKjs: VTKjsSEG }; -export { adaptersSR, adaptersSEG }; +export { adaptersSR, adaptersSEG, Enums }; diff --git a/packages/adapters/src/index.ts b/packages/adapters/src/index.ts index 7b553b61e2..a836329134 100644 --- a/packages/adapters/src/index.ts +++ b/packages/adapters/src/index.ts @@ -1,3 +1,3 @@ -import { adaptersSR, adaptersSEG } from "./adapters"; +import { adaptersSR, adaptersSEG, Enums } from "./adapters"; -export { adaptersSR, adaptersSEG }; +export { adaptersSR, adaptersSEG, Enums }; From 936d9ac8d989fd6feda81bc21e86ad1f2055472c Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 19 Jul 2023 17:10:13 -0400 Subject: [PATCH 12/20] update dcmjs --- packages/docs/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs/package.json b/packages/docs/package.json index 1cdfe5162a..e72a3314f3 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -40,7 +40,7 @@ "@mdx-js/react": "^1.6.21", "@svgr/webpack": "^6.2.1", "clsx": "^1.1.1", - "dcmjs": "^0.29.6", + "dcmjs": "^0.29.8", "detect-gpu": "^5.0.22", "dicom-parser": "^1.8.11", "dicomweb-client": "^0.8.4", From c153d0af3c3204920dbc199bb0b6dff84140b2e2 Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 19 Jul 2023 17:11:48 -0400 Subject: [PATCH 13/20] update api --- common/reviews/api/tools.api.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 46b1c30478..d635e6f31d 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -2010,6 +2010,9 @@ function getActiveSegmentationRepresentation(toolGroupId: string): ToolGroupSpec // @public (undocumented) function getActiveSegmentIndex(segmentationId: string): number | undefined; +// @public (undocumented) +function getAllSegmentationRepresentations(): Record; + // @public (undocumented) function getAllSynchronizers(): Array; @@ -4865,6 +4868,7 @@ declare namespace state_3 { getSegmentSpecificRepresentationConfig, setSegmentSpecificRepresentationConfig, getToolGroupIdsWithSegmentation, + getAllSegmentationRepresentations, getSegmentationRepresentationByUID, addColorLUT, getColorLUT, From b6531dbf8f5665d9b3fdfcea6b1b15673567600f Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 20 Jul 2023 11:37:16 -0400 Subject: [PATCH 14/20] fix example --- utils/ExampleRunner/template-config.js | 2 ++ utils/ExampleRunner/template-multiexample-config.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/utils/ExampleRunner/template-config.js b/utils/ExampleRunner/template-config.js index f0c066ce36..c4baf9d01c 100644 --- a/utils/ExampleRunner/template-config.js +++ b/utils/ExampleRunner/template-config.js @@ -2,6 +2,7 @@ const path = require('path'); const csRenderBasePath = path.resolve('packages/core/src/index'); const csToolsBasePath = path.resolve('packages/tools/src/index'); +const csAdapters = path.resolve('packages/adapters/src/index'); const csStreamingBasePath = path.resolve( 'packages/streaming-image-volume-loader/src/index' ); @@ -68,6 +69,7 @@ module.exports = { alias: { '@cornerstonejs/core': '${csRenderBasePath.replace(/\\/g, '/')}', '@cornerstonejs/tools': '${csToolsBasePath.replace(/\\/g, '/')}', + '@cornerstonejs/adapters': '${csAdapters.replace(/\\/g, '/')}', '@cornerstonejs/streaming-image-volume-loader': '${csStreamingBasePath.replace( /\\/g, '//' diff --git a/utils/ExampleRunner/template-multiexample-config.js b/utils/ExampleRunner/template-multiexample-config.js index 29a7d3c309..2b162b14fa 100644 --- a/utils/ExampleRunner/template-multiexample-config.js +++ b/utils/ExampleRunner/template-multiexample-config.js @@ -5,6 +5,7 @@ const path = require('path'); // etc... const csRenderBasePath = path.resolve('./packages/core/src/index'); const csToolsBasePath = path.resolve('./packages/tools/src/index'); +const csAdaptersBasePath = path.resolve('./packages/adapters/src/index'); const csStreamingBasePath = path.resolve( './packages/streaming-image-volume-loader/src/index' ); @@ -97,6 +98,7 @@ module.exports = { alias: { '@cornerstonejs/core': '${csRenderBasePath.replace(/\\/g, '/')}', '@cornerstonejs/tools': '${csToolsBasePath.replace(/\\/g, '/')}', + '@cornerstonejs/adapters': '${csAdaptersBasePath.replace(/\\/g, '/')}', '@cornerstonejs/streaming-image-volume-loader': '${csStreamingBasePath.replace( /\\/g, '/' From 5e9d513a424f055a3826ff61daee4824e0b1e51e Mon Sep 17 00:00:00 2001 From: Alireza Date: Tue, 25 Jul 2023 11:50:17 -0400 Subject: [PATCH 15/20] remove the promise await --- .../examples/segmentationExport/index.ts | 58 ++++++++---------- .../src/BaseStreamingImageVolume.ts | 59 +++++++++++++++---- 2 files changed, 73 insertions(+), 44 deletions(-) diff --git a/packages/adapters/examples/segmentationExport/index.ts b/packages/adapters/examples/segmentationExport/index.ts index b4bc70bbdf..c44896a1f8 100644 --- a/packages/adapters/examples/segmentationExport/index.ts +++ b/packages/adapters/examples/segmentationExport/index.ts @@ -108,45 +108,37 @@ addButtonToToolbar({ title: "Create SEG", onClick: async () => { const volume = cache.getVolume(volumeId); + const segUID = segmentationRepresentationUID[0]; - const volumeImages = volume.convertToCornerstoneImages(); - const imagePromises = volumeImages.map(image => image.promise); + const images = volume.getCornerstoneImages(); - await Promise.all(imagePromises).then(images => { - const labelmap3D = - Cornerstone3D.Segmentation.generateLabelMaps2DFrom3D( - segmentationVolume - ); - - const segUID = segmentationRepresentationUID[0]; - - labelmap3D.metadata = []; - // labelmap3D.labelmaps2D.forEach(labelmap2D => { - labelmap3D.segmentsOnLabelmap.forEach(segmentIndex => { - const color = segmentation.config.color.getColorForSegmentIndex( - toolGroupId, - segUID, - segmentIndex - ); - - console.debug("color", segmentIndex, color); - const segmentMetadata = generateMockMetadata( - segmentIndex, - color - ); - labelmap3D.metadata[segmentIndex] = segmentMetadata; - }); + const labelmapObj = + Cornerstone3D.Segmentation.generateLabelMaps2DFrom3D( + segmentationVolume + ); - const segBlob = Cornerstone3D.Segmentation.generateSegmentation( - images, - labelmap3D, - metaData + // Generate fake metadata as an example + labelmapObj.metadata = []; + labelmapObj.segmentsOnLabelmap.forEach(segmentIndex => { + const color = segmentation.config.color.getColorForSegmentIndex( + toolGroupId, + segUID, + segmentIndex ); - //Create a URL for the binary. - const objectUrl = URL.createObjectURL(segBlob); - window.open(objectUrl); + const segmentMetadata = generateMockMetadata(segmentIndex, color); + labelmapObj.metadata[segmentIndex] = segmentMetadata; }); + + const segBlob = Cornerstone3D.Segmentation.generateSegmentation( + images, + labelmapObj, + metaData + ); + + //Create a URL for the binary. + const objectUrl = URL.createObjectURL(segBlob); + window.open(objectUrl); } }); diff --git a/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts b/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts index 158b6ce58f..d3d1dfd5c3 100644 --- a/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts +++ b/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts @@ -786,13 +786,12 @@ export default class BaseStreamingImageVolume extends ImageVolume { * * @param imageId - the imageId of the image to be converted * @param imageIdIndex - the index of the imageId in the imageIds array - * @returns imageLoadObject containing the promise that resolves - * to the cornerstone image + * @returns image object containing the pixel data, metadata, and other information */ - public convertToCornerstoneImage( + public getCornerstoneImage( imageId: string, imageIdIndex: number - ): Types.IImageLoadObject { + ): Types.IImage { const { imageIds } = this; const frameIndex = this._imageIdIndexToFrameIndex(imageIdIndex); @@ -854,7 +853,7 @@ export default class BaseStreamingImageVolume extends ImageVolume { ? modalityLutModule.rescaleIntercept : 0; - const image: Types.IImage = { + return { imageId, intercept, windowCenter, @@ -880,8 +879,42 @@ export default class BaseStreamingImageVolume extends ImageVolume { rowPixelSpacing: spacing[1], invert, }; + } + + /** + * Converts the requested imageId inside the volume to a cornerstoneImage + * object. It uses the typedArray set method to copy the pixelData from the + * correct offset in the scalarData to a new array for the image + * Duplicate of getCornerstoneImageLoadObject for legacy reasons + * + * @param imageId - the imageId of the image to be converted + * @param imageIdIndex - the index of the imageId in the imageIds array + * @returns imageLoadObject containing the promise that resolves + * to the cornerstone image + */ + public convertToCornerstoneImage( + imageId: string, + imageIdIndex: number + ): Types.IImageLoadObject { + return this.getCornerstoneImageLoadObject(imageId, imageIdIndex); + } + + /** + * Converts the requested imageId inside the volume to a cornerstoneImage + * object. It uses the typedArray set method to copy the pixelData from the + * correct offset in the scalarData to a new array for the image + * + * @param imageId - the imageId of the image to be converted + * @param imageIdIndex - the index of the imageId in the imageIds array + * @returns imageLoadObject containing the promise that resolves + * to the cornerstone image + */ + public getCornerstoneImageLoadObject( + imageId: string, + imageIdIndex: number + ): Types.IImageLoadObject { + const image = this.getCornerstoneImage(imageId, imageIdIndex); - // 5. Create the imageLoadObject const imageLoadObject = { promise: Promise.resolve(image), }; @@ -889,14 +922,18 @@ export default class BaseStreamingImageVolume extends ImageVolume { return imageLoadObject; } - public convertToCornerstoneImages(): Types.IImageLoadObject[] { + /** + * Returns an array of all the volume's images as Cornerstone images. + * It iterates over all the imageIds and converts them to Cornerstone images. + * + * @returns An array of Cornerstone images. + */ + public getCornerstoneImages(): Types.IImage[] { const { imageIds } = this; - const imageLoadObjects = imageIds.map((imageId, imageIdIndex) => { - return this.convertToCornerstoneImage(imageId, imageIdIndex); + return imageIds.map((imageId, imageIdIndex) => { + return this.getCornerstoneImage(imageId, imageIdIndex); }); - - return imageLoadObjects; } /** From 9d59ed9772cc34ac0553e2338f757256c62455f1 Mon Sep 17 00:00:00 2001 From: Alireza Date: Tue, 25 Jul 2023 13:14:57 -0400 Subject: [PATCH 16/20] apply review comments --- .../adapters/Cornerstone/Segmentation_4X.js | 17 ++++++++-- .../Segmentation/generateLabelMaps2DFrom3D.ts | 22 +++++++++++-- .../Segmentation/generateSegmentation.ts | 14 +++++---- .../src/adapters/helpers/codeMeaningEquals.ts | 11 +++++-- .../src/adapters/helpers/graphicTypeEquals.ts | 5 +++ .../adapters/src/adapters/helpers/toArray.ts | 4 +-- packages/adapters/src/adapters/index.ts | 3 +- packages/adapters/src/index.ts | 4 +-- ...tDatasetModule.ts => getInstanceModule.ts} | 31 +++++++++++++------ .../wadors/metaData/metaDataProvider.ts | 26 ++++------------ .../wadouri/metaData/metaDataProvider.ts | 26 ++++------------ 11 files changed, 94 insertions(+), 69 deletions(-) rename packages/dicomImageLoader/src/imageLoader/{getDatasetModule.ts => getInstanceModule.ts} (64%) diff --git a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js index a7d795f855..bf030d8c25 100644 --- a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js +++ b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js @@ -278,6 +278,7 @@ async function generateToolState( skipOverlapping = false, tolerance = 1e-3, TypedArrayConstructor = Uint8Array, + maxBytesPerChunk = 199000000, eventTarget, triggerEvent } = options; @@ -344,7 +345,7 @@ async function generateToolState( // Todo: need to test this with rle data pixelDataChunks = [pixelData]; } else { - pixelDataChunks = unpackPixelData(multiframe); + pixelDataChunks = unpackPixelData(multiframe, { maxBytesPerChunk }); if (!pixelDataChunks) { throw new Error("Fractional segmentations are not yet supported"); @@ -1358,9 +1359,10 @@ function checkIfPerpendicular(iop1, iop2, tolerance) { * unpackPixelData - Unpacks bit packed pixelData if the Segmentation is BINARY. * * @param {Object} multiframe The multiframe dataset. + * @param {Object} options Options for the unpacking. * @return {Uint8Array} The unpacked pixelData. */ -function unpackPixelData(multiframe) { +function unpackPixelData(multiframe, options) { const segType = multiframe.SegmentationType; let data; @@ -1378,7 +1380,7 @@ function unpackPixelData(multiframe) { // For extreme big data, we can't unpack the data at once and we need to // chunk it and unpack each chunk separately. // MAX 2GB is the limit right now to allocate a buffer - return getUnpackedChunks(data, 199000000); + return getUnpackedChunks(data, options.maxBytesPerChunk); } const pixelData = new Uint8Array(data); @@ -1661,6 +1663,15 @@ function getSegmentMetadata(multiframe, seriesInstanceUid) { }; } +/** + * Reads a range of bytes from an array of ArrayBuffer chunks and + * aggregate them into a new Uint8Array. + * + * @param {ArrayBuffer[]} chunks - An array of ArrayBuffer chunks. + * @param {number} offset - The offset of the first byte to read. + * @param {number} length - The number of bytes to read. + * @returns {Uint8Array} A new Uint8Array containing the requested bytes. + */ function readFromUnpackedChunks(chunks, offset, length) { const mapping = getUnpackedOffsetAndLength(chunks, offset, length); diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateLabelMaps2DFrom3D.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateLabelMaps2DFrom3D.ts index 1e76d4a60d..7423409491 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateLabelMaps2DFrom3D.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateLabelMaps2DFrom3D.ts @@ -1,13 +1,29 @@ -function generateLabelMaps2DFrom3D(labelmap3D) { +/** + * Generates 2D label maps from a 3D label map. + * @param labelmap3D - The 3D label map object to generate 2D label maps from. It is derived + * from the volume labelmap. + * @returns The label map object containing the 2D label maps and segments on label maps. + */ +function generateLabelMaps2DFrom3D(labelmap3D): { + scalarData: number[]; + dimensions: number[]; + segmentsOnLabelmap: number[]; + labelmaps2D: { + segmentsOnLabelmap: number[]; + pixelData: number[]; + rows: number; + columns: number; + }[]; +} { // 1. we need to generate labelmaps2D from labelmaps3D, a labelmap2D is for each // slice const { scalarData, dimensions } = labelmap3D; // scalarData is a flat array of all the pixels in the volume. - const labelmaps2D = []; const segmentsOnLabelmap3D = new Set(); + // X-Y are the row and column dimensions, Z is the number of slices. for (let z = 0; z < dimensions[2]; z++) { const pixelData = scalarData.slice( z * dimensions[0] * dimensions[1], @@ -38,7 +54,7 @@ function generateLabelMaps2DFrom3D(labelmap3D) { segmentsOnLabelmap3D.add(segmentIndex); }); - labelmaps2D[dimensions[2] - z] = labelmap2D; + labelmaps2D[dimensions[2] - 1 - z] = labelmap2D; } // remove segment 0 from segmentsOnLabelmap3D diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts index c5fbc2c8bd..62ae07fb14 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts @@ -22,7 +22,10 @@ function generateSegmentation(images, labelmaps, metadata, options) { /** * _createMultiframeSegmentationFromReferencedImages - description * - * @param images - An array of the cornerstone image objects. + * @param images - An array of the cornerstone image objects related to the reference + * series that the segmentation is derived from. You can use methods such as + * volume.getCornerstoneImages() to get this array. + * * @param options - the options object for the SegmentationDerivation. * @returns The Seg derived dataSet. */ @@ -33,14 +36,13 @@ function _createMultiframeSegmentationFromReferencedImages( ) { const datasets = images.map(image => { // add the sopClassUID to the dataset - const allMetadataModules = metadata.get("dataset", image.imageId); - + const instance = metadata.get("instance", image.imageId); return { ...image, - ...allMetadataModules, + ...instance, // Todo: move to dcmjs tag style - SOPClassUID: allMetadataModules.SopClassUID, - SOPInstanceUID: allMetadataModules.SopInstanceUID, + SOPClassUID: instance.SopClassUID, + SOPInstanceUID: instance.SopInstanceUID, PixelData: image.getPixelData(), _vrMap: { PixelData: "OW" diff --git a/packages/adapters/src/adapters/helpers/codeMeaningEquals.ts b/packages/adapters/src/adapters/helpers/codeMeaningEquals.ts index c7dfaaffad..61c8bcf36c 100644 --- a/packages/adapters/src/adapters/helpers/codeMeaningEquals.ts +++ b/packages/adapters/src/adapters/helpers/codeMeaningEquals.ts @@ -1,5 +1,12 @@ -const codeMeaningEquals = codeMeaningName => { - return contentItem => { +/** + * Returns a function that checks if a given content item's ConceptNameCodeSequence.CodeMeaning + * matches the provided codeMeaningName. + * @param codeMeaningName - The CodeMeaning to match against. + * @returns A function that takes a content item and returns a boolean indicating whether the + * content item's CodeMeaning matches the provided codeMeaningName. + */ +const codeMeaningEquals = (codeMeaningName: string) => { + return (contentItem: any) => { return ( contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName ); diff --git a/packages/adapters/src/adapters/helpers/graphicTypeEquals.ts b/packages/adapters/src/adapters/helpers/graphicTypeEquals.ts index f07a0a2953..1ec2597b99 100644 --- a/packages/adapters/src/adapters/helpers/graphicTypeEquals.ts +++ b/packages/adapters/src/adapters/helpers/graphicTypeEquals.ts @@ -1,3 +1,8 @@ +/** + * Checks if a given content item's GraphicType property matches a specified value. + * @param {string} graphicType - The value to compare the content item's GraphicType property to. + * @returns {function} A function that takes a content item and returns a boolean indicating whether its GraphicType property matches the specified value. + */ const graphicTypeEquals = graphicType => { return contentItem => { return contentItem && contentItem.GraphicType === graphicType; diff --git a/packages/adapters/src/adapters/helpers/toArray.ts b/packages/adapters/src/adapters/helpers/toArray.ts index a9233773bd..e91a900928 100644 --- a/packages/adapters/src/adapters/helpers/toArray.ts +++ b/packages/adapters/src/adapters/helpers/toArray.ts @@ -1,5 +1,3 @@ -const toArray = function (x) { - return Array.isArray(x) ? x : [x]; -}; +const toArray = x => (Array.isArray(x) ? x : [x]); export { toArray }; diff --git a/packages/adapters/src/adapters/index.ts b/packages/adapters/src/adapters/index.ts index c497cc706f..a3e96f1d74 100644 --- a/packages/adapters/src/adapters/index.ts +++ b/packages/adapters/src/adapters/index.ts @@ -2,6 +2,7 @@ import { CornerstoneSR } from "./Cornerstone"; import { Cornerstone3DSR, Cornerstone3DSEG } from "./Cornerstone3D"; import { VTKjsSEG } from "./VTKjs"; import * as Enums from "./enums"; +import * as helpers from "./helpers"; const adaptersSR = { Cornerstone: CornerstoneSR, @@ -14,4 +15,4 @@ const adaptersSEG = { VTKjs: VTKjsSEG }; -export { adaptersSR, adaptersSEG, Enums }; +export { adaptersSR, adaptersSEG, Enums, helpers }; diff --git a/packages/adapters/src/index.ts b/packages/adapters/src/index.ts index a836329134..cbbaada2a1 100644 --- a/packages/adapters/src/index.ts +++ b/packages/adapters/src/index.ts @@ -1,3 +1,3 @@ -import { adaptersSR, adaptersSEG, Enums } from "./adapters"; +import { adaptersSR, adaptersSEG, Enums, helpers } from "./adapters"; -export { adaptersSR, adaptersSEG, Enums }; +export { adaptersSR, adaptersSEG, Enums, helpers }; diff --git a/packages/dicomImageLoader/src/imageLoader/getDatasetModule.ts b/packages/dicomImageLoader/src/imageLoader/getInstanceModule.ts similarity index 64% rename from packages/dicomImageLoader/src/imageLoader/getDatasetModule.ts rename to packages/dicomImageLoader/src/imageLoader/getInstanceModule.ts index 2522aec87b..b8d3663b6f 100644 --- a/packages/dicomImageLoader/src/imageLoader/getDatasetModule.ts +++ b/packages/dicomImageLoader/src/imageLoader/getInstanceModule.ts @@ -5,12 +5,12 @@ * @param types - An array of metadata types to retrieve. * @returns An object containing the retrieved metadata with capitalized keys. */ -function getDatasetModule( +function getInstanceModule( imageId: string, metaDataProvider: any, types: string[] ): object; -function getDatasetModule(imageId, metaDataProvider, types) { +function getInstanceModule(imageId, metaDataProvider, types) { const result = {}; for (const t of types) { try { @@ -34,11 +34,24 @@ function getDatasetModule(imageId, metaDataProvider, types) { return result; } -function capitalizeTag(tag) { - const parts = tag.split(/([0-9]+)/); - return parts - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(''); -} +const capitalizeTag = (tag: string) => + tag.charAt(0).toUpperCase() + tag.slice(1); + +const instanceModuleNames = [ + 'multiframeModule', + 'generalSeriesModule', + 'patientStudyModule', + 'imagePlaneModule', + 'nmMultiframeGeometryModule', + 'imagePixelModule', + 'modalityLutModule', + 'voiLutModule', + 'sopCommonModule', + 'petIsotopeModule', + 'overlayPlaneModule', + 'transferSyntax', + 'petSeriesModule', + 'petImageModule', +]; -export { getDatasetModule }; +export { getInstanceModule, instanceModuleNames }; diff --git a/packages/dicomImageLoader/src/imageLoader/wadors/metaData/metaDataProvider.ts b/packages/dicomImageLoader/src/imageLoader/wadors/metaData/metaDataProvider.ts index c4c8fdf845..419207f2fb 100644 --- a/packages/dicomImageLoader/src/imageLoader/wadors/metaData/metaDataProvider.ts +++ b/packages/dicomImageLoader/src/imageLoader/wadors/metaData/metaDataProvider.ts @@ -15,7 +15,10 @@ import { } from './extractPositioningFromMetadata'; import { getImageTypeSubItemFromMetadata } from './NMHelpers'; import isNMReconstructable from '../../isNMReconstructable'; -import { getDatasetModule } from '../../getDatasetModule'; +import { + getInstanceModule, + instanceModuleNames, +} from '../../getInstanceModule'; function metaDataProvider(type, imageId) { if (type === 'multiframeModule') { @@ -269,25 +272,8 @@ function metaDataProvider(type, imageId) { } // This is used for gathering all the metadata for export - if (type === 'dataset') { - const types = [ - 'multiframeModule', - 'generalSeriesModule', - 'patientStudyModule', - 'nmMultiframeGeometryModule', - 'imagePlaneModule', - 'imagePixelModule', - 'voiLutModule', - 'modalityLutModule', - 'sopCommonModule', - 'petIsotopeModule', - 'overlayPlaneModule', - 'transferSyntax', - 'petSeriesModule', - 'petImageModule', - ]; - - return getDatasetModule(imageId, metaDataProvider, types); + if (type === 'instance') { + return getInstanceModule(imageId, metaDataProvider, instanceModuleNames); } } diff --git a/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts b/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts index dbe74b49df..981735f5e4 100644 --- a/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts +++ b/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts @@ -16,7 +16,10 @@ import { extractSliceThicknessFromDataset, } from './extractPositioningFromDataset'; import isNMReconstructable from '../../isNMReconstructable'; -import { getDatasetModule } from '../../getDatasetModule'; +import { + getInstanceModule, + instanceModuleNames, +} from '../../getInstanceModule'; function metaDataProvider(type, imageId) { const { dicomParser } = external; @@ -247,25 +250,8 @@ function metaDataProvider(type, imageId) { }; } - if (type === 'dataset') { - const types = [ - 'multiframeModule', - 'generalSeriesModule', - 'patientStudyModule', - 'imagePlaneModule', - 'nmMultiframeGeometryModule', - 'imagePixelModule', - 'modalityLutModule', - 'voiLutModule', - 'sopCommonModule', - 'petIsotopeModule', - 'overlayPlaneModule', - 'transferSyntax', - 'petSeriesModule', - 'petImageModule', - ]; - - return getDatasetModule(imageId, metaDataProvider, types); + if (type === 'instance') { + return getInstanceModule(imageId, metaDataProvider, instanceModuleNames); } } From b4afff5d3a6b9ce0729aa290a523e85014fa489e Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 26 Jul 2023 14:39:49 -0400 Subject: [PATCH 17/20] add adapters section to examples --- utils/ExampleRunner/example-info.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index 44b54e4105..ea9079ff83 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -17,6 +17,9 @@ }, "dicom-image-loader": { "description": "usage of cornerstone dicom image loader" + }, + "adapters": { + "description": "usage of cornerstone adapters" } }, "examplesByCategory": { From cb4ebc5d59645505b5b920934e0becce87d8a630 Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 27 Jul 2023 14:07:09 -0400 Subject: [PATCH 18/20] add adapters dep to docs --- packages/docs/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/docs/package.json b/packages/docs/package.json index de64c8312d..0fa01c9f13 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -32,6 +32,7 @@ "@cornerstonejs/dicom-image-loader": "^1.6.0", "@cornerstonejs/streaming-image-volume-loader": "^1.6.0", "@cornerstonejs/tools": "^1.6.0", + "@cornerstonejs/adapters": "^1.6.0", "@docusaurus/core": "2.3.1", "@docusaurus/module-type-aliases": "2.3.1", "@docusaurus/plugin-google-gtag": "2.3.1", From 2fde8fbde62d5ced8e36c1a49972ebc434ec1cee Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 27 Jul 2023 14:08:39 -0400 Subject: [PATCH 19/20] add eslint rule --- .eslintrc.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.eslintrc.json b/.eslintrc.json index 42a6c1fcc9..34122cc969 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -53,6 +53,8 @@ "tsconfigRootDir": "./" }, "rules": { + // Enforce consistent brace style for all control statements for readability + "curly": "error", "tsdoc/syntax": "warn", "@typescript-eslint/ban-ts-comment": "off", "no-console": [ From e65febcf848220b02c2a27c23baf7b36d9ca7716 Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 27 Jul 2023 14:22:52 -0400 Subject: [PATCH 20/20] example deploy --- utils/ExampleRunner/build-all-examples-cli.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utils/ExampleRunner/build-all-examples-cli.js b/utils/ExampleRunner/build-all-examples-cli.js index a0c8cbac92..a35b48fa7a 100644 --- a/utils/ExampleRunner/build-all-examples-cli.js +++ b/utils/ExampleRunner/build-all-examples-cli.js @@ -63,6 +63,10 @@ if (options.fromRoot === true) { path: 'packages/dicomImageLoader/examples', regexp: 'index.ts', }, + { + path: 'packages/adapters/examples', + regexp: 'index.ts', + }, ], }; } else {