成品展示
项目在线地址:
https://giticecube.github.io/nifti-viewer/
无法访问的话,试试加点科学。
项目环境:
Vue 3
Cornerstone3D 2.0
Vite 6
需求剖析:
- 读取本地Nifti文件
- 需要
Nifti Volume Loader
- 需要
- 四视图显示
- 渲染四视图需要
Volume
- 渲染四视图需要
如何快速起手一个项目?(如何从官方样例学习如何使用Cornerstone3D)
- https://www.cornerstonejs.org/live-examples/niftibasic
- 通过这个样例学习如何读取Nifti文件
- 导入依赖项
- 创建一个
Id
为element1
的div
元素,并将其长宽设置为500px
(显示窗口的大小) - 定义
viewportId1
- 定义
niftiURL
- 运行导入的
initDemo
进行环境的初始化 - 注册
Nifti
加载器:imageLoader.registerImageLoader('nifti', cornerstoneNiftiImageLoader);
- 定义
imageIds
:需要使用niftiURL
- 定义
renderingEngineId
- 实例化渲染引擎:
const renderingEngine = new RenderingEngine(renderingEngineId);
- 定义视窗参数数组:设置各个窗口的渲染参数(
viewportId
、type
、element1
等)。 - 使用视窗数组中的参数对引擎进行设置。
renderingEngine.setViewports(viewportInputArray);
- 在
renderingEngine
中获取viewport
- 将
imageIds
装载到viewport
中 - 渲染引擎渲染
renderingEngine.render();
- 通过这个样例学习如何读取Nifti文件
- https://www.cornerstonejs.org/live-examples/niftiwithtools
- 通过这个样例学习如何渲染三视图
MPR
- 通过这个样例学习如何渲染三视图
- https://www.cornerstonejs.org/live-examples/volumeviewport3d
- 通过这个样例学习如何渲染
3D Volume
- 通过这个样例学习如何渲染
- 如果你不知道怎么复现这些样例,可以查看我
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
部分- 首先导入依赖项
javascriptimport { 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文件解压
- 定义变量
javascriptlet 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的钩子,以下代码将在组件挂载后执行,因为不写在钩子里是引用不到这些元素的,因为还没有元素。
javascriptonMounted(() => { const element1 = document.getElementById('element1'); const element2 = document.getElementById('element2'); const element3 = document.getElementById('element3'); const element4 = document.getElementById('element4'); setup(); })
-
setup
函数
javascriptasync 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
呢?- 如果文件在你的项目中,属于静态资源,那么可以使用构建工具的静态资源带入为
url
的方法。在vite
中,是通过let niftiURL = new URL('你的文件路径', import.meta.url).href;
导入的。 - 如果你的文件是通过
input
上传的,那么可以通过URL.createObjectURL(file);
方法。
- 如果文件在你的项目中,属于静态资源,那么可以使用构建工具的静态资源带入为
- 好的,那么我们有了可以
fetch
的url
,当你读取.nii
文件时,应该可以直接显示了,但是当我们读取.nii.gz
文件时,又会报错了,原因是createNiftiImageIdsAndCacheMetadata
这个方法会检测url
是否以.gz
结尾来检测文件是否是压缩文件,而我们提供的url
通过编码后已经没有这些.gz
的特征了,所以我们我们在使用createNiftiImageIdsAndCacheMetadata
方法前,先将我们的文件通过pako
这个库解压成.nii
文件,然后再转化为url
传入这个方法。
- 官方提供的
javascriptasync 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/",
})