三维模型预处理中的一个关键步骤是空间归一化。不同来源的CAD模型、扫描点云或网格体,原始尺寸和中心位置差异很大,直接输入网络会导致训练不稳定。归一化的目的是把模型变换到一个统一参考空间,最常见的目标是单位立方体或单位球。可是归一化有两种常见做法,一种是对三个坐标轴分别缩放,另一种是三个轴统一缩放。前者实现简单,容易把模型严丝合缝地放入目标范围,但会引入各向异性变形,改变原始几何比例。

各向异性缩放为什么会让模型变形
各向异性缩放的“各向异性”指的是缩放变换对不同方向使用不同因子。假设一个模型的包围盒在X轴方向长度为2.0,在Y轴方向长度为1.0,在Z轴方向长度为0.5。如果想把三个轴都映射到[-0.5, 0.5]区间,逐轴归一化会给X轴乘以0.5,给Y轴乘以1.0,给Z轴乘以2.0。这样得到的模型在三个方向的范围完全一致,但原始比例2:1:0.5变成了1:1:1。
下面这段代码是典型的逐轴归一化写法:
import numpy as np
def anisotropic_normalize(vertices):
"""绕包围盒中心逐轴缩放,范围统一但会破坏比例。"""
min_coords = vertices.min(axis=0)
max_coords = vertices.max(axis=0)
center = (min_coords + max_coords) / 2.0
size = max_coords - min_coords
vertices_centered = vertices - center
vertices_scaled = vertices_centered / size
return vertices_scaled
这段代码中,size是一个包含三个分量的向量,分别保存X、Y、Z方向的包围盒长度。由于NumPy的广播机制,vertices_centered / size实际上对每个顶点坐标的三个分量执行了不同的除法。模型的长边被压缩,短边被拉伸。对于分类任务来说,这种变形可能让同一类别的物体在几何上失去一致性。例如长条形零件与方形零件经过逐轴归一化后,外形差异被人为缩小,网络更难找到稳定特征。
更隐蔽的问题是,各向异性缩放不仅改变顶点位置,还会改变局部几何关系。点云中邻域点的距离在不同方向上被不同程度放大,法向量方向也会偏离原始值。后续如果使用图卷积、PointNet++等依赖局部坐标或距离的网络,输入空间的扭曲会直接传递到特征计算中。因此,除非任务明确要求把形状强行填充到固定体积,否则预处理阶段应尽量避免逐轴独立缩放。
比例保持约束的核心实现
比例保持约束要求缩放矩阵是一个标量乘以单位矩阵,即三个坐标轴使用完全相同的缩放系数。实现上并不复杂,通常取包围盒三个尺寸中的最大值作为缩放基准。假设目标尺寸为1.0,最长边为max_extent,那么缩放系数就是scale = 1.0 / max_extent。三个轴都乘以这个系数,模型整体被缩放,但任意两点之间的距离比保持不变。
import numpy as np
def isotropic_normalize(vertices, target_size=1.0):
"""使用统一缩放系数,保持模型原始长宽高比例。"""
min_coords = vertices.min(axis=0)
max_coords = vertices.max(axis=0)
center = (min_coords + max_coords) / 2.0
size = max_coords - min_coords
max_extent = np.max(size)
scale = target_size / max_extent
vertices_centered = vertices - center
vertices_scaled = vertices_centered * scale
return vertices_scaled
这段代码与逐轴版本的关键区别在于,它不再除以size向量,而是除以一个标量max_extent。缩放后的模型最长边长度正好等于target_size,其他两条边则按照原始比例缩放,可能小于目标范围。这样处理之后,模型不会填满整个单位立方体,但形状保持不变。如果需要固定输入尺寸,可以对空白区域做零填充,而不是继续拉伸短边。
批量训练时还要考虑全局缩放一致性。如果每个样本独立使用自己的max_extent计算缩放系数,那么小模型与大模型在输入空间中仍然可能具有不同尺度。对于需要保留物理尺寸或相对大小的任务,应该先在训练集上统计一个全局缩放系数。例如计算所有样本包围盒对角线的最大值或分位数,然后所有样本统一使用这个系数缩放。下面是一个简单示例:
import numpy as np
def compute_global_scale(all_vertices):
"""根据整个数据集的最大包围盒对角线计算全局缩放系数。"""
max_diag = 0.0
for verts in all_vertices:
min_coords = verts.min(axis=0)
max_coords = verts.max(axis=0)
diag = np.linalg.norm(max_coords - min_coords)
if diag > max_diag:
max_diag = diag
return 1.0 / max_diag
这种全局缩放方式将所有模型映射到同一个尺度基准下,避免不同样本之间因为局部归一化而产生尺度漂移。在点云分类或CAD检索中,全局缩放通常比逐样本缩放更稳定。
点云、体素与网格预处理的差异
不同数据表示对比例保持的要求有所不同。点云数据通常以N×3的坐标矩阵表示,预处理流程包括中心化、缩放和采样。中心化一般使用包围盒中心或点云质心。缩放阶段如果采用各向同性缩放,点云形状稳定;如果使用各向异性缩放,点云在某个方向上会被过度拉伸,导致局部邻域结构扭曲。对于PointNet这类直接对坐标做MLP的网络,各向异性影响相对有限;但对于依赖K近邻和局部几何的网络,比例保持尤其重要。
体素表示的处理更为特殊。体素化之前需要将模型缩放到固定分辨率网格中。有些实现为了充分利用体素空间,会把模型沿三个轴分别缩放到填满网格,这实际上就是各向异性缩放。这种处理对某些体素分类网络可能无害,因为3D卷积可以从体素占用模式中学习特征,但会丢失真实比例信息。如果任务涉及尺寸估计或形状生成,建议在体素化时保留比例,将模型放入一个固定大小的容器中,空余体素填零。
网格数据除了顶点位置,还有法线信息。各向异性缩放下,法线变换需要使用逆转置矩阵;而各向同性缩放只需要对法线做长度归一化,实现更简单。如果预处理代码中只对顶点做逐轴缩放却忘记正确处理法线,渲染或几何计算会出现明显错误。因此,对于网格输入,比例保持还能减少法线变换出错的风险。
实际项目中,比例保持与数据增强并不冲突。在完成统一缩放之后,可以对三个轴施加非常小的随机扰动,模拟轻微的非刚性变形。例如在整体缩放系数基础上,给每个轴乘以0.95到1.05之间的随机数。这种扰动会引入有限各向异性,但幅度受控,可以提升模型对形状微小变化的鲁棒性。关键是主归一化仍然以比例保持为基础,而不是完全依赖逐轴缩放。
验证预处理代码是否引入意外变形
判断预处理管线是否保持了比例,最直接的方法是构造一个已知比例的长方体。例如生成长度为4、宽度为2、高度为1的八个顶点,分别输入归一化函数,再计算输出包围盒三个方向的长度。如果输出尺寸比例仍然是4:2:1,说明缩放正确;如果比例变成1:1:1或其他值,则说明代码中存在逐轴缩放。这个测试虽然简单,但能提前暴露很多隐蔽问题。
import numpy as np
def test_isotropic_scale():
box = np.array([
[-2.0, -1.0, -0.5],
[ 2.0, -1.0, -0.5],
[ 2.0, 1.0, -0.5],
[-2.0, 1.0, -0.5],
[-2.0, -1.0, 0.5],
[ 2.0, -1.0, 0.5],
[ 2.0, 1.0, 0.5],
[-2.0, 1.0, 0.5]
])
normalized = isotropic_normalize(box)
out_size = normalized.max(axis=0) - normalized.min(axis=0)
print(out_size / out_size.max())
# 期望输出接近 [1.0, 0.5, 0.25]
另一个可量化的指标是缩放矩阵的条件数。如果缩放变换表示为对角矩阵diag(sx, sy, sz),其条件数等于最大缩放因子与最小缩放因子的比值。各向同性缩放的条件数为1;逐轴缩放的条件数越大,形状扭曲越严重。在预处理代码中打印或记录每个样本的条件数,可以帮助监控数据质量。对于比例保持管线,条件数应始终接近1,除非有意做数据增强。
训练过程中还可以从数据分布角度观察。记录每个batch输入模型在三个轴上的尺寸均值与方差,如果某个轴的方差异常偏高,可能意味着数据集中混入了经过不同缩放策略处理的样本。此时需要回到预处理源头排查,而不是在网络层面强行修正。预处理中的微小变形会随着网络层数增加而被放大,尽早发现可以节省大量调试时间。
预处理看似只是几个坐标变换,却决定了模型输入空间的质量。各向异性缩放与比例保持约束之间的选择,本质上是空间利用率与几何保真度之间的权衡。对于大多数以几何特征为核心的任务,优先保证比例稳定通常是更稳妥的做法。与其让网络去适应被扭曲的输入,不如在源头守住形状的一致性。