如何让 vtkSampleFunction 仅计算稀疏掩码位置的法线?
结论
vtkSampleFunction 没有公开接口可以接收任意稀疏 mask,只在选中位置计算法线。原代码中的 [mask] 发生在 sampler.Update() 之后,只能筛选结果,不能减少此前的采样计算。vtkSampleFunction 接口文档
如果只需要这些位置的法线,可以跳过整个网格的采样,直接在 mask 对应的空间坐标处调用 implicit.FunctionGradient(),再归一化并调整方向。
如何得到与 sampler 一致的法线
在核对过的 VTK 实现中,vtkSampleFunction 先调用 FunctionGradient(),将梯度归一化后取负值,再写入法线数组。因此,复现其输出时应使用:
[ \mathbf{n}=-\frac{\nabla f}{\lVert\nabla f\rVert} ]
梯度长度为零时保留零向量。负号会影响法线方向,不能直接把归一化梯度当作原来的 sampler 法线。vtkSampleFunction 源码
仅计算 mask 选中的位置
示例假设 mask 是长度为 nx * ny * nz 的一维布尔数组,顺序与原代码中 vtk_to_numpy(...) 返回的点数组一致。
import numpy as np
def masked_sample_normals(implicit, bounds, dims, mask):
nx, ny, nz = map(int, dims)
if min(nx, ny, nz) < 1:
raise ValueError("Sample dimensions must be positive")
mask = np.asarray(mask)
if mask.dtype != np.bool_ or mask.shape != (nx * ny * nz,):
raise ValueError("mask must be a flat boolean point mask")
bounds = np.asarray(bounds, dtype=float).reshape(3, 2)
origin = bounds[:, 0]
dimensions = np.array([nx, ny, nz])
spacing = np.ones(3, dtype=float)
active = dimensions > 1
spacing[active] = (
(bounds[:, 1] - bounds[:, 0])[active]
/ (dimensions[active] - 1)
)
point_ids = np.flatnonzero(mask)
normals = np.empty((point_ids.size, 3), dtype=np.float32)
gradient = [0.0, 0.0, 0.0]
for row, point_id in enumerate(point_ids):
i = int(point_id % nx)
j = int((point_id // nx) % ny)
k = int(point_id // (nx * ny))
point = origin + spacing * np.array([i, j, k])
implicit.FunctionGradient(point.tolist(), gradient)
g = np.array(gradient, dtype=float)
length = np.linalg.norm(g)
if length > 0.0:
g /= length
normals[row] = -g
return normals
# implicit、bounds、nx、ny、nz 使用原来的值。
# 用这次调用替换完整采样流程,无须先执行 sampler.Update()。
normals = masked_sample_normals(
implicit, bounds, (nx, ny, nz), mask
)
返回数组的形状为 (mask 中 True 的数量, 3),行顺序与原来的一维布尔索引顺序一致。示例只构造选中位置的法线,不创建完整的距离场或法线场。
使用 FunctionGradient() 可以沿用 vtkImplicitFunction 的变换处理。如果设置过 SetTransform(),直接调用底层 EvaluateGradient() 可能无法保持相同语义。vtkImplicitFunction 文档
索引和采样位置需要注意什么
VTK 在这里按 x 轴最快、其次 y 轴、最后 z 轴的顺序排列点,即:
[ \mathrm{pointId}=i+n_x(j+n_y k) ]
如果布尔掩码的形状是 (nz, ny, nx),可以用 mask.ravel(order="C") 转成示例需要的一维数组。如果形状是 (nx, ny, nz),则应先使用 mask.transpose(2, 1, 0),再按 C 顺序展平。
原代码读取的 GetPointData().GetNormals() 对应网格采样点。如果 mask 标记的是单元中心,就需要根据单元中心构造坐标,不能直接套用这些 pointId。
性能和方向上的限制
对于稀疏 mask,这种做法将梯度查询次数从全部采样点数减少到选中点数。不过,Python 循环有调用开销,实际是否更快取决于掩码密度、网格规模和所用 VTK 实现,不能保证固定的加速比例。选中点很多时,可以考虑在 C++ 中实现相同的遍历逻辑。
如果其他步骤仍然需要完整距离场,可以保留 sampler 并调用 ComputeNormalsOff(),再单独计算选中位置的法线。这样只能省去完整法线场的计算,完整标量场仍然需要采样。
示例中的负号用于匹配 vtkSampleFunction 的输出。如果需要距离增大的方向,应使用归一化后的正梯度。对于 vtkImplicitPolyDataDistance,距离符号依赖输入表面的方向信息;开放或方向不一致的网格需要单独检查方向是否符合业务预期。vtkImplicitPolyDataDistance 文档
备注:内容仅供参考。
