封面《ましろ色シンフォニー》

前言

之前师兄师姐的系统中展示医疗影像用的都是 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 监听 volumelistchecktag 的变化。完整代码如下

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"))
);
// 画图ref
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;
}
// 选择要展示的volume
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);
//console.log(_volumeList);
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 库做一个记录,因为这个库在展示医疗影像上效果非常好,希望对大家有所帮助。