首页
/ OpenCV Shi-Tomasi 角点检测详解:goodFeaturesToTrack 原理、参数调优与源码实现

OpenCV Shi-Tomasi 角点检测详解:goodFeaturesToTrack 原理、参数调优与源码实现

2026-09-06 12:23:38作者:柏廷章Berta

本文以 OpenCV 官方教程中的 Shi-Tomasi 角点检测章节为主体,完整讲解 Shi-Tomasi 评分函数 R = min(λ₁, λ₂) 的判定原理、cv.goodFeaturesToTrack() 的完整参数语义与默认值,并深入 OpenCV 仓库 modules/features 模块的 C++ 源码,说明质量过滤、局部极大值筛选与非极大值抑制(基于网格加速)的具体实现,帮助你在特征点跟踪场景中写出可复现、可调参的角点检测代码。

1. 从 Harris 到 Shi-Tomasi:评分函数的改动

Harris 角点检测器的评分函数为:

R = λ₁·λ₂ - k·(λ₁ + λ₂)²

1994 年,J. Shi 与 C. Tomasi 在论文 Good Features to Track 中对其做了一个小改动:不再使用上述响应函数,而是直接取两个特征值的较小者:

R = min(λ₁, λ₂)

R 大于某个阈值时,该像素即被判定为角点。将其绘制到 λ₁ - λ₂ 平面上,可以得到下图:

Shi-Tomasi 判据在 λ1-λ2 平面上的角点区域

从图中可以直观看出:只有当 λ₁λ₂ 都高于某个最小值 λ_min 时(图中绿色区域),该点才被认为是角点。这与 Harris 判据的"十字形"响应区域不同——min(λ₁, λ₂) 的等值线是两条平行于坐标轴的直线,因此它天然具有"双特征值都强才算角点"的语义:平坦区域(两个特征值都小)、边缘区域(只有一个特征值大)都会被排除,只有真正的角点(两个特征值都大)才入选。这也正是 Shi-Tomasi 在结果稳定性上优于 Harris 的核心原因:它给出的角点分数对特征值排序不敏感(min 对 λ₁、λ₂ 对称),且分数本身就是一个可以按绝对大小比较的"质量值"。

2. goodFeaturesToTrack 函数接口与完整参数

OpenCV 提供了 cv.goodFeaturesToTrack() 函数:使用 Shi-Tomasi 方法(或指定时使用 Harris 方法)在图像中找出 N 个最强的角点。输入图像应为单通道图像(8-bit 或 32-bit 浮点)。

函数签名(Python 侧对应 C++ 声明,见 modules/features/include/opencv2/features.hpp):

corners = cv2.goodFeaturesToTrack(
    image,          # 输入单通道图像(8U 或 32F)
    maxCorners,     # 期望返回的最大角点数;<= 0 表示不限制,返回所有角点
    qualityLevel,   # 质量阈值,0~1 之间的相对值(见下文 4.1 节的归一化细节)
    minDistance,    # 返回角点之间的最小欧氏距离
    mask,           # 可选:感兴趣区域,CV_8UC1 且与图像同尺寸
    blockSize,     # 计算每个像素邻域导数协方差矩阵的平均块大小,默认 3
    gradientSize,  # Sobel 算子的孔径参数,默认 3
    useHarrisDetector,  # True 时用 cornerHarris,False 时用 cornerMinEigenVal,默认 False
    k              # Harris 检测器的自由参数,默认 0.04
)

官方头文件注释中对各参数的说明(摘自 features.hpp)值得逐条对照:

  • image:输入 8-bit 或 32-bit 浮点单通道图像;
  • maxCorners:返回角点的最大数量;若找到的角点比请求的多,则返回其中最强的若干个;maxCorners <= 0 表示不设上限;
  • qualityLevel:表征图像角点最小可接受质量的参数。该值会乘以全场最好的角点质量值(即最小特征值或 Harris 响应值的最大值),质量值小于该乘积的角点被剔除。官方注释给出的例子:若最好角点的质量值为 1500、qualityLevel = 0.01,则质量值小于 15 的角点全部被拒;
  • minDistance:返回角点之间的最小欧氏距离;
  • mask:可选的感兴趣区域(ROI),CV_8UC1、非空、与图像同尺寸;
  • blockSize:用于计算每个像素邻域导数协方差矩阵的平均块大小;
  • useHarrisDetector / k:选择 cornerHarriscornerMinEigenVal 作为评分函数,k 是 Harris 的自由参数(默认 0.04)。

另外官方注释还给出了一条很重要的性质:若用两个不同的 qualityLevel(A > B)分别调用,则 A 对应的返回角点向量必然是 B 对应返回向量的前缀——即提高质量阈值只会从结果头部截断,不会打乱顺序。

3. 完整示例:找出 25 个最强角点

