第 24 章:Filter 对象、等值面提取与 vtkClipPolyData

学习目标

本章把 Filter 当作产生新数据的算法对象来学习。你将分清输入、输出、参数与数据连接;用 vtkImageMarchingCubes 把 ImageData 的标量场变成 PolyData;再用 vtkClipPolyData 和 vtkPlane 真正切开三角网格;最后能通过点数、单元数和几何位置验证结果,而不是只看渲染画面。

24.1 Filter 在管线中的职责

Reader 输出内存数据后,可以直接交给 Mapper,也可以先交给 Filter。Filter 接受数据对象,根据算法和参数计算新的数据对象:

vtkImageData ──> vtkImageMarchingCubes ──> vtkPolyData
                                            │
                                            v
                                      vtkClipPolyData ──> vtkPolyData
                                                               │
                                                               v
                                                       vtkMapper → vtkActor

这与在 Three.js 中直接修改 Mesh 材质不同。等值面阈值改变,三角形的顶点、连接关系和数量可能变化;裁剪平面改变,保留下来的网格也可能变化。vtkMapper 决定如何把结果绘制出来,vtkActor 决定外观与场景位置,二者不负责创建等值面。

setInputData(data) 固定连接某个当前数据对象;setInputConnection(upstream.getOutputPort()) 连接上游算法的输出端口。后者能在下游需要数据时依管线请求上游更新。getOutputData() 用于检查 Filter 当前计算结果;getOutputPort() 用于继续连接下游。把 getOutputData() 的快照交给 Mapper,虽然一时能显示,但在参数改变后可能失去自动更新链路。本阶段把 ImageData 直接交给等值面过滤器,再用输出端口串联裁剪与 Mapper。

24.2 vtkImageMarchingCubes:数值边界变三角形

这个 Filter 的输入应是带有单分量 active Point Data 标量的 vtkImageData。它遍历相邻采样点构成的体素单元,比较每个角点与等值阈值 q,在跨过 q 的单元边上插值交点,再按 marching cubes 的拓扑规则组成三角形。结果是 vtkPolyData,不是 Volume,也不保留原来每个体素里的完整标量信息。

import vtkImageMarchingCubes from '@kitware/vtk.js/Filters/General/ImageMarchingCubes';

image.getPointData().setActiveScalars('Density');
const iso = vtkImageMarchingCubes.newInstance({
  contourValue: 75,
  computeNormals: true,
  mergePoints: true,
});
iso.setInputData(image);
const surface = iso.getOutputData();
console.log(surface.isA('vtkPolyData')); // true
console.log(surface.getNumberOfPoints(), surface.getNumberOfCells());

contourValue 使用原始标量值,不是归一化后的 0–1 百分比。若值完全落在数组范围以外,通常没有交叉单元,输出为空。即使阈值位于全局最小最大值之间,也不保证出现你想要的连通外壳:噪声、离散采样、局部极值和边界截断都会影响网格。

computeNormals: true 给输出生成点法线,改善光照显示;它不改变阈值的科学含义。mergePoints: true 尝试复用共享边上的交点,减少重复顶点;是否启用可影响点数和后续拓扑处理。三角形数由输入场、阈值和采样网格共同决定,不应当硬编码为某个固定值。

修改阈值:

iso.setContourValue(110);
const changed = iso.getOutputData();
console.log(changed.getNumberOfCells());

这会使算法在下次请求输出时重新计算。与体渲染中改变 vtkPiecewiseFunction 的不透明度不同,Filter 创建的网格本身发生变化。如果切换 active scalars,要在 Point Data 中显式激活新数组,并标记输入数据修改;阶段示例在 image.modified() 后重新请求 Filter 输出。

24.3 把等值面送入显示管线

import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper';
import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';

const mapper = vtkMapper.newInstance();
mapper.setInputConnection(iso.getOutputPort());
mapper.setScalarVisibility(false);
const actor = vtkActor.newInstance();
actor.setMapper(mapper);
actor.getProperty().setColor(0.2, 0.8, 0.9);
renderer.addActor(actor);
renderer.resetCamera();
renderWindow.render();

示例关掉标量着色,使用统一的 Actor 颜色,因为关注的是几何变化。若要用数据给等值面着色,需要确认 Filter 输出中有哪些数组以及它们位于 Point Data 还是 Cell Data,不能假定原始体数组自动完整附着到结果上。第 14 章讲过 Mapper 的标量选择。

