Cornerstone3D:快速搭建可以读取本地文件且四视图显示的Nifti Viewer

成品展示

项目在线地址:

https://giticecube.github.io/nifti-viewer/

无法访问的话,试试加点科学。

项目环境:

  • Vue 3
  • Cornerstone3D 2.0
  • Vite 6

需求剖析:

  1. 读取本地Nifti文件
    • 需要Nifti Volume Loader
  2. 四视图显示
    • 渲染四视图需要Volume

如何快速起手一个项目?(如何从官方样例学习如何使用Cornerstone3D)

  1. https://www.cornerstonejs.org/live-examples/niftibasic
    • 通过这个样例学习如何读取Nifti文件
      1. 导入依赖项
      2. 创建一个Idelement1div元素,并将其长宽设置为500px(显示窗口的大小)
      3. 定义viewportId1
      4. 定义niftiURL
      5. 运行导入的initDemo进行环境的初始化
      6. 注册Nifti加载器:imageLoader.registerImageLoader('nifti', cornerstoneNiftiImageLoader);
      7. 定义imageIds:需要使用niftiURL
      8. 定义renderingEngineId
      9. 实例化渲染引擎:const renderingEngine = new RenderingEngine(renderingEngineId);
      10. 定义视窗参数数组:设置各个窗口的渲染参数(viewportIdtypeelement1等)。
      11. 使用视窗数组中的参数对引擎进行设置。 renderingEngine.setViewports(viewportInputArray);
      12. renderingEngine中获取viewport
      13. imageIds装载到viewport
      14. 渲染引擎渲染renderingEngine.render();
  2. https://www.cornerstonejs.org/live-examples/niftiwithtools
    • 通过这个样例学习如何渲染三视图MPR
  3. https://www.cornerstonejs.org/live-examples/volumeviewport3d
    • 通过这个样例学习如何渲染3D Volume
  4. 如果你不知道怎么复现这些样例,可以查看我Cornerstone3D专栏的其他文章。

