ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

三维点云配准利器:SHOT特征描述子原理与工程实践

三维点云配准利器:SHOT特征描述子原理与工程实践

做三维点云的人,十有八九会撞上同一个问题:两片不同视角采集的同一物体,肉眼一看就知道是同一个东西,算法却死活匹配不上。原因其实不复杂——三维空间里没有统一的“朝向”,同一个零件换个角度再扫描一次,坐标系里的数值全变了。这时候就得靠特征描述子去抹平视角差异,把关键点周围的几何结构转换成一个不随旋转变化的向量。SHOT特征描述子,全称Signature of Histograms of Orientations,就是这个领域里一个非常经典的选择,从2010年前后被提出到现在,十几年过去,很多三维配准、目标识别流程里依然能看到它的身影。

这篇文章不会去逐行推导论文公式,而是站在一个做点云算法的人的角度,把SHOT的每个关键设计拆开讲清楚:它为什么能旋转不变?局部参考坐标系到底在干什么?352维的特征向量是怎么算出来的?以及在真实项目里,哪些参数最影响效果、哪些坑最容易被踩到。无论你是第一次接触三维特征描述子,还是已经在点云任务里摸爬滚打过一阵,这篇应该都能提供一些可以直接落地的思路。

1. 三维点云匹配的底层难题:从“找特征点”到“给特征点做指纹”

1.1 为什么点云不能像图像那样直接对比

图像匹配可以粗暴地拿像素格子对齐,因为图像本身有固定的上下左右朝向;但点云没有这种“天然坐标系”。同一个平面,在扫描器A的视角下法向量是(0,0,1),换到扫描器B的视角下可能就变成了(0.7,0.2,0.6)。你要让程序认出它们是同一个位置,就必须设计一种表达方式,让这个位置在任何一个坐标系下都能算出同一个“签名”。

这里的“签名”就是特征描述子。在三维视觉里,我们通常先用关键点检测器挑出一些几何上有辨识度的点,比如角点、棱边交点、曲率剧烈变化的地方,然后再给每个关键点计算一个描述子。这个描述子负责把关键点周围一片局部区域的几何信息浓缩成一个数值向量。匹配的时候,就看两片点云里哪些关键点的描述子向量最像。

如果把整个流程比作给物体做身份登记,那关键点检测相当于决定“在脸上取哪几个位置做标记”,SHOT这种描述子算法就是“对每个标记位置采集一枚独一无二的指纹”。指纹必须稳定——同一个物理位置,不管从哪个角度按下去,印出来的纹路都应该基本一致。SHOT这类局部描述子之所以能胜任,核心在于它自带一套“局部坐标系”,这套坐标系完全由点云局部几何自己定义,全局旋转它跟着旋,于是特征在坐标系下的表示就不变了。

1.2 局部描述子和全局描述子的分工

三维描述子大致分两派:全局描述子和局部描述子。

全局描述子把整个物体或者整片场景当作一个整体,算出一个向量来描述它的轮廓或形状分布。好处是区分度很高,两个完全不同形状的物体,描述子差异巨大;代价是它对遮挡非常敏感,桌上一堆杂物里要识别出目标物体,全局描述子基本就废了,因为场景里混杂了太多无关结构。

局部描述子则只关心关键点周围一小块区域,比如半径几厘米的球。哪怕整个场景有一半被遮挡,只要关键点附近的几何没有被完全盖住,描述子就能保持稳定。SHOT属于局部描述子,这也决定了它的主要应用场景:三维配准、三维目标识别、机器人抓取前的物体定位,这些任务里遮挡和杂乱背景是常态,局部描述子天然比全局描述子更能扛。

2. SHOT的第一块地基:局部参考坐标系(LRF)是怎么立稳的

2.1 从协方差矩阵开始的三个坐标轴

SHOT的旋转不变性不是靠什么花哨技巧,而是靠一套严谨的局部参考坐标系(Local Reference Frame,简称LRF)。这套坐标系一旦在关键点周围立起来,后续所有统计工作都在这个坐标系里进行,全局旋转自然就被“吸收”掉了。

