首页
/ OpenCV 角点亚像素定位实战:cv::cornerSubPix 精化角点坐标

OpenCV 角点亚像素定位实战:cv::cornerSubPix 精化角点坐标

2026-09-07 09:20:40作者:幸俭卉

本文以 OpenCV 官方教程系列中的 “Detecting corners location in subpixels” 为核心,讲解如何使用 cv::cornerSubPix 把 Shi-Tomasi / Harris 检测器得到的整数像素角点精化到亚像素精度。教程依托仓库内真实可运行的 C++ / Python / Java 示例代码与 imgproc 模块源码,读者可以从中掌握亚像素精化背后的梯度正交假设与迭代原理,并直接落地到相机标定、特征匹配、运动跟踪等对精度敏感的实际任务中。

教程定位与目标

本教程位于 doc/tutorials/features/trackingmotion/corner_subpixels/corner_subpixels.markdown,属于 “TrackingMotion”(跟踪与运动)角点教程序列中的一环:它的前一篇是 自定义角点检测器教程(基于 cornerEigenValsAndVecs / cornerMinEigenVal 实现 Harris 与 Shi-Tomasi),本篇则解决“检测到角点之后如何让位置更精确”的问题。

本教程的核心目标只有一个:

使用 OpenCV 函数 cv::cornerSubPix,获得比整数像素更精确的角点坐标。

教程作者为 Ana Huamán,示例代码要求 OpenCV >= 3.0,与当前仓库代码保持兼容。

为什么需要亚像素精度

goodFeaturesToTrack(Shi-Tomasi)或 cornerHarris 检测角点时,输出坐标通常是整数像素Point2f 的整数值)。而在相机标定(棋盘格角点)、双目视差、单应矩阵求解等场景中,角点位置的微小偏差会被放大到最终的外参/内参误差中。当图像分辨率有限、角点边缘模糊或目标较远时,整数像素级坐标往往不足以满足精度需求。

亚像素精化的思路是:不放弃角点位置,而是利用角点邻域内的梯度信息做数值迭代,把坐标“挤”到小数级别。

原理:梯度正交假设与迭代求解