项目代码展示(Vue3)

  • 首先设置好四个视窗的HTML元素

    javascript 复制代码
    <template>
        <div class="main">
            <h1>Cornerstone3D Nifti Viewer(支持.nii和.nii.gz格式)</h1>
            <input type="file" multiple @change="handlefile" />
            <div id="view">
                <div>
                    <div id="element1"></div>
                    <div id="element2"></div>
                </div>
                <div>
                    <div id="element3"></div>
                    <div id="element4"></div>
                </div>
            </div>
        </div>
    </template>
    • 设置一下元素的样式,窗口的长宽不能为0
    javascript 复制代码
    <style scoped>
    .main {
        display: flex;
        flex-direction: column;
        width: 1200px;
    }
    
    #element1,
    #element2,
    #element3,
    #element4 {
        width: 500px;
        height: 500px;
    }
    
    #view {
        margin-top: 2rem;
        display: flex;
        flex-direction: row;
        justify-content: center;
        align-items: center;
    }
    </style>
  • 接下来就是JavaScript部分

    • 首先导入依赖项
    javascript 复制代码
    import {
        RenderingEngine, Enums, imageLoader, CONSTANTS,
        setVolumesForViewports,
        volumeLoader,
    } from '@cornerstonejs/core';
    import {
        cornerstoneNiftiImageLoader,
        createNiftiImageIdsAndCacheMetadata,
    } from '@cornerstonejs/nifti-volume-loader';
    import { initDemo } from '../utils/demo/helpers';  //是个初始化函数,从cornerstone3D的官方仓库,有同名的文件,拷贝过来的
    import { onMounted } from 'vue';
    import pako from 'pako';   //用于将.gz文件解压
    • 定义变量
    javascript 复制代码
    let viewport;
    let imageIds;
    let volumeId;
    let volume;
    let renderingEngineId;
    let renderingEngine;
    const viewportId1 = 'CT_NIFTI_AXIAL';
    const viewportId2 = 'CT_NIFTI_SAGITTAL';
    const viewportId3 = 'CT_NIFTI_CORONAL';
    const viewportId4 = '3D_VIEWPORT';
    let viewportInputArray;
    
    const viewportIds = [viewportId1, viewportId2, viewportId3, viewportId4];
    const { ViewportType } = Enums;
    
    let niftiURL = 'https://ohif-assets.s3.us-east-2.amazonaws.com/nifti/CTACardio.nii.gz';//官方的样例,可能需要科学才能加载成功。
    const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use
    • 使用Vue的钩子,以下代码将在组件挂载后执行,因为不写在钩子里是引用不到这些元素的,因为还没有元素。

      javascript 复制代码
      onMounted(() => {
      	    const element1 = document.getElementById('element1');
      	    const element2 = document.getElementById('element2');
      	    const element3 = document.getElementById('element3');
      	    const element4 = document.getElementById('element4');
      	    setup();
      	})
    • setup函数

    javascript 复制代码
    async function setup() {
        volumeId = `${volumeLoaderScheme}:${niftiURL}`; // VolumeId with loader id + volume id
        await initDemo();  //初始化
    
        imageLoader.registerImageLoader('nifti', cornerstoneNiftiImageLoader);  //注册nifti格式的图像加载器
    
        imageIds = await createNiftiImageIdsAndCacheMetadata({ url: niftiURL });  //创建nifti格式的图像id并缓存元数据
    
        renderingEngineId = 'myRenderingEngine'; 
        renderingEngine = new RenderingEngine(renderingEngineId);  //创建渲染引擎
    
    
        viewportInputArray = [    //视口数组
            {
                viewportId: viewportId1,
                type: Enums.ViewportType.ORTHOGRAPHIC,
                element: element1,
                defaultOptions: {
                    orientation: Enums.OrientationAxis.AXIAL,
                },
            },
            {
                viewportId: viewportId2,
                type: Enums.ViewportType.ORTHOGRAPHIC,
                element: element2,
                defaultOptions: {
                    orientation: Enums.OrientationAxis.SAGITTAL,
                },
            },
            {
                viewportId: viewportId3,
                type: Enums.ViewportType.ORTHOGRAPHIC,
                element: element3,
                defaultOptions: {
                    orientation: Enums.OrientationAxis.CORONAL,
                },
            },
            {
                viewportId: viewportId4,
                type: ViewportType.VOLUME_3D,
                element: element4,
                defaultOptions: {
                    orientation: Enums.OrientationAxis.CORONAL,
                    background: CONSTANTS.BACKGROUND_COLORS.slicer3D,
                },
            },
        ];
    
        renderingEngine.setViewports(viewportInputArray);  //设置视口
        volume = await volumeLoader.createAndCacheVolume(volumeId, {   //创建并缓存体积
            imageIds,
        });
        await volume.load();   //加载体积
        viewport = renderingEngine.getViewport(viewportId4);  //获取视口
        await setVolumesForViewports(   //设置视口的体积
            renderingEngine,
            [{ volumeId }],
            viewportInputArray.map((v) => v.viewportId)
        ).then(() => {
            viewport.setProperties({
                preset: 'CT-Bone',
            });
            viewport.render();
    
        })
        renderingEngine.render();   //渲染
    
    }
    • 到目前为止,已经实现了读取服务器的文件,并显示在四视图上,都是通过样例就可以直接写出来的代码。接下来,将会写样例中没有出现的读取本地文件功能。
      • 官方提供的url:'https://ohif-assets.s3.us-east-2.amazonaws.com/nifti/CTACardio.nii.gz';加载太久了,甚至加载不出来,怎么办?我想读取本地文件,我可以直接使用我的文件的地址吗?比如http://localhost:5173/mynii.nii
      • 我试过了不可行,不可行的原因是因为这个url无法通过createNiftiImageIdsAndCacheMetadata函数产生有效的imageIds。原因是createNiftiImageIdsAndCacheMetadata使用fetch方法接受一个url,而localhost这种形式的url无法使用fetch方法。那么什么形式的url可以使用fetch呢?
        1. 如果文件在你的项目中,属于静态资源,那么可以使用构建工具的静态资源带入为url的方法。在vite中,是通过let niftiURL = new URL('你的文件路径', import.meta.url).href;导入的。
        2. 如果你的文件是通过input上传的,那么可以通过URL.createObjectURL(file);方法。
      • 好的,那么我们有了可以fetchurl,当你读取.nii文件时,应该可以直接显示了,但是当我们读取.nii.gz文件时,又会报错了,原因是createNiftiImageIdsAndCacheMetadata这个方法会检测url是否以.gz结尾来检测文件是否是压缩文件,而我们提供的url通过编码后已经没有这些.gz的特征了,所以我们我们在使用createNiftiImageIdsAndCacheMetadata方法前,先将我们的文件通过pako这个库解压成.nii文件,然后再转化为url传入这个方法。
    javascript 复制代码
    async function handlefile(e) {
        let file = e.target.files[0];
        if (!file) return;
        if (file.name.endsWith('.gz')) {   // 如果是gz压缩文件
            const fileContent = await file.arrayBuffer();
    
            try {
                // 使用 pako 解压
                const decompressedData = pako.inflate(new Uint8Array(fileContent));
                file = new Blob([decompressedData], { type: 'application/octet-stream' });
            } catch (error) {
                console.error('Decompression failed:', error);
            }
        }
    
        //更新设置,并重新渲染
        niftiURL = URL.createObjectURL(file);
        imageIds = await createNiftiImageIdsAndCacheMetadata({ url: niftiURL });
        volumeId = `${volumeLoaderScheme}:${niftiURL}`;
        volume = await volumeLoader.createAndCacheVolume(volumeId, {
            imageIds,
        });
        await volume.load();
        await setVolumesForViewports(
            renderingEngine,
            [{ volumeId }],
            viewportInputArray.map((v) => v.viewportId)
        ).then(() => {
            viewport.setProperties({
                preset: 'CT-Bone',
            });
            viewport.render();
    
        })
        renderingEngine.render();
    }