构建LRF的第一步,是收集以关键点p为球心、半径r为球内的所有邻域点。对这批邻域点计算协方差矩阵,假设共有n个邻域点,第i个点的坐标为p_i,则:

M = (1/n) Σ (p_i - p)(p_i - p)^T

这个3×3的对称矩阵描述了邻域点的空间分布。对M做特征值分解,会得到三个特征值和三个对应的特征向量。特征值大小代表该方向上的数据方差大小,所以最大特征值对应的特征向量,就是邻域点分布最“展开”的方向,用它作为LRF的X轴。次大特征值对应特征向量作为Y轴,最小作为Z轴,理论上这就是一个完整的坐标系。

但事情没这么简单。特征值分解得到的特征向量存在一个符号不确定的问题:v和-v都是合法的特征向量,到底选哪个方向当正方向?这就像给你一把没有指向标记的尺子,你可以正着拿也可以反着拿,但最后量出来的数值完全是两套。如果不同视角下同一关键点的X轴方向发生翻转,描述子也就对不上了。

2.2 符号消歧:靠“少数服从多数”定方向

SHOT作者解决符号不确定性的方案非常朴素:让邻域点分布投票。对候选的X方向v,统计邻域点中满足(p_i - p)与v点乘大于0的个数。如果多数点在v这一侧,就保留v;否则取反,用-v当X轴。这样一个全局旋转后,虽然邻域点坐标变了,但它们的相对分布关系没变,投票结果跟着旋转,选出来的X轴方向也一起旋转,从而保持一致性。

得到X轴后,Z轴的选择也有一条讲究的路径:把邻域点的法向量取平均,得到平均法向量n_avg,然后用X轴与n_avg做叉积得到Z轴的候选方向,再做一次同样的符号消歧。最后用Z轴叉乘X轴得到Y轴。整个LRF就完整了。

我这里想强调一个工程上的细节:很多第一次看SHOT的人会以为Z轴就是关键点的法向量。实际上论文里用的是“X轴与平均法向量的叉积”,不直接把法向量当坐标轴。原因在于,法向量本身也存在方向翻转问题,直接用它当Z轴会引入不确定性;而叉积得到的轴天然与X轴正交,再配合消歧步骤,整个坐标系的稳定性会好很多。

2.3 LRF的质量就是SHOT的生命线

LRF构建得好不好,直接决定了SHOT描述子的质量上限。邻域半径r如果选得比物体特征尺度小很多,协方差矩阵就只覆盖了一块几乎平坦的表面,特征值混乱,坐标轴方向会被噪声统治。反过来,r选得太大,邻域里融入了太多背景点,坐标轴会偏向场景中的强结构,导致同一个关键点在两片视角下的参考系产生明显偏差。

我在实际项目里的经验是:r最好取目标物体典型结构尺寸的1/10到1/5。比如说你要匹配一个大约20厘米大小的机械零件,关键部位尺寸在2到4厘米之间,那r取4到6厘米通常是个合理的起点。这个范围既能捕捉到足够的局部几何结构,又不容易把无关背景卷进来。当然,具体数值还得结合点云密度和噪声水平微调,这一点我在后面专门展开。

3. 352维指纹的生成全程:球邻域切分与直方图统计拆解

3.1 把球空间切成格子,在格子里数数

LRF立稳之后,SHOT开始实质性的“指纹采集”。它的思路可以概括成一句话:把关键点周围的球形邻域按照方位角、仰角、径向距离三个维度切成若干个小区域,然后在每个小区域里统计法向量方向的分布,最后把所有小区域的统计结果拼起来。

具体划分方式是:

  • 方位角方向切8份:绕Z轴一圈360°,每45°一份,对应8个扇形区间。
  • 仰角方向切2份:从Z轴正方向到负方向,0°到180°被分成两段,每90°一段。
  • 径向方向切2份:从球心到半径r被切成两圈,内圈和外圈。

三者一组合,就得到8×2×2=32个子空间。每个子空间都是球壳上的一小块“格子”。

关键来了,这些格子的形状不是等体积的——靠近球心的格子小,靠近球壳的格子大,仰角靠近极点的格子也比赤道附近的格子面积小。SHOT不打算对体积差异做补偿,而是把每个格子的直方图单独归一化,这样体积差异带来的点数差异就被吸收掉了。这一设计非常实用,因为点云密度本来就不均匀,硬要按实际体积去加权反而会放大噪声。