cv::cornerSubPix 的原理来自 Forstner 等人在 1987 年提出的快速亚像素角点定位方法(仓库 API 注释中引用为 @cite forstner1987fast,见 modules/imgproc/include/opencv2/imgproc.hpp#L1762-L1804)。

其核心观察是:从亚像素角点中心 \(q\) 指向其邻域内任意点 \(p_i\) 的向量,与 \(p_i\) 处的图像梯度 \(DI_{p_i}\) 近似正交(受图像噪声与测量噪声影响)。

对每个邻域点,误差定义为:

\[\epsilon_i = {DI_{p_i}}^T \cdot (q - p_i)\]

我们的目标是寻找 \(q\) 使所有 \(\epsilon_i\) 最小。令误差为零,可建立方程组:

\[\sum_i(DI_{p_i} \cdot {DI_{p_i}}^T) \cdot q - \sum_i(DI_{p_i} \cdot {DI_{p_i}}^T \cdot p_i) = 0\]

其中梯度在 \(q\) 的“搜索窗口”内求和。把第一项梯度相关矩阵记为 \(G\)、第二项记为 \(b\),则:

\[q = G^{-1} \cdot b\]

算法将邻域窗口中心移动到新求得的 \(q\) 上,不断迭代,直到角点中心位置的变化小于设定阈值为止。

结合源码可以看得更细。modules/imgproc/src/cornersubpix.cpp 的实现中有几点关键工程细节:

  • 高斯加权搜索窗:源码先按 winSize 构造 (2·win+1) × (2·win+1) 的掩码,权重为 exp(-x²)·exp(-y²) 型高斯分布(cornersubpix.cpp#L71-L80),离中心越近的像素对自相关矩阵贡献越大。
  • zeroZone(死区):若设置了 zeroZone,搜索窗正中央对应区域(win.height - zeroZone.height .. win.height + zeroZone.height 区间)的掩码值会被置 0(cornersubpix.cpp#L83-L93),从而避免自相关矩阵奇异。
  • 迭代细节:每次迭代用 getRectSubPix 以当前估计位置为中心重采样邻域,用中心差分计算梯度 tgx/tgy,累加 a/b/c/bb1/bb2 并解线性方程组(cornersubpix.cpp#L107-L130)。
  • 迭代上限:源码内部另有 MAX_ITERS = 100 的硬上限(cornersubpix.cpp#L49),同时要求输入为单通道图像,且图像尺寸至少为 2·win + 5,初始角点必须在图像范围内,否则会抛出异常(cornersubpix.cpp#L64-L66)。

示例代码:C++ / Python / Java

本教程在仓库内提供了三种语言的完整示例(原 markdown 通过 @include 指令直接嵌入源码文件):

示例默认读取测试图像 pic3.png(可通过 samples::findFilesamples/data/pic3.png 中找到),并先做 Shi-Tomasi 角点检测,再对检测结果做亚像素精化。

C++ 完整代码

/**
 * @function cornerSubPix_Demo.cpp
 * @brief Demo code for refining corner locations
 * @author OpenCV team
 */

#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/features.hpp"
#include <iostream>

using namespace cv;
using namespace std;

/// Global variables
Mat src, src_gray;

int maxCorners = 10;
int maxTrackbar = 25;

RNG rng(12345);
const char* source_window = "Image";

/// Function header
void goodFeaturesToTrack_Demo( int, void* );

/**
 * @function main
 */
int main( int argc, char** argv )
{
    /// Load source image and convert it to gray
    CommandLineParser parser( argc, argv, "{@input | pic3.png | input image}" );
    src = imread( samples::findFile( parser.get<String>( "@input" ) ) );
    if( src.empty() )
    {
        cout << "Could not open or find the image!\n" << endl;
        cout << "Usage: " << argv[0] << " <Input image>" << endl;
        return -1;
    }
    cvtColor( src, src_gray, COLOR_BGR2GRAY );

    /// Create Window
    namedWindow( source_window );

    /// Create Trackbar to set the number of corners
    createTrackbar( "Max corners:", source_window, &maxCorners, maxTrackbar, goodFeaturesToTrack_Demo );

    imshow( source_window, src );

    goodFeaturesToTrack_Demo( 0, 0 );

    waitKey();
    return 0;
}

/**
 * @function goodFeaturesToTrack_Demo.cpp
 * @brief Apply Shi-Tomasi corner detector
 */
void goodFeaturesToTrack_Demo( int, void* )
{
    /// Parameters for Shi-Tomasi algorithm
    maxCorners = MAX(maxCorners, 1);
    vector<Point2f> corners;
    double qualityLevel = 0.01;
    double minDistance = 10;
    int blockSize = 3, gradientSize = 3;
    bool useHarrisDetector = false;
    double k = 0.04;

    /// Copy the source image
    Mat copy = src.clone();

    /// Apply corner detection
    goodFeaturesToTrack( src_gray,
                         corners,
                         maxCorners,
                         qualityLevel,
                         minDistance,
                         Mat(),
                         blockSize,
                         gradientSize,
                         useHarrisDetector,
                         k );


    /// Draw corners detected
    cout << "** Number of corners detected: " << corners.size() << endl;
    int radius = 4;
    for( size_t i = 0; i < corners.size(); i++ )
    {
        circle( copy, corners[i], radius, Scalar(rng.uniform(0,255), rng.uniform(0, 256), rng.uniform(0, 256)), FILLED );
    }

    /// Show what you got
    namedWindow( source_window );
    imshow( source_window, copy );

    /// Set the needed parameters to find the refined corners
    Size winSize = Size( 5, 5 );
    Size zeroZone = Size( -1, -1 );
    TermCriteria criteria = TermCriteria( TermCriteria::EPS + TermCriteria::COUNT, 40, 0.001 );

    /// Calculate the refined corner locations
    cornerSubPix( src_gray, corners, winSize, zeroZone, criteria );

    /// Write them down
    for( size_t i = 0; i < corners.size(); i++ )
    {
        cout << " -- Refined Corner [" << i << "]  (" << corners[i].x << "," << corners[i].y << ")" << endl;
    }
}

代码逻辑分四步:

  1. 读取图像(命令行 @input 可指定路径,默认 pic3.png)并转灰度 COLOR_BGR2GRAY
  2. 调用 goodFeaturesToTrack 做 Shi-Tomasi 角点检测(质量水平 qualityLevel=0.01、最小间距 minDistance=10blockSize=3gradientSize=3、不使用 Harris 测度、k=0.04),得到整数级角点 vector<Point2f> corners
  3. 用随机颜色在图上画出检测到的角点,便于目视比对;
  4. 配置 cornerSubPix 参数后,就地(in-place)把 corners 精化为亚像素坐标并打印。

Python 完整代码

from __future__ import print_function
import cv2 as cv
import numpy as np
import argparse
import random as rng

source_window = 'Image'
maxTrackbar = 25
rng.seed(12345)

def goodFeaturesToTrack_Demo(val):
    maxCorners = max(val, 1)

    # Parameters for Shi-Tomasi algorithm
    qualityLevel = 0.01
    minDistance = 10
    blockSize = 3
    gradientSize = 3
    useHarrisDetector = False
    k = 0.04

    # Copy the source image
    copy = np.copy(src)

    # Apply corner detection
    corners = cv.goodFeaturesToTrack(src_gray, maxCorners, qualityLevel, minDistance, None, \
        blockSize=blockSize, gradientSize=gradientSize, useHarrisDetector=useHarrisDetector, k=k)

    # Draw corners detected
    print('** Number of corners detected:', corners.shape[0])
    radius = 4
    for i in range(corners.shape[0]):
        cv.circle(copy, (int(corners[i,0,0]), int(corners[i,0,1])), radius, (rng.randint(0,256), rng.randint(0,256), rng.randint(0,256)), cv.FILLED)

    # Show what you got
    cv.namedWindow(source_window)
    cv.imshow(source_window, copy)

    # Set the needed parameters to find the refined corners
    winSize = (5, 5)
    zeroZone = (-1, -1)
    criteria = (cv.TERM_CRITERIA_EPS + cv.TermCriteria_COUNT, 40, 0.001)

    # Calculate the refined corner locations
    corners = cv.cornerSubPix(src_gray, corners, winSize, zeroZone, criteria)

    # Write them down
    for i in range(corners.shape[0]):
        print(" -- Refined Corner [", i, "]  (", corners[i,0,0], ",", corners[i,0,1], ")")

# Load source image and convert it to gray
parser = argparse.ArgumentParser(description='Code for Shi-Tomasi corner detector tutorial.')
parser.add_argument('--input', help='Path to input image.', default='pic3.png')
args = parser.parse_args()

src = cv.imread(cv.samples.findFile(args.input))
if src is None:
    print('Could not open or find the image:', args.input)
    exit(0)

src_gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)

# Create a window and a trackbar
cv.namedWindow(source_window)
maxCorners = 10 # initial threshold
cv.createTrackbar('Threshold: ', source_window, maxCorners, maxTrackbar, goodFeaturesToTrack_Demo)
cv.imshow(source_window, src)
goodFeaturesToTrack_Demo(maxCorners)

cv.waitKey()

两种语言的行为完全等价。窗口上方的 “Max corners / Threshold” 滑条(范围 1–25,默认 10)让用户可以实时调节要检测的角点数量,每次拖动都会重新执行检测与精化流程,交互式地观察不同角点数量下的结果。

参数详解:检测阶段与精化阶段

整个示例的参数可分为“粗检测”与“亚像素精化”两阶段。

粗检测阶段(goodFeaturesToTrack)

参数 示例取值 含义
maxCorners 10(滑条 1–25 可调) 最多返回的角点数,代码中用 max(maxCorners, 1) 保证至少检测 1 个
qualityLevel 0.01 最小特征值阈值(相对最大特征值的比例),低于此值的候选角点被丢弃
minDistance 10 角点之间的最小欧氏距离,用于去重,避免角点扎堆
blockSize 3 计算导数协方差矩阵时的邻域块大小
gradientSize 3 Sobel 梯度算子孔径大小
useHarrisDetector false 为 false 时用 Shi-Tomasi 的最小特征值准则,为 true 时用 Harris 角点响应,需配合 k
k 0.04 Harris 响应公式中的自由参数,仅当 useHarrisDetector=true 时生效

亚像素精化阶段(cornerSubPix)

函数原型(modules/imgproc/include/opencv2/imgproc.hpp#L1802-L1804):

void cv::cornerSubPix( InputArray image, InputOutputArray corners,
                       Size winSize, Size zeroZone,
                       TermCriteria criteria );
参数 示例取值 含义与底层影响
image src_gray 输入单通道 8-bit 或浮点图像。源码要求 src.channels() == 1,且图像尺寸需满足 src.cols >= win.width*2 + 5src.rows >= win.height*2 + 5(见 cornersubpix.cpp#L64-L66
corners Shi-Tomasi 输出 既是输入又是输出:传入初始整数坐标,函数返回时被原地改写为精化后的亚像素坐标(C++ 中为 vector<Point2f>,需 CV_32F
winSize (5, 5) 搜索窗口“半边”长度。API 明确说明:winSize=Size(5,5) 实际对应 (5*2+1)×(5*2+1)=11×11 的搜索窗(imgproc.hpp#L1792-L1793
zeroZone (-1, -1) 搜索窗中央“死区”的半边长,此区域内不参与梯度求和,用于规避自相关矩阵奇异性;(-1,-1) 表示不设死区(imgproc.hpp#L1794-L1797)。源码中要求其小于搜索窗尺寸的一半,否则忽略
criteria EPS+COUNT,maxCount=40,eps=0.001 迭代终止条件:达到 maxCount 次迭代,或角点单次位移小于 epsilon 即停止(imgproc.hpp#L1798-L1800)。源码内部最多迭代 MAX_ITERS=100 次,且比较时用的是 epsilon²

经验取值提示(来自 API 文档与示例):winSize 越大,参与自相关矩阵求和的邻域越大,结果越平滑但对模糊区域可能偏移;对普通图像 winSize=(5,5)zeroZone=(-1,-1) 是教程示例的稳妥默认。若角点间距很近或纹理杂乱,可通过 criteriaepsilon(如 0.001)控制精度与迭代成本之间的平衡。

运行与结果验证

运行任一语言示例并调节滑条后,控制台会输出类似:

** Number of corners detected: 10
 -- Refined Corner [0]  (180.45, 38.97)
 -- Refined Corner [1]  (150.31, 85.12)
 ...

注意精化后的坐标已带小数,这正是 cornerSubPix 迭代收敛的结果。

教程文档给出了两组效果图。检测与精化所使用的原始图像为:

corner_subpixels 原始输入图像

对原始图像执行 Shi-Tomasi 检测并用实心圆标出、随后做亚像素精化的结果如下——所有角点均落在棋盘格内角的真实顶点附近:

corner_subpixels 角点检测与精化结果

调试与边界条件提示

把本示例迁移到自己的数据时,结合源码的约束条件需要注意几点:

  • 传给 cornerSubPix 的图像必须是灰度单通道(8-bit 或浮点)。直接传 BGR 彩色图会触发源码中的 CV_Assert(src.channels() == 1)
  • 初始角点必须落在图像内,否则源码会抛出 StsOutOfRange 异常(cornersubpix.cpp#L99-L103)。用 goodFeaturesToTrack 产出的角点天然满足该条件,但若自造角点列表需自行剔除越界点。
  • 搜索窗尺寸不宜超出图像边界过近,源码对图像最小尺寸有硬性要求(2*winSize + 5)。
  • 亚像素精化对初始位置有一定依赖,通常先用整数级检测器给出足够接近的初值,再交给 cornerSubPix 收敛;初值偏离真实角点过远时,迭代可能落入局部最优。

进阶阅读

在仓库中继续深入这一主题,可以参考以下资源:

把“检测出角点”升级为“精确到小数位的角点”,正是从玩具 Demo 走向相机标定等工程精度要求的第一步。

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