第四章 特征点提取(上):Harris角点检测、Shi-Tomasi角点、尺度空间理论
各位同学,欢迎来到视觉定位系统的第四讲。
今天咱们聊一个非常核心的话题——特征点提取。说白了,就是教计算机怎么在图像里找到那些“有特点”的地方。你想想看,如果一张图全是白墙,那计算机根本分不清自己在哪里。但如果有墙角、桌角、窗户边,这些位置就变成了定位的“锚点”。
4.1 为什么需要角点?
我先问大家一个问题:一张图像里,什么样的像素点最有价值?
我个人习惯把图像中的点分成三类:
- 平坦区域:像素值变化很小,比如白墙。这种点毫无区分度。
- 边缘:沿着某个方向变化剧烈,但垂直方向变化不大。比如桌子的边沿。
- 角点:无论往哪个方向移动,像素值都会剧烈变化。比如墙角、棋盘格交叉点。
角点就是我们要找的“明星”。在SLAM里,我们靠这些角点来匹配不同帧之间的图像,从而估算相机的运动。
核心观点:角点 = 图像中局部区域在各个方向上都有明显灰度变化的点。
4.2 Harris角点检测——经典中的经典
Harris角点检测是1988年提出的,到现在三十多年了,依然活跃在工业界。我当年刚入行时,第一个项目就是用它做特征匹配。
4.2.1 数学原理
Harris的核心思想很简单:用一个窗口在图像上滑动,观察窗口内像素值的变化量。
假设窗口移动了 (u, v),那么灰度变化量可以写成:
E(u,v) = Σ w(x,y) * [I(x+u, y+v) - I(x,y)]²
其中 w(x,y) 是窗口函数,通常用高斯权重。
经过泰勒展开和化简,最终得到一个2x2的矩阵 M:
M = Σ w(x,y) * [ Ix² IxIy ]
[ IxIy Iy² ]
其中 Ix 和 Iy 是图像在 x 和 y 方向的梯度。
然后我们计算一个响应值 R:
R = det(M) - k * (trace(M))²
其中 k 是经验常数,通常取 0.04~0.06。
判断规则很简单:
- R > 阈值 → 角点
- R < 0 → 边缘
- |R| 很小 → 平坦区域
我的经验:k 值对结果影响挺大的。k 太小会检测出很多边缘点,k 太大又会漏掉真正的角点。我一般从 0.04 开始调,根据实际场景微调。
4.2.2 代码实现
OpenCV里直接调用就行,但我建议你至少手写一次,理解底层逻辑。
import cv2
import numpy as np
img = cv2.imread('checkerboard.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Harris角点检测
gray = np.float32(gray)
dst = cv2.cornerHarris(gray, blockSize=2, ksize=3, k=0.04)
# 阈值筛选
img[dst > 0.01 * dst.max()] = [0, 0, 255]
cv2.imshow('Harris Corners', img)
cv2.waitKey(0)
参数说明:
- blockSize:窗口大小,一般取2或3
- ksize:Sobel算子的孔径大小
- k:Harris自由参数
避坑指南:我曾经在一个光照变化剧烈的场景里用Harris,结果角点数量忽多忽少。后来发现,Harris对光照变化比较敏感。解决办法是先用直方图均衡化做预处理。
4.3 Shi-Tomasi角点——Harris的改良版
Shi-Tomasi是Harris的“升级版”。它俩的数学推导几乎一样,唯一的区别在于响应值的计算方式。
4.3.1 改进在哪里?
Harris用的是 det(M) - k*trace(M)²,而Shi-Tomasi直接取矩阵 M 的两个特征值 λ1 和 λ2 中的较小值:
R = min(λ1, λ2)
如果这个最小值大于阈值,就认为是角点。
为什么这样改?我个人的理解是:Harris的公式里有个 k 需要调参,而Shi-Tomasi直接看特征值,更直观,也更稳定。
4.3.2 代码实现
import cv2
import numpy as np
img = cv2.imread('checkerboard.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Shi-Tomasi角点检测
corners = cv2.goodFeaturesToTrack(gray, maxCorners=100, qualityLevel=0.01, minDistance=10)
corners = np.int0(corners)
for corner in corners:
x, y = corner.ravel()
cv2.circle(img, (x, y), 3, (0, 255, 0), -1)
cv2.imshow('Shi-Tomasi Corners', img)
cv2.waitKey(0)
参数说明:
- maxCorners:最多检测多少个角点
- qualityLevel:质量阈值,0.01表示取最大响应值的1%作为阈值
- minDistance:两个角点之间的最小距离,避免扎堆
对比总结:Harris适合需要均匀分布角点的场景,Shi-Tomasi更适合需要“最强”角点的场景。我自己的项目里,如果做稠密重建会用Harris,做稀疏特征匹配会用Shi-Tomasi。
4.4 尺度空间理论——为什么需要它?
好,现在问题来了。你想想看,一个角点在近处看是角点,在远处看可能就变成边缘了。这就是尺度问题。
我举个例子:你站在10米外看一个棋盘格,每个格子很小,角点也很小。但如果你凑到1米处,角点就变大了。同一个物理点,在不同尺度下表现完全不同。
4.4.1 尺度空间的核心思想
尺度空间理论认为:图像在不同尺度下的信息都是有价值的。我们需要构建一个“尺度金字塔”,在不同尺度下分别检测特征点。
最常用的方法是高斯金字塔:
L(x, y, σ) = G(x, y, σ) * I(x, y)
其中 G 是高斯核,σ 是尺度参数。σ 越大,图像越模糊,相当于在更大尺度下观察。
4.4.2 为什么用高斯核?
高斯核是唯一能产生“尺度空间”的线性核。原因有两个:
- 高斯核是平滑的,不会引入伪影
- 高斯卷积满足“半群性质”:先卷积 σ1,再卷积 σ2,等价于直接卷积 √(σ1²+σ2²)
嗯,这里要注意:不是所有平滑核都能用。我见过有人用均值滤波做尺度空间,结果在边缘处产生了大量假角点。
4.4.3 尺度空间的实现
import cv2
import numpy as np
img = cv2.imread('scene.jpg', 0)
# 构建高斯金字塔
layer = img.copy()
pyramid = [layer]
for i in range(6):
layer = cv2.pyrDown(layer)
pyramid.append(layer)
# 在不同尺度下检测角点
for i, layer in enumerate(pyramid):
dst = cv2.cornerHarris(np.float32(layer), 2, 3, 0.04)
# 将检测结果映射回原图尺寸
# ... (缩放处理)
我的建议:实际项目中不要手动实现尺度空间,直接用SIFT或ORB。它们内部已经做好了尺度不变性。但理解原理很重要,因为面试经常问。
4.5 本章知识体系
下面我用一张图来总结本章的核心逻辑:
4.6 本章小结
今天咱们聊了三个核心内容:
- Harris角点:基于矩阵M的响应值R,经典但需要调参k
- Shi-Tomasi角点:直接取特征值最小值,更稳定
- 尺度空间理论:用高斯金字塔解决多尺度问题
说实话,这些内容看起来是基础,但实际项目中踩坑最多的就是这些基础。我见过太多人调不好参数就怪算法不行,其实是你没理解它的原理。
下一章咱们继续聊特征点提取的下半部分——SIFT和ORB。到时候你会发现,今天的尺度空间理论就是SIFT的基石。