下面是教程中的完整示例,在灰度图像上检测 25 个最强角点并绘制圆圈(对应仓库示例 goodFeaturesToTrack_Demo.py 的基本流程):

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

img = cv.imread('blox.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

corners = cv.goodFeaturesToTrack(gray, 25, 0.01, 10)
corners = np.int0(corners)

for i in corners:
    x, y = i.ravel()
    cv.circle(img, (x, y), 3, 255, -1)

plt.imshow(img), plt.show()

参数对应关系:25 为期望角点数,0.01 为质量等级(全场最强角点分数的 1% 作为下限),10 为角点间最小欧氏距离(像素)。

运行结果如下,圆圈即为检测出的 25 个最强角点:

Shi-Tomasi 检测到的 25 个最强角点(blox 图像)

注意 corners 的返回类型是 N x 3float32 数组,每个角点以 (x, y, 1) 形式存放(即 Point2f 的内存布局),因此绘制前需要 np.int0() 取整并用 i.ravel() 解包出 x, y

调参建议

  • qualityLevel 过低(如 0.001)会放行大量弱角点,配合较小的 minDistance 会在纹理密集区域堆出过多点;过高(如 0.1)则可能整图只剩零星几个点。0.01~0.04 是常见起步区间;
  • minDistance 决定了角点的空间密度,通常取 5~20 像素;它同时影响第 5 节中网格非极大值抑制的单元尺寸;
  • 若只想在图像某区域检测,可传入与图像同尺寸的 CV_8UC1 二值 mask(源码会强制校验 mask 类型与尺寸,见 4.2 节)。

4. 源码纵深:goodFeaturesToTrack 的算法骨架

goodFeaturesToTrack 的 C++ 实现位于 modules/features/src/featureselect.cpp。从源码结构看,整个算法分为四个阶段,与官方头文件注释的描述一一对应。

4.1 阶段一:评分图计算与质量阈值过滤

// modules/features/src/featureselect.cpp
if( useHarrisDetector )
    cornerHarris( image, eig, blockSize, gradientSize, harrisK );
else
    cornerMinEigenVal( image, eig, blockSize, gradientSize );

double maxVal = 0;
minMaxLoc( eig, 0, &maxVal, 0, 0, _mask );
threshold( eig, eig, maxVal*qualityLevel, 0, THRESH_TOZERO );
dilate( eig, tmp, Mat() );

这段代码印证了两点:

  1. 评分函数的可切换性useHarrisDetectortrue 时调用 cornerHarris(响应 λ₁λ₂ - k(λ₁+λ₂)²),为 false 时调用 cornerMinEigenVal,即 Shi-Tomasi 的 min(λ₁, λ₂)。两种评分图都以单通道浮点矩阵 eig 承载;
  2. qualityLevel 是相对阈值:先用 minMaxLoc 求出全图最大响应 maxVal(若提供 mask 则在 mask 内求),再以 maxVal * qualityLevel 为阈值执行 THRESH_TOZERO——低于阈值的像素直接被清零。这解释了为什么 qualityLevel 的取值范围是 0~1:它本质是"相对于全场最强角点的比例",而不是绝对分数。

随后的 dilate 用 3×3 核求邻域最大值图 tmp,为下一步识别"严格局部极大值"做准备。

4.2 阶段二:收集局部极大值候选点

for( int y = 1; y < imgsize.height - 1; y++ )
    for( int x = 1; x < imgsize.width - 1; x++ )
    {
        float val = eig_data[x];
        // val != 0:通过质量阈值;val == tmp_data[x]:是本 3x3 邻域最大值
        if( val != 0 && val == tmp_data[x] && (!mask_data || mask_data[x]) )
            tmpCorners.push_back(eig_data + x);
    }

候选点必须同时满足三个条件:质量值非零(通过 qualityLevel 过滤)、等于膨胀图中对应值(即严格局部极大值,避免同一片角点区域返回一串相邻点)、位于 mask 允许区域内。入口处的断言也值得注意(featureselect.cpp#L300-L301):

CV_Assert( qualityLevel > 0 && minDistance >= 0 && maxCorners >= 0 );
CV_Assert( _mask.empty() || ((_mask.type() == CV_8UC1 || _mask.type() == CV_BoolC1) && _mask.sameSize(_image)) );

qualityLevel 必须为正数,mask 必须是 CV_8UC1/CV_BoolC1 且与图像同尺寸,否则直接抛异常。此外,若图像是 UMat 且满足 OpenCL 条件,代码会优先走 ocl_goodFeaturesToTrack 加速路径(featureselect.cpp#L303-L305)。

4.3 阶段三:按质量降序排序 + 网格加速的非极大值抑制

std::sort( tmpCorners.begin(), tmpCorners.end(), greaterThanPtr() );

if (minDistance >= 1)
{
    // Partition the image into larger grids
    const int cell_size = cvRound(minDistance);
    ...
    for( i = 0; i < total; i++ )
    {
        ...
        // 只检查候选点周围 3x3 个网格单元中已选角点
        for( int yy = y1; yy <= y2; yy++ )
            for( int xx = x1; xx <= x2; xx++ )
                ...
                if( dx*dx + dy*dy < minDistance )   // minDistance 已预先平方
                { good = false; goto break_out; }
        ...
        if (good)
        {
            grid[...].push_back(...);
            corners.push_back(...);
            ++ncorners;
            if( maxCorners > 0 && (int)ncorners == maxCorners )
                break;   // 达到 maxCorners 立即停止
        }
    }
}

这一步实现了教程中描述的"取最强的角点、丢弃其 minDistance 范围内所有更弱点、直到凑满 N 个"的流程,源码上有三个值得注意的细节:

  1. 排序键是指向评分图的指针greaterThanPtr() 按指针所指处的浮点值(质量值)降序排列,即始终先处理最强候选——这保证了"返回的最强 N 个"语义,也解释了 2 节中"提高 qualityLevel 只会截断前缀"的性质;
  2. 网格空间划分加速:朴素做法是每来一个候选点就与所有已选点做 O(N) 距离比较;源码改为把图像划分为 cell_size = round(minDistance) 的网格单元,只需检查候选点所在的 3×3 邻域单元,将非极大值抑制降到近似线性复杂度;
  3. 提前终止:凑满 maxCorners 个即 break,弱角点根本不需要参与后续比较;当 minDistance < 1 时则退化为不做距离抑制、直接按序取前 maxCorners 个。

最终输出被 reshape(2, n).convertTo(...) 转成 N×2 的浮点矩阵(对应 Python 端的 N×3 数组),若调用 goodFeaturesToTrackWithQuality 重载(见 features.hpp#L217-L221),还会额外输出每个角点的质量值向量 cornersQuality,便于后续按绝对分数做二次筛选。

4.4 对象化封装:GFTTDetector

除静态函数外,OpenCV 还提供 GFTTDetector 检测器类,用于把参数序列化、跨语言(C++/Python/Java)统一使用。其实现见 modules/features/src/gftt.cpp:构造函数接收 nfeatures, qualityLevel, minDistance, blockSize, gradientSize, useHarrisDetector, k 七个参数并支持 read()/write()FileStorage 读写;detect() 内部若输入不是 CV_8U 会自动 cvtColor 转灰度,再调用 goodFeaturesToTrack,最后把角点封装为 KeyPoint 列表(keypoints[i] = KeyPoint(corners[i], blockSize, -1, cornersQuality[i]))——注意 KeyPointsize 字段取 blockSizeresponse 字段存质量值,这解释了用 goodFeaturesToTrackWithQuality 时响应值的来源。

5. 与特征点跟踪的配合

教程结尾强调:goodFeaturesToTrack 更适用于跟踪场景("This function is more appropriate for tracking")。原因在于 Shi-Tomasi 角点质量稳定、数量可控、分布可疏密调节(minDistance),是 Lucas-Kanade 金字塔光流 calcOpticalFlowPyrLK 的理想"种子点"来源:每帧先 goodFeaturesToTrack 取当前帧角点,再用 calcOpticalFlowPyrLK 与上一帧角点匹配,即可实现稳定的多特征点跟踪。官方头文件注释也明确指出该函数"可用于初始化对象的点基跟踪器(initialize a point-based tracker of an object)",并建议与 calcOpticalFlowPyrLKestimateRigidTransform 配合使用(features.hpp#L178-L179)。仓库中可参考的相关示例与测试包括:

6. 小结

要素 说明
评分函数 R = min(λ₁, λ₂),双特征值均大才判为角点
核心函数 cv.goodFeaturesToTrack(image, maxCorners, qualityLevel, minDistance, ...)
qualityLevel 语义 相对阈值:实际阈值 = maxVal × qualityLevelmaxVal 为全场最强响应
默认参数 blockSize=3gradientSize=3useHarrisDetector=Falsek=0.04
实现位置 modules/features/src/featureselect.cpp(评分 + 质量过滤 + 局部极大 + 网格 NMS)
典型用途 calcOpticalFlowPyrLK 金字塔光流提供稳定、可控数量的跟踪种子点

掌握 min(λ₁, λ₂) 判据与"相对质量阈值 + 局部极大 + 距离抑制"三步筛选机制后,你可以按场景自由调整 qualityLevelminDistance 的配比,并在需要时用 goodFeaturesToTrackWithQuality 拿绝对质量值做二次筛选,从而在不同纹理密度的图像上获得数量与分布都符合预期的角点集合。

登录后查看全文
热门项目推荐
相关项目推荐