24.4 vtkClipPolyData:空间平面真正切掉单元

vtkClipPolyData 输入为 vtkPolyData,输出仍为 vtkPolyData。在本例中,用 vtkPlane 提供隐式函数。平面过点 p0,法向量为 n,计算 f(p)=n·(p−p0);f=0 是平面,正负号区分两侧。Filter 在跨越平面的多边形边上插入交点,并输出保留部分。

import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane';
import vtkClipPolyData from '@kitware/vtk.js/Filters/Core/ClipPolyData';

const plane = vtkPlane.newInstance({
  origin: [0, 0, 0],
  normal: [1, 0, 0],
});
const clip = vtkClipPolyData.newInstance({ clipFunction: plane });
clip.setInputConnection(iso.getOutputPort());

const clippedMapper = vtkMapper.newInstance();
clippedMapper.setInputConnection(clip.getOutputPort());
const clippedActor = vtkActor.newInstance();
clippedActor.setMapper(clippedMapper);
renderer.addActor(clippedActor);

在本教程的 37.4.2 实现中,默认 insideOut=false 保留 f(p)>value 一侧。上例 normal=[1,0,0]、origin=[0,0,0]、默认 value=0,即主要保留 x>0 一侧;平面正好穿过的单元通过插值生成新的边缘点。调用 clip.setInsideOut(true) 则保留相反侧。clip.setValue(数值) 改变在隐式函数数值轴上的阈值;在单位法向量的平面例子里,这相当于平移裁剪界线,但移动平面原点通常更直观:

plane.setOrigin(4, 0, 0); // 现在以 x=4 为裁剪界线
const kept = clip.getOutputData();

这与相机 near/far clipping 不同。相机裁剪影响画面中哪些片段被绘制,不把网格改成新的 vtkPolyData;vtkClipPolyData 的输出可进一步处理或保存。切开封闭等值面后,新边缘通常是敞开的,本 Filter 不自动为截面封盖。阶段示例界面的橙色线框是未裁剪原表面,蓝色实体是裁剪结果。

橙色完整等值面线框与蓝色裁剪后网格

24.5 两个输出与结果检查

主输出端口 0 是保留侧。若还需要另一侧,可先启用:

clip.setGenerateClippedOutput(true);
const kept = clip.getOutputData(0);
const removed = clip.getClippedOutput(); // 第二个输出,即端口 1

如果未启用 generateClippedOutput,不应把第二输出当作已经生成的另一半。这个选项可能增加计算和内存负担。Filter 也有 generateClipScalars 等配置;本例不需要用平面距离覆盖/生成显示标量,保持默认关闭即可。

检查时先看数据类型与非空性:

const before = iso.getOutputData();
const after = clip.getOutputData();
console.log(before.getNumberOfPoints(), before.getNumberOfCells());
console.log(after.getNumberOfPoints(), after.getNumberOfCells());

不要只用“裁剪后单元数必须小于原来”作断言:一个原三角形被平面切开并重新三角化后,局部可能分成多个三角形。更可靠的检查是观察保留侧的空间范围、跨平面边缘、insideOut 前后的侧别,以及输出是否为空。若阈值使等值面本来为空,后续裁剪也只会得到空结果。若平面根本不穿过表面,可能全部保留或全部去掉。

24.6 更新、代价与常见错误

移动平面会改变 vtkPlane 的修改时间,裁剪器可据此更新;改变等值面阈值会先重算等值面,再重算裁剪结果。拖动滑块时连续重算可能昂贵;对于大体积数据,可在松开滑块时再算、降低预览分辨率,或将耗时工作移到合适的后台计算流程。setInputConnection() 描述依赖关系,不能代替性能规划。

常见错误是:输入没有 active Point Data 标量;把 vtkImageData 直接交给只接受 PolyData 的裁剪器;把等值面 Filter 的输出端口误当成已渲染对象;改变标量数组后未通知数据对象;或认为“屏幕里被挡住”就是几何已经被裁掉。调试时依次打印输入 vtkClass、数组名和范围、等值面点/单元数、裁剪后点/单元数,再处理 Mapper 与相机。

本章小结

vtkImageMarchingCubes 把规则三维标量场中的一个阈值边界提取为表面;vtkClipPolyData 在表面上按空间条件真正切出新几何。两者都是数据变换对象,可以串在 Reader 与 Mapper 之间。你现在能够从磁盘数据走到数组选择、过滤结果和可见场景,并能在每一步核对实际的数据对象。