封面《ましろ色シンフォニー》
前言
之前师兄师姐的系统中展示医疗影像用的都是 2D 切片。这种方法会使得切片多,展示起来很丑陋且不够直观。因此一直想找一个 3D 的方案展示。这个时候找到了 niivue 库,所以在自己做的系统里面封装使用记录一下。
niivue
niivue 是一个基于 WebGL2 的前端库,可以在 Web 页面展示 NIFIT、DICOM 等多种格式的医学影像并且提供一个 3D 的展示效果。官方 Demo 地址 https://niivue.github.io/niivue-ui/
使用
因为我的系统是基于 React+Ant Design Pro 搭建,因此下面的工程都是基于 React 写,Vue 可以看官方文档
安装 & 使用
1
| npm install @niivue/niivue
|
官方使用文档如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" /> <title>NiiVue</title> </head> <body> <div> <label> Show Orient Cube <input type="checkbox" checked id="cube-checkbox" /> </label> <label> Ventricles Opacity <input type="range" min="0.0" max="1.0" step="0.1" value="0.5" id="opacity-slider"/> </label> </div> <div> <canvas id="gl1"></canvas> </div> <script type="module" async> import { Niivue } from "@niivue/niivue"; const opacitySlider = document.getElementById('opacity-slider'); const cubeCheckbox = document.getElementById('cube-checkbox'); const nv = new Niivue(); nv.attachTo("gl1"); const volumes = [ { url: 'https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/23/template.nii.gz', }, { url: 'https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/49/ventricles.nii.gz', opacity: opacitySlider.value, colormap: 'red' } ]; await nv.loadVolumes(volumes); opacitySlider.oninput = function() { nv.setOpacity(1, opacitySlider.value); } cubeCheckbox.oninput = function () { nv.opts.isOrientCube = cubeCheckbox.checked; nv.updateGLVolume(); } cubeCheckbox.oninput(); </script> </body> </html>
|
封装
niivue 本身很好用,但是组件中不含有图注,需要自己加,因此基于 Antd 的 CheckedTag 模块加上了一个图注。
这里面主要是使用 canvas
进行绘制,更新时在 useEffect
中对 volumelist
进行更新,useEffect
监听 volumelist
和 checktag
的变化。完整代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| import { Niivue } from "@niivue/niivue"; import { Space, Tag, Tooltip } from "antd"; import _ from "lodash"; import React, { useEffect, useRef, useState } from "react";
const { CheckableTag } = Tag;
export type Volume = { url: string; volume: { hdr: any; img: any }; colorMap: string; opacity: number; visible: boolean; strokeType?: string; };
export type NiiVueProps = { volumeList: Volume[]; };
export const NiiVue: React.FC<NiiVueProps> = ({ volumeList }: NiiVueProps) => { const [selectedTags, setSelectedTags] = useState<string[]>( _.compact(_.map(volumeList, "strokeType")) ); const canvas = useRef();
const handleChange = (tag: string, checked: boolean) => { const nextSelectedTags = checked ? [...selectedTags, tag] : selectedTags.filter((t) => t !== tag); console.log("You are interested in: ", nextSelectedTags); setSelectedTags(nextSelectedTags); };
useEffect(() => { if (!volumeList || volumeList.length === 0) { return; } let _volumeList = _.compact( _.map(volumeList, (volume) => { if (volume.strokeType) { if (_.includes(selectedTags, volume.strokeType)) { return { ...volume, }; } } else { return volume; } }) ); const nv = new Niivue(); nv.attachToCanvas(canvas.current); nv.loadVolumes(_volumeList); }, [volumeList, selectedTags]);
return ( <> <div> <Space> {volumeList && volumeList.length > 0 && _.map(volumeList, (volume) => { if (volume.strokeType) { return ( <CheckableTag style={{ backgroundColor: selectedTags.includes(volume.strokeType) ? volume.colorMap : "", }} key={volume.strokeType} checked={selectedTags.includes(volume.strokeType)} onChange={(checked) => { handleChange(volume.strokeType, checked); }} > {volume.strokeType} </CheckableTag> ); } })} </Space> </div> <div> <Tooltip title="按v键切换影像"> <canvas ref={canvas} height={480} width={640} /> </Tooltip> </div> </> ); };
|
效果
完整代码在 https://github.com/qxdn/niivue-demo-qxdn
使用的数据来自 ISLES2022
后记
这篇主要为所使用的 niivue 库做一个记录,因为这个库在展示医疗影像上效果非常好,希望对大家有所帮助。