From d5fd5863583b36a10d4b01679133179e4f3da520 Mon Sep 17 00:00:00 2001 From: Forrest Date: Wed, 28 Jun 2023 17:43:03 -0400 Subject: [PATCH] fix(itk): downgrade so median filter works The median filter depends on itk.js v13 and an older version of ITKHelper, which has been copied in-tree. --- externals/ITKReader/ITKDicomImageReader.js | 2 +- package-lock.json | 100 ++++++--- package.json | 4 +- src/components/core/GirderBox/script.js | 2 +- src/components/tools/MedianFilter/script.js | 2 +- src/vtk/ITKHelper/index.js | 216 ++++++++++++++++++++ 6 files changed, 296 insertions(+), 30 deletions(-) create mode 100644 src/vtk/ITKHelper/index.js diff --git a/externals/ITKReader/ITKDicomImageReader.js b/externals/ITKReader/ITKDicomImageReader.js index c0106080..c6027673 100644 --- a/externals/ITKReader/ITKDicomImageReader.js +++ b/externals/ITKReader/ITKDicomImageReader.js @@ -2,7 +2,7 @@ import regeneratorRuntime from 'regenerator-runtime'; import macro from '@kitware/vtk.js/macro'; -import ITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper'; +import ITKHelper from 'paraview-glance/src/vtk/ITKHelper'; import readImageDICOMFileSeries from 'itk/readImageDICOMFileSeries'; diff --git a/package-lock.json b/package-lock.json index 06c28537..a4b8f7b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@girder/components": "3.1.0", "@kitware/vtk.js": "28.0.0", "@linusborg/vue-simple-portal": "0.1.5", - "itk": "14.1.1", + "itk": "13.1.4", "jszip": "3.7.1", "mousetrap": "1.6.5", "patch-package": "^6.4.7", @@ -14160,22 +14160,50 @@ } }, "node_modules/itk": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/itk/-/itk-14.1.1.tgz", - "integrity": "sha512-gLHkndNe7s00lYzasdM74IgrxryF5PY5uqlTRXlCXK09l095M8Qs6WKhZm/DEavQ+rhE5tQL1qowqmQTIfUM2A==", - "dependencies": { - "@babel/runtime": "^7.13.6", - "axios": "^0.21.1", - "commander": "^2.20.3", - "fs-extra": "^9.1.0", - "mime-types": "^2.1.29", - "promise-file-reader": "^1.0.3", + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/itk/-/itk-13.1.4.tgz", + "integrity": "sha512-WS3su/SaU/ks+9rsAgBKJh6aVw2Mg0lEEXYPJmMJzGpbLiP0MEoHEo0bsTigFAGWMkDq4/PfaIf7RO0PGq962Q==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "axios": "^0.19.0", + "commander": "^2.19.0", + "fs-extra": "^9.0.0", + "mime-types": "^2.1.21", + "promise-file-reader": "^1.0.2", "webworker-promise": "^0.4.2" }, "bin": { "itk-js": "itk-js-cli.js" } }, + "node_modules/itk/node_modules/axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", + "dependencies": { + "follow-redirects": "1.5.10" + } + }, + "node_modules/itk/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/itk/node_modules/follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "dependencies": { + "debug": "=3.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/itk/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -15538,8 +15566,7 @@ "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "node_modules/multicast-dns": { "version": "6.2.3", @@ -39390,19 +39417,43 @@ } }, "itk": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/itk/-/itk-14.1.1.tgz", - "integrity": "sha512-gLHkndNe7s00lYzasdM74IgrxryF5PY5uqlTRXlCXK09l095M8Qs6WKhZm/DEavQ+rhE5tQL1qowqmQTIfUM2A==", - "requires": { - "@babel/runtime": "^7.13.6", - "axios": "^0.21.1", - "commander": "^2.20.3", - "fs-extra": "^9.1.0", - "mime-types": "^2.1.29", - "promise-file-reader": "^1.0.3", + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/itk/-/itk-13.1.4.tgz", + "integrity": "sha512-WS3su/SaU/ks+9rsAgBKJh6aVw2Mg0lEEXYPJmMJzGpbLiP0MEoHEo0bsTigFAGWMkDq4/PfaIf7RO0PGq962Q==", + "requires": { + "@babel/runtime": "^7.9.2", + "axios": "^0.19.0", + "commander": "^2.19.0", + "fs-extra": "^9.0.0", + "mime-types": "^2.1.21", + "promise-file-reader": "^1.0.2", "webworker-promise": "^0.4.2" }, "dependencies": { + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, "fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -40518,8 +40569,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "multicast-dns": { "version": "6.2.3", diff --git a/package.json b/package.json index 1355b59b..22f34f26 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@girder/components": "3.1.0", "@kitware/vtk.js": "28.0.0", "@linusborg/vue-simple-portal": "0.1.5", - "itk": "14.1.1", + "itk": "13.1.4", "jszip": "3.7.1", "mousetrap": "1.6.5", "patch-package": "^6.4.7", @@ -119,4 +119,4 @@ "@semantic-release/github" ] } -} +} \ No newline at end of file diff --git a/src/components/core/GirderBox/script.js b/src/components/core/GirderBox/script.js index a7a1a194..4d5ef7f9 100644 --- a/src/components/core/GirderBox/script.js +++ b/src/components/core/GirderBox/script.js @@ -11,7 +11,7 @@ import { import writeImageArrayBuffer from 'itk/writeImageArrayBuffer'; -import ITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper'; +import ITKHelper from 'paraview-glance/src/vtk/ITKHelper'; import vtkXMLPolyDataWriter from '@kitware/vtk.js/IO/XML/XMLPolyDataWriter'; import vtkXMLWriter from '@kitware/vtk.js/IO/XML/XMLWriter'; import vtkSTLWriter from '@kitware/vtk.js/IO/Geometry/STLWriter'; diff --git a/src/components/tools/MedianFilter/script.js b/src/components/tools/MedianFilter/script.js index dc30c22c..a7a4c9bf 100644 --- a/src/components/tools/MedianFilter/script.js +++ b/src/components/tools/MedianFilter/script.js @@ -1,7 +1,7 @@ import runPipelineBrowser from 'itk/runPipelineBrowser'; import IOTypes from 'itk/IOTypes'; import macro from '@kitware/vtk.js/macro'; -import ITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper'; +import ITKHelper from 'paraview-glance/src/vtk/ITKHelper'; import { Portal } from '@linusborg/vue-simple-portal'; diff --git a/src/vtk/ITKHelper/index.js b/src/vtk/ITKHelper/index.js new file mode 100644 index 00000000..9b484282 --- /dev/null +++ b/src/vtk/ITKHelper/index.js @@ -0,0 +1,216 @@ +import macro from '@kitware/vtk.js/macros'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; +import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; + +const { vtkErrorMacro } = macro; + +// see itk.js/PixelTypes.js +const ITKPixelTypes = { + Unknown: 0, + Scalar: 1, + RGB: 2, + RGBA: 3, + Offset: 4, + Vector: 5, + Point: 6, + CovariantVector: 7, + SymmetricSecondRankTensor: 8, + DiffusionTensor3D: 9, + Complex: 10, + FixedArray: 11, + Array: 12, + Matrix: 13, + VariableLengthVector: 14, + VariableSizeMatrix: 15, +}; + +/** + * Converts an itk.js image to a vtk.js image. + * + * Requires an itk.js image as input. + */ +function convertItkToVtkImage(itkImage, options = {}) { + const vtkImage = { + origin: [0, 0, 0], + spacing: [1, 1, 1], + }; + + const dimensions = [1, 1, 1]; + const direction = [1, 0, 0, 0, 1, 0, 0, 0, 1]; + + for (let idx = 0; idx < itkImage.imageType.dimension; ++idx) { + vtkImage.origin[idx] = itkImage.origin[idx]; + vtkImage.spacing[idx] = itkImage.spacing[idx]; + dimensions[idx] = itkImage.size[idx]; + for (let col = 0; col < itkImage.imageType.dimension; ++col) { + // ITK (and VTKMath) use a row-major index axis, but the direction + // matrix on the vtkImageData is a webGL matrix, which uses a + // column-major data layout. Transpose the direction matrix from + // itkImage when instantiating that vtkImageData direction matrix. + direction[col + idx * 3] = + itkImage.direction.data[idx + col * itkImage.imageType.dimension]; + } + } + + // Create VTK Image Data + const imageData = vtkImageData.newInstance(vtkImage); + + // Create VTK point data -- the data associated with the pixels / voxels + const pointData = vtkDataArray.newInstance({ + name: options.scalarArrayName || 'Scalars', + values: itkImage.data, + numberOfComponents: itkImage.imageType.components, + }); + + imageData.setDirection(direction); + imageData.setDimensions(...dimensions); + // Always associate multi-component pixel types with vtk.js point data + // scalars to facilitate multi-component volume rendering + imageData.getPointData().setScalars(pointData); + + // Associate the point data that are 3D vectors / tensors + // Refer to itk-js/src/PixelTypes.js for numerical values + switch (itkImage.imageType.pixelType) { + case ITKPixelTypes.Scalar: + break; + case ITKPixelTypes.RGB: + break; + case ITKPixelTypes.RGBA: + break; + case ITKPixelTypes.Offset: + break; + case ITKPixelTypes.Vector: + if ( + itkImage.imageType.dimension === 3 && + itkImage.imageType.components === 3 + ) { + imageData.getPointData().setVectors(pointData); + } + break; + case ITKPixelTypes.Point: + break; + case ITKPixelTypes.CovariantVector: + if ( + itkImage.imageType.dimension === 3 && + itkImage.imageType.components === 3 + ) { + imageData.getPointData().setVectors(pointData); + } + break; + case ITKPixelTypes.SymmetricSecondRankTensor: + if ( + itkImage.imageType.dimension === 3 && + itkImage.imageType.components === 6 + ) { + imageData.getPointData().setTensors(pointData); + } + break; + case ITKPixelTypes.DiffusionTensor3D: + if ( + itkImage.imageType.dimension === 3 && + itkImage.imageType.components === 6 + ) { + imageData.getPointData().setTensors(pointData); + } + break; + case ITKPixelTypes.Complex: + break; + case ITKPixelTypes.FixedArray: + break; + case ITKPixelTypes.Array: + break; + case ITKPixelTypes.Matrix: + break; + case ITKPixelTypes.VariableLengthVector: + break; + case ITKPixelTypes.VariableSizeMatrix: + break; + default: + vtkErrorMacro( + `Cannot handle unexpected ITK.js pixel type ${itkImage.imageType.pixelType}` + ); + return null; + } + + return imageData; +} + +const vtkArrayTypeToItkComponentType = new Map([ + ['Uint8Array', 'uint8_t'], + ['Int8Array', 'int8_t'], + ['Uint16Array', 'uint16_t'], + ['Int16Array', 'int16_t'], + ['Uint32Array', 'uint32_t'], + ['Int32Array', 'int32_t'], + ['Float32Array', 'float'], + ['Float64Array', 'double'], +]); + +/** + * Converts a vtk.js image to an itk.js image. + * + * Requires a vtk.js image as input. + */ +function convertVtkToItkImage(vtkImage, copyData = false) { + const itkImage = { + imageType: { + dimension: 3, + pixelType: ITKPixelTypes.Scalar, + componentType: '', + components: 1, + }, + name: 'name', + origin: vtkImage.getOrigin(), + spacing: vtkImage.getSpacing(), + direction: { + data: [1, 0, 0, 0, 1, 0, 0, 0, 1], + }, + size: vtkImage.getDimensions(), + }; + + const direction = vtkImage.getDirection(); + + const dimension = itkImage.size.length; + itkImage.imageType.dimension = dimension; + itkImage.direction.rows = dimension; + itkImage.direction.columns = dimension; + + // Transpose the direction matrix from column-major to row-major + for (let idx = 0; idx < dimension; ++idx) { + for (let idy = 0; idy < dimension; ++idy) { + itkImage.direction.data[idx + idy * dimension] = + direction[idy + idx * dimension]; + } + } + + const pointData = vtkImage.getPointData(); + + let vtkArray; + if (pointData.getTensors() !== null) { + itkImage.imageType.pixelType = ITKPixelTypes.DiffusionTensor3D; + vtkArray = pointData.getTensors(); + } else if (pointData.getVectors() != null) { + itkImage.imageType.pixelType = ITKPixelTypes.Vector; + vtkArray = pointData.getVectors(); + } else { + vtkArray = pointData.getScalars(); + } + + itkImage.imageType.componentType = vtkArrayTypeToItkComponentType.get( + vtkArray.getDataType() + ); + + if (copyData) { + // Copy the data array + itkImage.data = vtkArray.getData().slice(0); + } else { + itkImage.data = vtkArray.getData(); + } + + return itkImage; +} + +export default { + convertItkToVtkImage, + convertVtkToItkImage, +};