封面《ましろ色シンフォニー》
 前言
之前师兄师姐的系统中展示医疗影像用的都是 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
 | 
官方使用文档如下
| 12
 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 的变化。完整代码如下
| 12
 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 库做一个记录,因为这个库在展示医疗影像上效果非常好,希望对大家有所帮助。