4、基于特征的代价计算:Census变换、Rank变换、BRIEF描述子、Hamming距离计算
好,咱们进入第四章。前面几章我们聊了像素级的代价计算,比如AD、SAD这些。说白了,它们就是直接拿像素值做减法。但实际项目中你会发现,光照一变,或者相机增益不同,这些方法立马就崩了。
为什么会这样?因为像素值本身对光照太敏感了。你想想看,同一个物体,左边亮右边暗,左右图的像素值差一大截,代价计算自然不准。
所以,这一章我们聊聊基于特征的代价计算。核心思想很简单:不直接比像素值,而是比像素周围的局部结构特征。光照变了,但局部结构(比如明暗关系)通常不变。这就是它的抗噪、抗光照变化的底气所在。
核心观点:基于特征的代价计算,本质上是将像素值映射到一个对光照不敏感的特征空间,再在这个空间里计算匹配代价。
4.1 Census变换:局部二值模式
Census变换是我个人非常喜欢的一种方法。它简单、粗暴,但效果出奇的好。
它的做法是这样的:以一个像素为中心,取一个窗口(比如3x3或7x7)。然后,把窗口内每个邻居像素的灰度值与中心像素比较。如果邻居比中心亮,记为1;否则记为0。最后,把这些0和1按顺序串起来,形成一个二进制串。这个串就是该像素的Census特征。
举个例子,一个3x3窗口:
中心像素值:100
邻居像素值:
[ 90, 110, 95 ]
[ 85, 100, 120 ]
[ 105, 80, 115 ]
比较结果(大于中心为1,否则0):
[ 0, 1, 0 ]
[ 0, 0, 1 ]
[ 1, 0, 1 ]
二进制串(按行优先):0 1 0 0 0 1 1 0 1
十进制表示:0b010001101 = 141
嗯,这里要注意:Census变换只关心相对大小,不关心绝对差值。所以光照整体变亮或变暗,只要局部明暗关系不变,Census值就不变。这就是它抗光照的秘诀。
我的经验:我在项目中遇到过光照剧烈变化的室外场景,用SAD基本没法看,换成Census后,匹配质量提升了一个档次。但Census也有缺点——它对重复纹理比较敏感,因为局部结构可能非常相似。
4.2 Rank变换:排序的力量
Rank变换和Census有点像,但思路更简洁。它不生成二进制串,而是直接统计窗口内有多少个邻居像素比中心像素小。这个数量就是Rank值。
还是上面那个3x3的例子:
邻居像素值:90, 110, 95, 85, 100, 120, 105, 80, 115
中心像素值:100
比100小的邻居:90, 95, 85, 80 → 共4个
所以Rank值 = 4
你看,Rank变换把整个窗口压缩成了一个整数。这个整数反映了中心像素在局部区域的“排序位置”。
Rank变换的计算比Census更简单,但信息量也少一些。我个人习惯是:如果对速度要求极高,用Rank;如果对精度要求更高,用Census。
避坑指南:我曾经在纹理稀疏区域吃过亏。Rank变换在平坦区域(所有像素值都差不多)会失效,因为Rank值几乎都一样,区分度极差。这时候需要结合其他代价方法一起用。
4.3 BRIEF描述子:更紧凑的二进制特征
BRIEF(Binary Robust Independent Elementary Features)是特征描述子领域的一个经典方法。它和Census的思路类似,但更灵活。
BRIEF的做法是:在像素周围随机选取N对点(比如256对),比较每对点中两个像素的灰度值。如果第一个比第二个亮,记为1,否则记为0。最终得到一个N位的二进制串。
和Census相比,BRIEF有两个关键区别:
- 采样模式不同:Census用固定窗口内的所有邻居,BRIEF用随机点对
- 比较对象不同:Census是中心vs邻居,BRIEF是邻居vs邻居
BRIEF的随机采样让它对局部结构有更强的描述能力,但同时也引入了随机性。实际应用中,通常会用高斯分布来指导采样点的位置,让靠近中心的点更密集。
// 伪代码:BRIEF描述子生成
function computeBRIEF(image, x, y, pointPairs):
descriptor = 0
for i = 0 to N-1:
p1 = pointPairs[i].first
p2 = pointPairs[i].second
if image[y+p1.y][x+p1.x] > image[y+p2.y][x+p2.x]:
descriptor |= (1 << i) // 第i位置1
return descriptor
我的建议:BRIEF在立体匹配中不如Census常用,因为它的随机性导致特征不够稳定。但如果你在做特征点匹配(比如ORB-SLAM),BRIEF就是标配了。
4.4 Hamming距离:二进制特征的匹配利器
好,特征算出来了,怎么比较两个特征的相似度呢?对于二进制特征(Census、BRIEF),最自然的度量就是Hamming距离。
Hamming距离的定义很简单:两个等长二进制串之间,对应位不同的个数。
比如:
串A: 1 0 1 1 0 0 1
串B: 1 1 0 1 0 1 1
不同位:第2位、第3位、第6位 → 共3位不同
所以Hamming距离 = 3
在计算机中,计算Hamming距离有一个非常高效的指令:POPCNT(population count),也就是统计一个整数中1的个数。两个二进制串的Hamming距离,就等于它们异或(XOR)后结果中1的个数。
// C++实现:计算两个Census特征的Hamming距离
int hammingDistance(uint64_t a, uint64_t b) {
return __builtin_popcountll(a ^ b); // GCC内置函数
}
这个操作在CPU上是一条指令完成的,速度极快。这也是为什么基于特征的代价计算在实际系统中非常受欢迎——计算快,抗干扰强。
关键点:Hamming距离越小,两个特征越相似。在立体匹配中,我们通常选择Hamming距离最小的候选点作为匹配点。
4.5 本章知识体系
为了让你更直观地理解这几种方法的关系,我画了一张图:
4.6 实战对比:什么时候用哪个?
说了这么多,你可能想问:实际项目中到底选哪个?我根据自己的经验,整理了一个对比表:
| 方法 | 抗光照能力 | 计算速度 | 抗噪声能力 | 适用场景 |
|---|---|---|---|---|
| Census变换 | ★★★★★ | ★★★★ | ★★★★ | 光照变化大的室外场景 |
| Rank变换 | ★★★★ | ★★★★★ | ★★★ | 对速度要求极高的嵌入式系统 |
| BRIEF描述子 | ★★★★ | ★★★ | ★★★ | 特征点匹配(非密集匹配) |
我的经验:如果你刚开始做立体匹配,我建议从Census入手。它是最平衡的选择,网上资料也多。等你把Census吃透了,再根据实际需求去尝试Rank或BRIEF。
好了,这一章的内容就到这里。基于特征的代价计算,说白了就是用局部结构信息代替原始像素值,从而获得对光照变化的鲁棒性。Census、Rank、BRIEF各有千秋,但核心思想是一致的。而Hamming距离,则是连接这些二进制特征与匹配代价的桥梁。
公众号:蓝海资料掘金营,微信deep3321