三维网格或点云文件中的顶点坐标,通常以建模软件的局部坐标系保存,坐标范围可能从几千到零点几毫米不等。如果不做任何处理,把这些原始坐标直接喂给神经网络,损失函数会被少数离群点支配,梯度更新也会变得不稳定。中心化、归一化和缩放规范化就是用来消除这些几何偏移与尺度差异的基础操作,它们不改变模型形状,但能显著影响训练速度和最终精度。

为什么原始坐标会拖累训练
3D模型通常来自不同的建模工具、扫描设备或参数化生成脚本。一个汽车模型可能用米做单位,一个机械零件却用毫米甚至微米表示。即使单位相同,同一个数据集中,有的模型靠近原点,有的模型边界框中心偏移到几百个单位之外。这种分布差异会让网络第一层的权重必须同时适应大范围平移和不同尺度,优化器很难找到合适的步长。
以点云分类网络为例,输入是 (N, 3) 的坐标矩阵。如果 x、y、z 坐标均值不为零,卷积或注意力模块中的位置编码会把偏移量当作有效特征;如果坐标范围过大,激活函数如 Sigmoid、Tanh 容易进入饱和区,ReLU 虽然不会饱和,但大数值会导致梯度爆炸。更隐蔽的问题是,尺度不一致会让数据增强中的随机旋转、抖动失去可比性,因为同样旋转 10 度对 100 单位长的模型和对 0.1 单位长的模型影响完全不同。
网格、体素和点云表示对预处理的需求并不完全一样。网格顶点可以直接应用变换,但面片法线需要随旋转矩阵变化,而不是随平移或缩放;体素化之前通常需要先做缩放,否则小模型可能只占据几个体素,大模型则超出体素边界。因此,预处理方法要根据下游任务和表示形式来设计。
三类基础预处理操作的具体实现
中心化解决的是平移问题。常见做法有两种:用所有顶点坐标的均值作为质心,或者用包围盒最小角与最大角的平均值作为几何中心。质心对顶点密度敏感,如果模型某个区域采样点特别密集,质心会偏向该区域;包围盒中心只依赖边界,更稳定,但容易受离群点影响。实践中可以先剔除明显离群点,再计算包围盒中心。
import numpy as np
def center_by_centroid(vertices):
# vertices: (N, 3)
centroid = np.mean(vertices, axis=0)
return vertices - centroid, centroid
def center_by_bbox(vertices):
min_corner = np.min(vertices, axis=0)
max_corner = np.max(vertices, axis=0)
center = (min_corner + max_corner) / 2.0
return vertices - center, center
中心化之后,模型原点对齐,但尺度仍然可能差异巨大。归一化通常指把模型缩放到一个统一范围,比如单位球、单位立方体或标准差为 1 的分布。按最大距离归一化到单位球,适合旋转不变的任务,因为球体内所有方向对称;按包围盒最大边长归一化到单位立方体,适合体素化任务,能保证模型完整落在固定分辨率网格内;按标准差归一化更侧重统计分布,在点云自编码器中常见。
def normalize_max_radius(vertices):
# 中心化后,按最大欧氏距离缩放到单位球
distances = np.linalg.norm(vertices, axis=1)
max_distance = np.max(distances)
if max_distance < 1e-8:
return vertices, 1.0
scale = 1.0 / max_distance
return vertices * scale, scale
def normalize_std(vertices):
std = np.std(vertices, axis=0)
std[std < 1e-8] = 1.0
return vertices / std, std
缩放规范化比普通归一化多一层业务约束。例如在三维重建中,需要把模型缩放到真实物理尺寸;在体素分类中,需要根据体素分辨率把模型调整到固定网格大小,比如让最大边长对应 32 个或 64 个体素。缩放比例可以来自包围盒对角线、最大边长或数据集统计量。下面代码展示了单位立方体和体素对齐两种策略。
def scale_to_unit_cube(vertices, target_size=1.0):
min_corner = np.min(vertices, axis=0)
max_corner = np.max(vertices, axis=0)
center = (min_corner + max_corner) / 2.0
vertices_centered = vertices - center
size = max_corner - min_corner
max_extent = np.max(size)
if max_extent < 1e-8:
return vertices_centered, 1.0
scale = target_size / max_extent
return vertices_centered * scale, scale
def scale_to_voxel_grid(vertices, voxel_size=0.05):
min_corner = np.min(vertices, axis=0)
max_corner = np.max(vertices, axis=0)
center = (min_corner + max_corner) / 2.0
vertices_centered = vertices - center
size = max_corner - min_corner
max_extent = np.max(size)
if max_extent < 1e-8:
return vertices_centered, voxel_size
scale = 1.0 / max_extent * (max_extent / voxel_size)
return vertices_centered * scale, scale
如果数据在 GPU 上以批量张量组织,可以用 PyTorch 实现同样逻辑。注意 torch.norm 在维度上的写法,以及避免除零的 clamp 操作。批量处理时不要对每个样本单独循环,尽量利用张量广播。
import torch
def normalize_tensor(points):
# points: (B, N, 3) 或 (N, 3)
centroid = torch.mean(points, dim=-2, keepdim=True)
points_centered = points - centroid
max_dist = torch.max(
torch.norm(points_centered, dim=-1, keepdim=True),
dim=-2,
keepdim=True
).values
max_dist = torch.clamp(max_dist, min=1e-8)
points_normalized = points_centered / max_dist
return points_normalized, centroid, max_dist
不同策略对训练结果的影响
单位球归一化在点云分类和分割任务中非常流行,因为它不依赖模型朝向,能保留旋转不变的特性。但单位球会让所有模型的最大半径变成 1,小模型被放大、大模型被缩小,这对需要理解相对尺寸的任务可能有害。单位立方体则保持包围盒比例,但如果模型某一维度特别长,其他维度信息会被压缩得更厉害。
体素对齐策略和网络输入分辨率强相关。例如要输入 32x32x32 的体素网格,可以先把模型最大边长缩放到 32 个体素对应长度,再体素化。这样不同物体在网格中占比一致,但可能损失绝对尺度。有人在分类任务中对比过单位球和单位立方体,发现单位球收敛更快但容易过拟合细小局部特征,单位立方体对条形物体更友好。
法线处理是另一个容易忽略的点。顶点坐标做非均匀缩放时,法线不能直接用同一个缩放矩阵,否则光照和几何特征会变形。正确做法是根据变换矩阵的逆转置矩阵重新计算法线,或者在缩放后从三角面片重新估计法线。如果只是做均匀缩放,法线方向不变,长度可以通过归一化恢复。
数据增强中的随机旋转、抖动和缩放通常放在这些预处理之后。先做固定预处理,再施加随机增强,可以让增强参数的范围有明确物理意义。比如随机缩放 0.9 到 1.1,在单位球归一化后表示约 10% 的尺寸扰动,不会因为原始模型尺度差异而产生完全不同级别的扰动。
工程实现中的常见问题
信息泄漏是预处理阶段最典型的坑。中心、缩放因子和标准差必须在训练集上计算,然后应用到验证集和测试集。如果对全量数据先做统计再划分,验证集等于偷偷看到了测试分布,评估指标会虚高。在线推理时,单个模型没有全局统计量,可以用训练阶段保存的均值和缩放因子,或者对单模型使用其自身包围盒,但要保证和训练时逻辑一致。
退化网格和空模型也要处理。某些 CAD 文件可能只包含边或者没有三角面片,顶点数量为零或所有顶点共线。此时 max_distance 可能为零,除零会让整个批次出现 NaN。代码里的阈值判断不能省,遇到退化数据应该丢弃或赋予默认单位变换。批量训练时还要注意不同样本的顶点数量不同,如果使用 DataLoader 默认的拼接方式,中心化和归一化要在 collate_fn 里逐样本完成。
另一个实际问题是缓存。预处理后的数据如果每次都重算,会拖慢数据流水线。可以离线计算并保存为 .npy 或 .npz 文件,训练时直接加载;也可以在 Dataset 类里做一次缓存。缓存时保存的是预处理后的坐标和对应变换参数,方便后续数据增强和可视化还原。不要把原始文件和预处理文件混在一起,否则版本管理容易出错。
最后,预处理不是越多越好。过度的缩放和标准化可能破坏模型原有的几何比例,比如人体姿态估计中如果把每个骨骼链都缩放到统一长度,关节角度反而难以学习。建议根据任务先做可视化检查,随机抽取预处理后的模型用三维渲染确认形状和朝向是否合理,再进入正式训练。