其他设置

package.json

javascript 复制代码
{
  "name": "nii",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@cornerstonejs/calculate-suv": "^1.1.0",
    "@cornerstonejs/core": "^2.14.2",
    "@cornerstonejs/dicom-image-loader": "^2.14.2",
    "@cornerstonejs/nifti-volume-loader": "^2.14.2",
    "@cornerstonejs/tools": "^2.14.2",
    "@icr/polyseg-wasm": "^0.4.0",
    "canvas": "^3.0.0",
    "dcmjs": "^0.37.0",
    "dicomweb-client": "^0.10.4",
    "jest": "^29.7.0",
    "pako": "^2.1.0",
    "resemblejs": "^5.0.0",
    "vue": "^3.5.13"
  },
  "devDependencies": {
    "@originjs/vite-plugin-commonjs": "^1.0.3",
    "@vitejs/plugin-vue": "^5.2.1",
    "jest-canvas-mock": "^2.5.2",
    "vite": "^6.0.5",
    "vite-plugin-wasm": "^3.4.1"
  }
}

vite.config.json

javascript 复制代码
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from "node:url"
import vue from '@vitejs/plugin-vue'
import { viteCommonjs } from "@originjs/vite-plugin-commonjs"
import wasm from 'vite-plugin-wasm';

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue(), viteCommonjs()],
  optimizeDeps: {
    exclude: ["@cornerstonejs/dicom-image-loader",],
    include: ["dicom-parser"],
  },
  server: {
    host: "0.0.0.0",
    port: 3000,
  },
  build: {
    rollupOptions: {
      external: ["@icr/polyseg-wasm"],
    },
  },
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
  worker: {
    format: "es",
    rollupOptions: {
      external: ["@icr/polyseg-wasm"],
    },
  },
  assetsInclude: ['**/*.gz'],
  base: "/nifti-viewer/",
})
相关推荐
ss27316 分钟前
被催更了,2025元旦源码继续免费送
java·vue.js·spring boot·后端·微信小程序·开源
@ 前端小白26 分钟前
封装倒计时自定义react hook
前端·javascript·react.js
CodeClimb1 小时前
【华为OD-E卷 - 最优资源分配 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
16年上任的CTO1 小时前
一文大白话讲清楚CSS性能优化
前端·javascript·css·性能优化·css性能优化
jjw_zyfx1 小时前
vue3 css实现文字输出带光标显示,文字输出完毕,光标消失的效果
前端·javascript·css
球球不吃虾1 小时前
VuePress2配置unocss的闭坑指南
前端·javascript
时空对望2 小时前
javascript
开发语言·javascript·ecmascript
16年上任的CTO2 小时前
一文大白话讲清楚CSS预编译语言,包括Sass,Scss,Less,Stylus
javascript·css·css3·sass·scss·stylus
几度泥的菜花2 小时前
jQuery理论
前端·javascript
爱学习的小羊啊3 小时前
【前端】Vue 3.5的SSR渲染优化与Lazy Hydration
前端·javascript·vue.js