3.2 为什么统计的是余弦值而不是角度

给每个子空间分配完邻域点之后,SHOT要统计的是“局部参考坐标系Z轴方向与邻域点法向量之间的夹角”的分布。更准确地讲,它统计的是这两个单位向量点积的分布,也就是夹角的余弦值cosθ。

这里有个容易被忽略的知识点:两个单位向量的点积,就等于夹角余弦值。所以SHOT在实现时根本不需要算任何反三角函数,直接做点积就拿到了统计量,计算效率非常高。更重要的是,余弦函数在θ接近0°时变化非常平缓,对法向量估计的小误差不敏感;而如果直接统计角度值,在0°附近一个微小的法向量扰动就会让数值跳动明显。这属于“算法设计时顺带把昂鲁棒性做进去”的典型例子。

在[-1,1]这个余弦取值区间里,SHOT分成11个等宽区间,也就是每个bin的宽度是2/11≈0.1818。选择一个子空间里所有邻域点的法向量与Z轴的余弦值,投到对应的bin里,就得到这个子空间的直方图。11这个数字是作者实验调出来的平衡点:bin数太少,描述子区分度不够;bin数太多,对法向量噪声又太敏感。32个子空间,每个11个bin,拼在一起就是352维。

3.3 完整数据流:从原始点云到可匹配的特征向量

把上面所有环节串起来,一次完整SHOT特征计算就是下面这个过程:

  1. 输入:原始点云、关键点位置、预设半径r。
  2. 对点云做法向量估计,通常用K近邻或半径邻域做PCA,得到每个点的单位法向量。
  3. 对每个关键点,收集半径r内的邻域点,构建LRF坐标系统(X轴、Y轴、Z轴)。
  4. 把邻域点坐标转换到LRF坐标系下,按方位角、仰角、径向距离划分到32个子空间。
  5. 每个子空间内,计算邻域点法向量与Z轴夹角的余弦值,投到11个bin里,得到局部直方图。
  6. 对每个局部直方图单独做L2归一化,再把32段拼接成352维向量。
  7. 最后对整体向量做一次L2归一化(这一步不同实现有差异,但工程上推荐做)。

完成之后,每个关键点就拿到一个352维的浮点向量。匹配时常见做法是直接算两个向量之间的欧氏距离或者余弦相似度,距离越小说明两个关键点的局部几何越相似。这一步也解释了为什么SHOT对点云密度变化比较敏感需要预处理,因为直方图统计本质上是“数点”,如果一片区域点特别密,另一片区域点特别疏,同样的几何结构会得到不同形状的直方图。所以拿到点云后做一次体素降采样,让密度尽量均匀,往往是让SHOT发挥效果的第一步。

3.4 直观理解:把描述子想象成“花瓣”

352维听起来很抽象,其实可以想象成一朵由32片花瓣组成的花,每片花瓣上的“褶皱”就是11个bin里的数值。旋转物体时,整朵花跟着旋转,但花瓣上的图案不变;平移物体时,花的位置变了,图案还是不变。只有局部几何真正发生变化,比如多了一个遮挡物、一个棱角磨平了,花瓣上的图案才会明显改变。这就是SHOT为什么能被用于识别同一个关键点的核心机制。

4. 项目实测:SHOT的调参思路、同类描述子对比和几个真坑

4.1 一个典型的SHOT配准流程怎么搭

SHOT很少单独用,它永远嵌在更大的pipeline里。我搭过比较顺手的流程是这样:

先对两片待配准点云做体素降采样,把密度拉到基本均匀;然后估计法向量;接着提取关键点,可以用体素格网的中心作为关键点,也可以搭配角点检测算法;对每个关键点计算SHOT描述子;再用最近邻搜索在两片点云的描述子之间找匹配对;之后用RANSAC或类似策略剔除错误匹配,估计一个初始变换;最后用ICP做精配准。

这套流程下来,SHOT最核心的贡献就是给后续步骤提供一批高质量对应关系。如果描述子本身区分度不行,后面RANSAC要迭代很久,甚至直接跑飞。

