第 15 章:vtkImageMapper 与 vtkImageSlice——从三维数据取一张切片
学习目标
第 11 章已把数值放入 vtkImageData。本章学习如何显示其中一层,以及切片层号、世界坐标、窗宽窗位和屏幕插值各控制什么。读完后,你应该能搭建一个可调层号的查看器,并清楚说明为什么看到三维数据的切片仍不等于体渲染。
15.1 一份三维数据,一张二维画面
vtkImageData 可以有 [nx, ny, nz] 个采样点,但一次轴向切片只选定一个固定的 k,显示所有 (i, j, k)。第 4 阶段示例使用 32 × 24 × 12 的规则网格:
const image = vtkImageData.newInstance();
image.setDimensions(32, 24, 12);
image.setOrigin(0, 0, 0);
image.setSpacing(0.8, 1.2, 2);
image.getPointData().setScalars(intensityArray);
这份图像有 32 × 24 × 12 = 9216 个点标量。K=5 切片选取第 5 层的 32 × 24 = 768 个采样位置。网格点的世界 z 坐标为 0 + 5 × 2 = 10。本例原点和方向简单,便于核对;遇到非单位方向或非零 extent,应利用 indexToWorld 检查位置。
15.2 vtkImageMapper 选择哪一层
与表面数据使用 vtkMapper 不同,图像切片使用 vtkImageMapper:
const imageMapper = vtkImageMapper.newInstance();
imageMapper.setInputData(image);
imageMapper.setSlicingMode(vtkImageMapper.SlicingMode.K);
imageMapper.setSlice(5);
K 表示沿图像的第三个索引轴取切片。I 和 J 对应另外两组正交切面。它们说的是图像索引轴;X、Y、Z 模式则表达世界坐标轴方向的切片。当图像 direction 不是单位矩阵时,K 轴可能不再平行于世界 Z 轴,不能再把“K=5”直接读成“世界 z=10”。
setSlice(5) 在 K 模式下指定层号,而不是一次射线积分的深度。示例中的滑块限制为 0–11,对应第三维 12 个采样点的有效索引。真实数据可能有非零起始 extent,滑块边界应从数据的 extent 读取,不能总是写成 0–nz−1。
15.3 vtkImageSlice 是场景对象
Mapper 仍不能单独进入 Renderer,需要一个 vtkImageSlice:
const imageSlice = vtkImageSlice.newInstance();
imageSlice.setMapper(imageMapper);
renderer.addViewProp(imageSlice);
renderer.resetCamera();
renderWindow.render();
这条关系与表面的 vtkMapper → vtkActor 平行,但对象类型不同:
| 数据 | Mapper | 可见对象 | 典型用途 |
|---|---|---|---|
vtkPolyData |
vtkMapper |
vtkActor |
三角网格、线、点的表面显示 |
vtkImageData |
vtkImageMapper |
vtkImageSlice |
从规则网格显示一张切片 |
vtkImageSlice.getProperty() 返回 vtkImageProperty,不是表面 Actor 的 vtkProperty。因此调灰度窗宽窗位、图像插值,应去找 ImageSlice 的 Property;调切片层号,应去找 ImageMapper。
15.4 窗宽与窗位:显示范围的快速控制
医学影像常用 Window/Level,中文通常称窗宽/窗位。窗位 L 是灰度窗口中心,窗宽 W 是窗口跨度;直观上关注的数值区间约为:
[L − W/2, L + W/2]
第 4 阶段示例初始 W=100, L=80,所以关注的区间约为 [30, 130]:
imageSlice.getProperty().setColorWindow(100);
imageSlice.getProperty().setColorLevel(80);
在默认灰度显示中,低于窗口的值靠近黑端,高于窗口的值靠近白端,窗口内的值映射到中间灰度。减小窗宽会提高该区间内的对比度,同时更容易让两端的细节饱和;移动窗位则把关注区间移向更高或更低的数值。它们只改变显示,不改写 Intensity 的原始采样值。
窗宽窗位与第 14 章的表面 mapper.setScalarRange(...) 功能相近,设置位置却不同。若为 ImageProperty 另配颜色表并启用其 useLookupTableScalarRange,还要确认究竟由窗宽窗位还是颜色表范围控制显示。第 17–18 章会细讲传递函数。
15.5 图像插值与像素感
切片投到屏幕时,屏幕像素通常不会一一对应原始采样点。ImageProperty 可选择:
imageSlice.getProperty().setInterpolationTypeToNearest();
// 或
imageSlice.getProperty().setInterpolationTypeToLinear();
最近邻保持原始采样块的边界,适合检查标签图或观察像素结构;线性插值使连续强度图像看起来更平滑,但新显示的中间灰度是插值结果,不等于多了测量数据。示例用“最近邻插值”开关比较两者。它控制切片显示采样,和第 14 章表面 Mapper 的“先插值标量还是先映射颜色”不是同一个设置。
spacing=[0.8, 1.2, 2] 表明三个方向的物理距离不同。若把屏幕上每个采样都画成相同宽高的方块,形状会失真。示例按世界坐标显示,平面内横向与纵向跨度分别由 0.8 和 1.2 决定。
15.6 切片与体渲染的根本区别
切片只显示一个位置的二维截面;移动滑块是在不同截面之间切换。屏幕某个像素主要对应当前切片附近的采样值。体渲染则沿每个屏幕像素对应的三维视线多次采样,将多个深度位置的颜色与不透明度累积。后者需要定义哪些值透明、哪些值可见;第 16–20 章会逐步建立这个模型。
vtkImageMapper 本章适合沿图像轴的正交切片。任意倾斜切面通常需要图像重采样或相应的 reslice Mapper;不要把改变相机角度误认为改变了切片平面。相机只改变观察切片的方向。
15.7 排错检查
| 现象 | 检查 |
|---|---|
| 画面空白 | ImageData 是否有 Point Data 标量;Mapper 是否连接;ImageSlice 是否加入 Renderer;相机是否看向切片 |
| 切片拖动却不变 | setSlicingMode 与 setSlice 是否设置在同一个 Mapper;数据在 k 方向是否真的变化 |
| 长宽比例不对 | spacing、direction 与相机投影设置 |
| 灰度全黑或全白 | 原始数组范围与当前窗宽窗位是否匹配 |
| 把层号当成毫米数 | 用 indexToWorld([i,j,k]) 验证对应的世界位置 |
本章小结
vtkImageMapper 选择三维规则网格中的切片,vtkImageSlice 将它放入场景,vtkImageProperty 控制窗宽窗位和图像插值。此时你看到的是三维数据的一张二维截面。下一阶段会从一张切片转向沿视线采样整个体,解释真正的体渲染。