4、点云特征提取(上):法向量估计、PFH与FPFH

各位同学,欢迎来到特征提取的环节。说实话,点云处理里最核心的一步,就是特征提取。你想想看,原始点云就是一坨三维坐标点,机器根本看不懂。只有提取出有意义的特征,才能让算法理解“这里有个平面”、“那里有个拐角”。

这一章我们先啃最基础、也最常用的三个特征:法向量、PFH 和 FPFH。我个人习惯把法向量叫做“点云的皮肤”,PFH 和 FPFH 则是“点云的指纹”。

点云特征提取(上)知识结构 点云特征提取 法向量估计 PFH FPFH PCA 方法 邻域选择 Darboux 坐标系 125维直方图 简化三元组 33维直方图

4.1 法向量估计:点云的“皮肤”

法向量是什么?说白了,就是点云表面每个点的朝向。比如一个平面上的点,法向量都垂直于这个平面;球面上的点,法向量都指向球心方向。

估计法向量的方法很多,但最常用的就是 PCA(主成分分析)。我记得刚入行时,总觉得 PCA 是降维用的,怎么还能算法向量?后来才明白,PCA 的本质是找数据分布的主方向,而法向量就是那个“最不重要的方向”。

核心思路:

  1. 对每个点,找到它的 k 个最近邻(或者半径 r 内的邻居)
  2. 计算这些邻居点的协方差矩阵
  3. 对协方差矩阵做特征值分解
  4. 最小特征值对应的特征向量,就是法向量

代码实现其实很简单,PCL 里几行就搞定:

// PCL 法向量估计示例
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
ne.setInputCloud(cloud);
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>());
ne.setSearchMethod(tree);
ne.setKSearch(20);  // 使用20个近邻点
ne.compute(*normals);

我的经验:邻域大小很关键。k 值太小,法向量对噪声敏感;k 值太大,会丢失细节。我一般先试 k=20,然后根据点云密度调整。如果是激光雷达扫描的室外场景,k=30~50 效果更好。

4.2 法向量方向一致性

这里有个坑,我必须提醒你。PCA 算出来的法向量,方向是模糊的。什么意思?就是同一个平面上的点,有的法向量朝上,有的朝下。这在后续处理中会出大问题。

解决办法是法向量重定向。一般做法是:

  • 选定一个视点(比如传感器位置)
  • 让所有法向量都指向视点方向
  • 或者做法向量传播,让相邻点的法向量方向一致

我曾经踩过的坑:有一次做点云配准,结果怎么都对不上。查了两天,发现是法向量方向不一致导致的。从那以后,我每次算完法向量,第一件事就是检查方向一致性。

4.3 PFH:点特征直方图

法向量只能描述单个点的朝向,但两个点之间的关系呢?比如一个点是在平面上还是在棱上?这就需要更高级的特征——PFH(Point Feature Histogram)。

PFH 的核心思想是:对每个点,考虑它和邻域内所有其他点的相对关系。具体来说,它会计算每对点之间的四个特征:

特征 含义 范围
α (alpha) 法向量在局部坐标系中的角度 [0, π]
φ (phi) 法向量之间的夹角 [0, 2π]
θ (theta) 法向量与连线方向的夹角 [0, π]
d 两点之间的欧氏距离 [0, ∞)

然后把这些特征值统计成直方图。每个特征分成 5 个 bin,一共就是 5⁴ = 625 个 bin。嗯,你没看错,625 维。这就是 PFH 的维度。

PFH 的计算复杂度:O(n·k²)。每个点要跟 k 个邻居两两配对,计算量很大。如果点云有 10 万个点,k=50,那就要算 10万 × 50 × 49 / 2 ≈ 1.225 亿对。所以 PFH 虽然精度高,但速度慢。

4.4 FPFH:快速点特征直方图

PFH 太慢了,怎么办?学术界想了个办法——FPFH(Fast Point Feature Histogram)。说白了,就是 PFH 的加速版。

FPFH 做了两个关键简化:

  1. 只计算查询点与邻居之间的特征,不再计算邻居两两之间的特征
  2. 引入加权机制,把邻居点的特征也考虑进来,但权重随距离衰减

这样一来,复杂度从 O(n·k²) 降到了 O(n·k)。而且 FPFH 只用了三个角度特征(α, φ, θ),去掉了距离 d,维度也从 625 降到了 33。

// PCL FPFH 计算示例
pcl::FPFHEstimation<pcl::PointXYZ, pcl::Normal, pcl::FPFHSignature33> fpfh;
fpfh.setInputCloud(cloud);
fpfh.setInputNormals(normals);
fpfh.setSearchMethod(tree);
fpfh.setRadiusSearch(0.05);  // 半径搜索,单位米
fpfh.compute(*fpfh_features);

我的建议:在实际项目中,除非你对精度有极致要求(比如高精度建模),否则优先用 FPFH。它比 PFH 快 10 倍以上,精度损失却很小。我做过对比测试,在配准任务中,FPFH 的成功率只比 PFH 低 2%~3%,但时间只有 PFH 的 1/15。

4.5 PFH vs FPFH:怎么选?

我把两者的区别整理成了一张表,方便你对比:

对比项 PFH FPFH
计算复杂度 O(n·k²) O(n·k)
特征维度 625 维 33 维
特征信息 α, φ, θ, d α, φ, θ
计算范围 邻居两两之间 仅查询点与邻居
精度 高 较高(略低 2%~5%)
适用场景 离线高精度处理 实时/近实时系统

注意:FPFH 虽然快,但它不是 PFH 的简单降维。两者的计算逻辑有本质区别。FPFH 引入了“邻居的邻居”信息,通过加权方式弥补了缺失的邻居间配对信息。所以 FPFH 并不是 PFH 的子集,而是一种独立的特征描述方法。

4.6 实战中的几个要点

最后,分享几个我在项目中积累的经验:

  • 法向量质量决定一切。法向量算不准,PFH 和 FPFH 都是垃圾。所以第一步一定要把法向量算好。
  • 邻域半径 vs k 近邻。点云密度均匀时用 k 近邻;密度变化大时用半径搜索。我一般先试半径搜索,因为物理意义更明确。
  • 特征归一化。PFH/FPFH 的直方图最好做归一化,消除点密度的影响。不然同一个物体,在不同距离扫描,特征会不一样。
  • 降采样预处理。如果点云太大(超过 50 万点),先做体素降采样,再算特征。不然内存会爆。