4.2 邻域半径r的调参逻辑

邻域半径r大概率是SHOT里最需要反复试的参数。我试过的策略是:先用点云平均点间距的10到20倍作为下限,再用目标物体典型特征的尺寸作为上限。

举个例子,如果点云经过降采样后平均点间距约0.5厘米,目标零件上最明显的倒角或棱边宽约3厘米,那r从3厘米试起,逐步加1厘米,观察匹配正确率的变化。一般来说,r偏小会让描述子容易受噪声影响,因为能参与统计的邻域点太少了;r偏大则会让不同关键点的描述子趋于雷同,因为大家都包含了太多相同的背景结构。

另外提醒一句:SHOT描述子不具备尺度不变性。同一个物体在一副点云里占1万个点、在另一副里只占2000个点,如果不提前统一降采样密度,就算r设成同样的值,两个邻域球包含的实际物理范围也可能差很多。所以多尺度对齐是另一个话题,但基础前提是先把两片点云的尺度拉齐。

4.3 真坑一:法向量方向不一致

法向量估计本身就有符号翻转的问题:同一片平面,算法可能在这片区域把法向量朝上,在另一片区域朝下。SHOT的直方图统计用的是法向量与Z轴夹角的余弦值,法向量方向一翻,余弦值就反号,直方图会整个错位。

这个问题在场景里有多个不成气候的平面时特别容易踩到。我碰到过的情况是,一个平面墙和一个桌面,软件重建出来的网格法向量整体一致,但某些孤立的三角形面片法向量突然反了,SHOT描述子就出现了一堆类似“离群点”的向量。

处理办法通常是做法向量重定向:让法向量在局部邻域内保持方向一致,比如从某个种子点开始,让相邻点的法向量点积为正,并向外传播。这个过程做完之后,SHOT的稳定性会明显上一个台阶。有些开源点云库不会自动帮你做这一步,需要自己写逻辑,别偷懒跳过。

4.4 真坑二:均匀密度是直方图类描述子的命门

另一个我经常遇到的坑是点云密度极不均匀。扫描仪离物体近的地方点密,远的地方点疏;重建出来的模型也可能因为视场角关系,出现一侧密一侧疏。SHOT对这种情况的抵抗力比FPFH稍强,因为每个子空间单独归一化,但差距依然有限。

最直接的解法是体素降采样:把空间划成固定大小的格子,每格只保留一个点或者取平均。这个操作能在很大程度上抹平密度差异。还有一招是先把点云转成网格或者深度图,在网格曲面上均匀采样得到密度一致的新点云,再做SHOT。这招的效果最好,但流程重一些,适合离线处理场景。

4.5 和其他描述子的对比与选择

这里给出一份我在项目里常用的宏观对比表,方便大家选型时定位:

描述子空间表达方式维度主要优势常见短板
SHOTLRF+球形网格直方图352旋转稳健、判别力强、抗遮挡好计算量偏大、对密度和法向量质量敏感
FPFH点对角度特征直方图33计算快、内存占用小局部细节表达能力弱一些
3DSC三维形状上下文网格视划分而定对全局形状轮廓有感知对局部细节、旋转鲁棒性略弱

如果任务是实时性要求高的场景,比如移动机器人每帧都要配准,FPFH可能比SHOT更合适,因为33维向量的计算和匹配成本低一个量级;如果任务是把抓取零件从杂乱场景里精确识别出来,SHOT的高判别力优势就很值得。至于3DSC,它对形状轮廓更敏感,适合场景相对简单、结构有清晰轮廓的场合。

我自己的习惯是:不确定选什么时,先跑一个SHOT,再用FPFH做对照。如果FPFH在同样匹配策略下能达到SHOT九成以上的正确率,就用FPFH保速度;如果正确率差距明显,就老老实实上SHOT,同时接受它的计算成本。

4.6 匹配策略里的隐藏细节

拿到352维向量之后,匹配也不是无脑找最近邻。我踩过的坑是,单纯找欧氏距离最近的向量,很容易出现“一对多”的歧义,尤其当场景里有重复结构,比如一排形状相同的长方体箱子。

