本节摘要:角点是窗口往任意方向移动都引起灰度剧烈变化的点,是图像里最"好认"的局部结构。本节从滑动窗口直觉讲起,拆解 Harris 的自相关矩阵与响应函数,给出 Shi-Tomasi 的改进与 goodFeaturesToTrack 的实用封装,再带一眼 FAST 的提速思路。读完你能为下一节的描述符准备好高质量的锚点。
阅读完本节,你应当能够:
import cv2 import numpy as np img = cv2.imread('chess.jpg') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = np.float32(gray) # Harris 要求 float32 dst = cv2.cornerHarris(gray, blockSize=2, ksize=3, k=0.04) img[dst > 0.01 * dst.max()] = [0, 0, 255] # 响应超过最大值1%处标红 cv2.imshow('harris', img) cv2.waitKey(0)
拿一张棋盘格或窗框照片跑这段代码,所有格子的交点都会亮起红色小块——这些就是角点。三处值得注意:输入必须转 float32(第 1 章讲过算术溢出,这里同理);输出的 dst 是与原图同尺寸的"响应图",值越大越像角点;0.01 这个系数是"录用线",按最大响应的百分比卡,调到 0.05 会只剩最锐的角,调到 0.005 会混入不少边缘点。
再用工程上更常用的封装跑一遍:
corners = cv2.goodFeaturesToTrack(gray, maxCorners=100, qualityLevel=0.1, minDistance=10)
它直接返回角点坐标列表(亚像素精度),三个参数分别控制"最多要几个、质量下限、彼此最小间距"。做特征提取时我几乎总用它——自动做了非极大值抑制与均匀采样,省去手写阈值与后处理。
想象一个小窗口在图上滑动,每挪一步就对比窗口内灰度的变化量。三类的表现截然不同:
这就是"角点是好的特征"的原因:它在两个方向上都携带信息,换个视角拍摄后仍然能被再次认出。特征点检测的黄金标准——可重复性(另一张图里还能找到同一个点)、显著性(与邻居区分度高)、局部性(小块区域自足)——角点全部满足。反过来,边缘只在一维可定位(沿着边滑动无法定位),平坦区零信息,都不能当特征锚点。
窗口移动引起的灰度变化量,可以展开为梯度平方和的形式:x 梯度的平方和、y 梯度的平方和、两个梯度乘积的和,三个数排成 2×2 的自相关矩阵 M。这个矩阵的两个特征值 λ1、λ2 恰好刻画了"两个正交方向上的变化强度":
| λ1、λ2 的情况 | 区域类型 | 判定 |
|---|---|---|
| 都小 | 平坦 | 不是特征 |
| 一大一小 | 边缘 | 不是好特征 |
| 都大 | 角点 | 录取 |
Harris 的贡献是不真的去解特征值(当年算力金贵),而是用行列式与迹的组合构造响应函数:R 等于矩阵行列式减去 k 乘迹的平方。k 就是代码里那个 0.04,经验值域 0.04 到 0.06,多数资料都写 0.04,不必纠结。行列式正比于两特征值之积,迹正比于两者之和——一大一小时积小和大,R 变负,压掉边缘;两值都大时积大,R 冲高,角点胜出。
Shi-Tomasi 的改进直截了当:现在算特征值不贵了,直接用较小的那个特征值当响应——"两个方向都强"的判定一步到位,物理意义更清晰,实验里稳定性也更好。goodFeaturesToTrack 内部用的正是 Shi-Tomasi 判据。
Harris 对每个像素都算一遍矩阵,天然不便宜。FAST(Features from Accelerated Segment Test)换了个问法:取候选点周围一圈 16 个像素,若有一段连续超过 N 个都显著亮于或暗于中心,就是角点。整个判定只是比较与计数,连乘法都省了,速度是毫秒级视频特征检测的门槛级方案。代价是:没有响应强度度量(要靠后续打分)、对噪声敏感(通常先高斯平滑)、本身不带尺度与旋转不变性——这些短板由 ORB 补齐(下一节详说)。
cornerHarris 的 blockSize 是自相关矩阵累加的邻域尺寸,2 到 3 是常规;ksize 是梯度用的 Sobel 核,3 是标配;k 固定 0.04。goodFeaturesToTrack 的三参数联动性强:qualityLevel 0.01 到 0.05 起步,特征点太少先降质量线;minDistance 按目标密度定,做图像配准要均匀覆盖时给大一点(比如短边的 3%),做单目标定位时给小。maxCorners 设 0 表示不设上限,靠前两个参数自然收敛。
goodFeaturesToTrack 返回的坐标可再经 cornerSubPix 精化到亚像素(小数坐标),对测量类任务(两角点求距离)能把精度从 ±1 像素提到 ±0.1 像素量级。模板匹配、相机标定这类对位置精度敏感的场景建议默认加;只做粗匹配则不必。
角点天然扎堆在纹理丰富区(文字、花纹),大平面(墙面、天空)一个都没有。做图像拼接时这会导致重叠区匹配点全挤在一角,单应矩阵估计不稳。对策:goodFeaturesToTrack 配合 mask 参数只在感兴趣区域取点,或分区取点再合并,保证空间覆盖的均匀性。
⚠️ 常见坑:忘了转 float32 直接喂 cornerHarris,报错信息还是类型相关的天书;以及用二值图做 Harris——梯度在黑白边界上是悬崖,矩阵特征值虚高,检测出一圈"假角点"。角点检测的输入应是平滑的灰度图,必要时先轻度高斯。
💡 关键直觉:响应图 dst 本身就是一份"角性地图",比阈值卡出来的点更有分析价值。调参时 imshow 一下 dst(先归一化到 0–255),看"亮斑"分布是否与目标结构重合,比反复改 0.01 系数直观十倍。
Harris 的三张底牌值得单独说透,它们决定了适用边界。
光照不变性来自梯度:自相关矩阵的元素全是梯度乘积的加权和,亮度整体加一个常数(光照平移)不改变梯度,梯度同乘一个系数(对比度拉伸)在归一化的响应比较里被抵消。所以白天拍的和傍晚拍的同一面墙,Harris 找到的角点位置基本一致。
旋转不变性来自矩阵本身:自相关矩阵的特征值是旋转不变量——图像旋转时矩阵跟着旋转,但两个特征值(描述两个正交主方向上的变化强度)不变。于是"都大是角"的判定跨旋转成立。
尺度是它没有的那张牌:窗口尺寸固定,目标的表观尺寸变了,同一个物理角点在窗口里的"地位"就变了——远处的小窗角在 3×3 窗口里是角,走近后占了整个窗口反而像边缘。补救有两条路:预先把图缩放到目标尺寸近似一致(工程上常用,粗暴有效),或上下一节的尺度空间方案(SIFT 的正解)。
做分析用前者(要响应图做可视化与研究),做应用用后者(要现成的坐标列表)。两者判据同源(后者是 Shi-Tomasi),结果高度一致,差别只在封装的便利度。
不能。角点只是位置,跨图比对需要描述符(下一节)。直接拿两张图的角点按最近距离硬配,误配率高得离谱——两张图的角点数量动辄数百,空间分布又相似,纯粹的位置匹配毫无判别力。
cornerMinEigenShiTomasi 系列给的是角点;要边缘点用第 2 章第 5 节的边缘检测加采样。角点检测器的设计目标就是抑制边缘响应(响应函数里那个负值区间专门压边缘),指望它输出边缘点是缘木求鱼。
第 5 章第 2 节的光流跟踪需要"持续可跟的点",生产这些点是角点检测在工程里的高频用途,配方值得固化。
第一步按场景定取点区域。全图取点会把大量点浪费在背景上(跟踪背景毫无意义),用 ROI 或掩码把取点限制在目标区域——检测框内、手动框选区、运动掩码内,取决于上游给了什么。
第二步 goodFeaturesToTrack 三参数定档。maxCorners 按跟踪预算给(几十到一两百,点多了光流的计算与关联负担都重);qualityLevel 从 0.05 起步,点质量差(跟几帧就飘)就上调;minDistance 按目标尺寸给,小目标给 5 到 7 保证点分布,大画面给几十拉开间距。
第三步亚像素精化。测量与配准用途必须加 cornerSubPix(本节 3.2 讲过理由),纯跟踪用途可以省。
第四步验收:把点画到图上(circle 半径 3 最直观),检查分布是否覆盖目标的关键结构、有没有扎堆。通过后再喂给光流——第 5 章的跟踪质量,一半在这步就定了。
这套配方的底层认知只有一条:跟踪点的质量 = 位置的稳定性 × 分布的覆盖度,参数全部围绕这两条调。
角点检测的产出(坐标列表或响应图)到特征匹配之间还差一步描述编码。两者在工程上的分工边界清晰:检测环节的所有调参围绕"点的质量与分布",描述环节围绕"编码的区分度与速度"——出了问题先判断该回哪一节调,是本章排错的基本功。本节 3.7 的锚点配方产出的坐标列表,正是下一节 detectAndCompute 的输入形态,接口已经就位。
有了角点坐标只是第一步——"在哪里"。下一节的描述符回答"长什么样",把邻域编码成可比对的向量。