diff --git a/apps/aorta/package.json b/apps/aorta/package.json index 3e13d8d..dcf73a9 100644 --- a/apps/aorta/package.json +++ b/apps/aorta/package.json @@ -35,7 +35,12 @@ "@cornerstonejs/streaming-image-volume-loader": "1.41.0", "@cornerstonejs/calculate-suv": "1.1.0", "vtk.js": "29.2.0", - "itk": "14.1.1" + "itk": "14.1.1", + "@itk-wasm/dicom": "5.0.2", + "itk-wasm": "1.0.0-b.160", + "itk-image-io": "^1.0.0-b.18", + "itk-mesh-io": "^1.0.0-b.18", + "web-workers": "0.9.1" }, "devDependencies": { "@babel/core": "^7.21.8", diff --git a/apps/aorta/scripts/webpack.common.ts b/apps/aorta/scripts/webpack.common.ts index 35189e8..ae7c38e 100644 --- a/apps/aorta/scripts/webpack.common.ts +++ b/apps/aorta/scripts/webpack.common.ts @@ -7,6 +7,7 @@ import jsonLoader from "./loaders/json.loader"; import imgLoader from "./loaders/img.loader"; import babelLoader from "./loaders/babel.loader"; import { cssLoader, lessLoader } from "./loaders/style.loader"; +import CopyWebpackPlugin from "copy-webpack-plugin"; // 加载配置文件 const envConfig = dotenv.config({ @@ -63,6 +64,22 @@ const baseConfig: Configuration = { "process.env.BASE_ENV": JSON.stringify(process.env.BASE_ENV), "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV), }), + new CopyWebpackPlugin({ + patterns: [ + { + from: path.join(__dirname, "../", "node_modules", "web-workers"), + to: path.join(__dirname, "dist", "itk", "web-workers"), + }, + { + from: path.join(__dirname, "../", "node_modules", "itk-image-io"), + to: path.join(__dirname, "dist", "itk", "image-io"), + }, + { + from: path.join(__dirname, "../", "node_modules", "itk-mesh-io"), + to: path.join(__dirname, "dist", "itk", "mesh-io"), + }, + ], + }), ], }; diff --git a/apps/aorta/src/modules/Root/Viewer/LocalMpr/dicomLoader.js b/apps/aorta/src/modules/Root/Viewer/LocalMpr/dicomLoader.js new file mode 100644 index 0000000..ff3fc76 --- /dev/null +++ b/apps/aorta/src/modules/Root/Viewer/LocalMpr/dicomLoader.js @@ -0,0 +1,87 @@ +import macro from "@kitware/vtk.js/macro"; +import Helper from "@kitware/vtk.js/Common/DataModel/ITKHelper"; + + +const { convertItkToVtkImage } = Helper; + +import readImageDICOMFileSeries from "itk/readImageDICOMFileSeries"; + +function getArrayName(filename) { + const idx = filename.lastIndexOf("."); + const name = idx > -1 ? filename.substring(0, idx) : filename; + return `Scalars ${name}`; +} + +// ---------------------------------------------------------------------------- +// vtkITKDicomImageReader methods +// ---------------------------------------------------------------------------- + +function vtkITKDicomImageReader(publicAPI, model) { + // Set our className + model.classHierarchy.push("vtkITKDicomImageReader"); + + // Returns a promise to signal when image is ready + publicAPI.readFileSeries = (files, fileName) => { + if (!files || !files.length || files === model.files) { + return Promise.resolve(); + } + + if (fileName && fileName !== model.fileName) { + model.fileName = fileName; + } + + model.files = files; + + return readImageDICOMFileSeries(null, files) + .then(({ webWorker, image }) => { + webWorker.terminate(); + return image; + }) + .then(itkImage => { + const imageData = convertItkToVtkImage(itkImage, { + scalarArrayName: model.arrayName || getArrayName(model.fileName) + }); + model.output[0] = imageData; + + publicAPI.modified(); + + return imageData; + }); + }; + + publicAPI.requestData = (/* inData, outData */) => { + publicAPI.readFileSeries(model.files, model.fileName); + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + fileName: "", + // If null/undefined a unique array will be generated + arrayName: null +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + // Build VTK API + macro.obj(publicAPI, model); + macro.algo(publicAPI, model, 0, 1); + macro.setGet(publicAPI, model, ["fileName", "arrayName"]); + + // vtkITKDicomImageReader methods + vtkITKDicomImageReader(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, "vtkITKDicomImageReader"); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; diff --git a/apps/aorta/src/modules/Root/Viewer/LocalMpr/index.tsx b/apps/aorta/src/modules/Root/Viewer/LocalMpr/index.tsx index 7470789..610f015 100644 --- a/apps/aorta/src/modules/Root/Viewer/LocalMpr/index.tsx +++ b/apps/aorta/src/modules/Root/Viewer/LocalMpr/index.tsx @@ -1,11 +1,15 @@ import { useRef, useState } from "react"; import dicomParser from "dicom-parser"; -import vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume'; -import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; -import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; -import readImageDICOMFileSeries from "itk/readImageDICOMFileSeries"; -import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper'; +import vtkVolume from "@kitware/vtk.js/Rendering/Core/Volume"; +import vtkVolumeMapper from "@kitware/vtk.js/Rendering/Core/VolumeMapper"; +import vtkFullScreenRenderWindow from "@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow"; +import vtkITKHelper from "@kitware/vtk.js/Common/DataModel/ITKHelper"; +import DicomLoader from "./dicomLoader.js"; + +// import readImageDICOMFileSeries from "itk/readImageDICOMFileSeries"; +import { readImageDicomFileSeries } from "@itk-wasm/dicom"; +// import { readImageDICOMFileSeries } from "itk-wasm"; interface LocalMprProps { children?: JSX.Element; @@ -18,12 +22,23 @@ export const LocalMpr = (props: LocalMprProps) => { ) => { if (event.target.files) { const files = event.target.files; - const volumeImageData = await readImageDICOMFileSeries(files); - const vtkImage = vtkITKHelper.convertItkToVtkImage(volumeImageData); + if (!files.length) { + return; + } + + const fileList: File[] = []; + for (let i = 0; i < files.length; i++) { + fileList.push(files[i]); + } + + const image = await readImageDicomFileSeries(null, { + inputImages: fileList, + }); + const vtkImage = vtkITKHelper.convertItkToVtkImage(image); const volume = vtkVolume.newInstance(); const mapper = vtkVolumeMapper.newInstance(); - mapper.setInputData(vtkImage); volume.setMapper(mapper); + mapper.setInputData(vtkImage); // 创建 vtk 的全屏渲染窗口 const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6497677..aead81d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@cornerstonejs/tools': specifier: 1.41.0 version: 1.41.0(@babel/preset-env@7.21.5)(@kitware/vtk.js@29.2.0)(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(autoprefixer@10.4.15)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)(webpack@5.75.0)(wslink@1.11.4) + '@itk-wasm/dicom': + specifier: 5.0.2 + version: 5.0.2 '@kitware/vtk.js': specifier: 29.2.0 version: 192.168.4.201+4873/@kitware/vtk.js@29.2.0(@babel/preset-env@7.21.5)(autoprefixer@10.4.15)(webpack@5.75.0)(wslink@1.11.4) @@ -59,6 +62,15 @@ importers: itk: specifier: 14.1.1 version: 14.1.1 + itk-image-io: + specifier: ^1.0.0-b.18 + version: 1.0.0-b.18 + itk-mesh-io: + specifier: ^1.0.0-b.18 + version: 1.0.0-b.18 + itk-wasm: + specifier: 1.0.0-b.160 + version: 1.0.0-b.160 js-cookie: specifier: 3.0.5 version: 3.0.5 @@ -98,6 +110,9 @@ importers: vtk.js: specifier: 29.2.0 version: 29.2.0(@babel/preset-env@7.21.5)(autoprefixer@10.4.15)(webpack@5.75.0)(wslink@1.11.4) + web-workers: + specifier: 0.9.1 + version: 0.9.1 devDependencies: '@babel/core': specifier: ^7.21.8 @@ -3572,6 +3587,14 @@ packages: /@ioredis/commands@1.2.0: resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + /@itk-wasm/dicom@5.0.2: + resolution: {integrity: sha512-XA7J+DKtuied0yUNu5MtR4OsOOjZcexLWOdDFHDTWRWufa+/uU+Lux8mDBp3Ljh9qkJ+3ii5LRrXpBAv8ub3MQ==, tarball: http://192.168.4.201:4873/@itk-wasm%2fdicom/-/dicom-5.0.2.tgz} + dependencies: + itk-wasm: 1.0.0-b.160 + transitivePeerDependencies: + - debug + dev: false + /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4528,6 +4551,10 @@ packages: resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} dev: false + /@thewtex/zstddec@0.2.0: + resolution: {integrity: sha512-lIS+smrfa48WGlDVQSQSm0jBnwVp5XmfGJWU9q0J0fRFY9ohzK4s27Zg2SFMb1NWMp9RiANAdK+/q86EBGWR1Q==, tarball: http://192.168.4.201:4873/@thewtex%2fzstddec/-/zstddec-0.2.0.tgz} + dev: false + /@trysound/sax@0.2.0: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -4595,6 +4622,10 @@ packages: '@types/d3-color': 3.1.3 dev: false + /@types/emscripten@1.39.10: + resolution: {integrity: sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==, tarball: http://192.168.4.201:4873/@types%2femscripten/-/emscripten-1.39.10.tgz} + dev: false + /@types/eslint-scope@3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: @@ -5832,6 +5863,14 @@ packages: - debug dev: false + /axios@0.23.0: + resolution: {integrity: sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==, tarball: http://192.168.4.201:4873/axios/-/axios-0.23.0.tgz} + dependencies: + follow-redirects: 1.15.2 + transitivePeerDependencies: + - debug + dev: false + /axios@1.3.6: resolution: {integrity: sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==} dependencies: @@ -5862,6 +5901,16 @@ packages: - debug dev: false + /axios@1.6.2: + resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==, tarball: http://192.168.4.201:4873/axios/-/axios-1.6.2.tgz} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /babel-loader@9.1.2(@babel/core@7.21.8)(webpack@5.75.0): resolution: {integrity: sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==} engines: {node: '>= 14.15.0'} @@ -6483,6 +6532,11 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==, tarball: http://192.168.4.201:4873/commander/-/commander-11.1.0.tgz} + engines: {node: '>=16'} + dev: false + /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==, tarball: http://192.168.4.201:4873/commander/-/commander-2.20.3.tgz} @@ -6498,7 +6552,6 @@ packages: /commander@8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - dev: true /commander@9.2.0: resolution: {integrity: sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==, tarball: http://192.168.4.201:4873/commander/-/commander-9.2.0.tgz} @@ -8087,7 +8140,15 @@ packages: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.0 - dev: true + + /fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==, tarball: http://192.168.4.201:4873/fs-extra/-/fs-extra-11.2.0.tgz} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: false /fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} @@ -9013,6 +9074,56 @@ packages: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} + /itk-image-io@1.0.0-b.18: + resolution: {integrity: sha512-bvrn7dLdP5AkqpSdai849dzlbsS3YHs098BmKD1ZdfepARDjGs07kEsnSSQmy20CrRbIdG1vHcNkq5U37zWhKQ==, tarball: http://192.168.4.201:4873/itk-image-io/-/itk-image-io-1.0.0-b.18.tgz} + dependencies: + itk-wasm: 1.0.0-b.18 + transitivePeerDependencies: + - debug + dev: false + + /itk-mesh-io@1.0.0-b.18: + resolution: {integrity: sha512-3iV4Mx2GkXwC18susbRfIrZ4eZFCeM5yCEleALHlMDekrQAv/7Yo/6aq9lrWQN+Q8gGSNf5FE/Yk2bbmTqFcuQ==, tarball: http://192.168.4.201:4873/itk-mesh-io/-/itk-mesh-io-1.0.0-b.18.tgz} + dependencies: + itk-wasm: 1.0.0-b.18 + transitivePeerDependencies: + - debug + dev: false + + /itk-wasm@1.0.0-b.160: + resolution: {integrity: sha512-yrt0uRukqI9Q5s9HhzArxMNYvyU5CMbI1ddGzH0A4O3JWscnJug7J0RtY+RTl6nTHcqdrppNH7IRbnTXLZ79Rg==, tarball: http://192.168.4.201:4873/itk-wasm/-/itk-wasm-1.0.0-b.160.tgz} + hasBin: true + dependencies: + '@thewtex/zstddec': 0.2.0 + '@types/emscripten': 1.39.10 + axios: 1.6.2 + comlink: 4.4.1 + commander: 11.1.0 + fs-extra: 11.2.0 + glob: 8.1.0 + markdown-table: 3.0.3 + mime-types: 2.1.35 + wasm-feature-detect: 1.6.1 + transitivePeerDependencies: + - debug + dev: false + + /itk-wasm@1.0.0-b.18: + resolution: {integrity: sha512-k4vSWbHWVoDueZ0wXxfK52qZ/BMjNM0YTKawQKrpsvC2GX7MSVkZpYArZ81XKd8OkTS8uXCWWh+LS0I0otFp2A==, tarball: http://192.168.4.201:4873/itk-wasm/-/itk-wasm-1.0.0-b.18.tgz} + hasBin: true + dependencies: + '@babel/runtime': 7.22.15 + '@types/emscripten': 1.39.10 + axios: 0.23.0 + commander: 8.3.0 + fs-extra: 10.1.0 + mime-types: 2.1.35 + promise-file-reader: 1.0.3 + webworker-promise: 0.4.4 + transitivePeerDependencies: + - debug + dev: false + /itk@14.1.1: resolution: {integrity: sha512-gLHkndNe7s00lYzasdM74IgrxryF5PY5uqlTRXlCXK09l095M8Qs6WKhZm/DEavQ+rhE5tQL1qowqmQTIfUM2A==, tarball: http://192.168.4.201:4873/itk/-/itk-14.1.1.tgz} hasBin: true @@ -9518,6 +9629,10 @@ packages: engines: {node: '>=8'} dev: true + /markdown-table@3.0.3: + resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==, tarball: http://192.168.4.201:4873/markdown-table/-/markdown-table-3.0.3.tgz} + dev: false + /md5@2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} dependencies: @@ -13888,6 +14003,10 @@ packages: - webpack dev: false + /wasm-feature-detect@1.6.1: + resolution: {integrity: sha512-R1i9ED8UlLu/foILNB1ck9XS63vdtqU/tP1MCugVekETp/ySCrBZRk5I/zI67cI1wlQYeSonNm1PLjDHZDNg6g==, tarball: http://192.168.4.201:4873/wasm-feature-detect/-/wasm-feature-detect-1.6.1.tgz} + dev: false + /watchpack@2.4.0: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'} @@ -13914,6 +14033,10 @@ packages: '@zxing/text-encoding': 0.9.0 dev: false + /web-workers@0.9.1: + resolution: {integrity: sha512-tbUQc2CmCppjIM2kX2Nq4L9I8DDzQBEEGLyyoaf0a5dogG1aPh1BgmUqPSsy/CK2A4X3sJ8G1svqnnrVoCqSSA==, tarball: http://192.168.4.201:4873/web-workers/-/web-workers-0.9.1.tgz} + dev: false + /webgl-constants@1.1.1: resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==, tarball: http://192.168.4.201:4873/webgl-constants/-/webgl-constants-1.1.1.tgz} dev: false