建议套用图像特征匹配里常用的最近邻距离比策略:对查询关键点的描述子,找最近邻距离d1和次近邻距离d2,当d1/d2小于某个阈值(比如0.8)时才接受这个匹配对。这个策略能滤掉大量模糊匹配。之后再交给RANSAC去粗配准。这套组合在多数SHOT配准任务里都非常管用。

5. SHOT的变体与适用边界:带颜色、多尺度、以及那些不适合它的场合

5.1 颜色辅助的SHOT变体与选型依据

SHOT问世之后,陆续有一些变体方案往外走,其中最常见的思路是把颜色信息加进来。RGB-D相机普及之后,很多场景同时有几何和颜色两个通道,纯几何SHOT显然浪费了颜色这个判别信号。

典型做法是在每个子空间里额外统计颜色直方图,颜色空间用Lab,而不是RGB。原因是Lab空间的距离更贴近人眼感知,两个在RGB空间里数字差异很大的颜色,可能在视觉上很接近,而Lab空间能更合理地反映这种差异。几何部分的352维加上颜色部分的统计,描述子维度会明显上涨,匹配的判别力也对应提升。

不过我要提醒的是:颜色通道的抗干扰能力不如几何通道。光照变化、相机白平衡漂移都会让颜色直方图发生偏移。如果场景光照可控,比如工业质检的固定工位,颜色辅助的变体很值得用;如果是户外机器人或者室内光照乱变的环境,我建议还是回到纯几何SHOT,或者只把颜色当作粗筛条件,别混进特征向量里。

5.2 多尺度策略:应对物体大小变化

SHOT没有内建的尺度不变性,但这不代表不能用。工程上最常见的做法是把多个半径下计算的SHOT拼起来,组成一个超长特征向量。比如用r=3、5、8厘米各算一次,每段352维,拼成1056维。这样描述子对一定范围内的尺度变化也有了容忍度。代价是计算量翻两倍以上,匹配时复杂度也随之上升。

另一个思路是多尺度测试后,选出最稳定的那个半径:对同一关键点,在不同r下分别计算描述子,然后在两片点云间比较哪个r对应的描述子匹配一致性更高,就采用那个r作为后续计算的标准。这个方案比拼接更精准,但需要先做一遍探索性匹配,实时性会受影响。

5.3 什么时候别用SHOT

再好的工具也有边界。SHOT在一些场合下确实不太合适,我总结了几种典型情况。

第一,点云极其稀疏且分布极不均匀时。比如只有几万点的大范围扫描点云,单个关键点邻域里只有寥寥几个点,直方图统计完全失去意义。这种情况更适合先做网格化重建,或者改用全局描述子。

第二,局部几何过于对称。球面、圆柱面、平面这类结构,在关键点邻域内看起来四面八方都差不多,LRF的特征向量方向会变得极不稳定。哪怕极微小的噪声扰动,都可能让坐标轴翻转或旋转,描述子自然就对不上了。对称结构的处理需要额外的全局信息辅助。

第三,计算资源严重受限的场景。352维本身不算吓人,但成千上万个关键点累计起来就是一笔不小的开销。在嵌入式设备上跑实时配准,SHOT可能把CPU吃满。这时候FPFH或者轻量级描述子往往是更务实的选择。

第四,表面极度光滑、缺乏法向量变化特征的点云。SHOT的直方图区分度来自法向量方向的变化,如果整个表面都是镜面一样的平滑曲面,所有邻域点的法向量几乎都指向同一方向,直方图就会挤在同一两个bin里,不同位置的描述子长得差不多,匹配自然失败。这种情况得靠纹理、颜色或者更全局的结构特征来救。

最后说一个我自己在使用中沉淀下来的小习惯:拿到SHOT特征后,先别急着做全量匹配。我会随机抽几百个关键点,算一遍双向最近邻匹配率——也就是A点云里找B的最近邻,再反过来从B找A的最近邻,看两者是否互认。如果互认率连三成都不到,那多半是参数或者数据预处理出了问题,该先回头调体素大小、法向量方向,而不是硬调特征匹配阈值。SHOT是个好工具,但它对输入质量有要求,把它的“脾气”摸顺了,后面整个流程都省心。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进