第四章 特征点提取(上):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 本章知识体系

下面我用一张图来总结本章的核心逻辑:

特征点提取(上)知识体系 输入图像 Harris角点检测 Shi-Tomasi角点 尺度空间理论 R = det(M) - k·trace(M)² M = Σ w·[Ix² IxIy; IxIy Iy²] k=0.04~0.06 R = min(λ1, λ2) 直接取特征值最小值 无需调参k L(x,y,σ) = G*I 高斯金字塔 多尺度检测 特征点集合(位置+尺度) 后续应用:特征匹配 → 位姿估计 → 建图

4.6 本章小结

今天咱们聊了三个核心内容:

  1. Harris角点:基于矩阵M的响应值R,经典但需要调参k
  2. Shi-Tomasi角点:直接取特征值最小值,更稳定
  3. 尺度空间理论:用高斯金字塔解决多尺度问题

说实话,这些内容看起来是基础,但实际项目中踩坑最多的就是这些基础。我见过太多人调不好参数就怪算法不行,其实是你没理解它的原理。

下一章咱们继续聊特征点提取的下半部分——SIFT和ORB。到时候你会发现,今天的尺度空间理论就是SIFT的基石。