首页
/ OpenCV Harris 角点检测实战详解:cornerHarris 原理、参数调参与完整示例

OpenCV Harris 角点检测实战详解:cornerHarris 原理、参数调参与完整示例

2026-09-06 18:33:43作者:董灵辛Dennis

本教程围绕 OpenCV 图像特征检测中的经典问题——Harris 角点(兴趣点)检测展开。你将理解"特征"在计算机视觉中的含义、角点为何能作为稳定特征,掌握 Harris-Stephens 方法的完整数学推导,并学会使用 cv::cornerHarrisC++ 示例Python 示例 中落地,最后还能借助源码级剖析掌握 blockSize、apertureSize、k 等核心参数的实际作用,为图像拼接、立体匹配、运动跟踪等后续任务打牢基础。

目标

阅读并动手运行本文对应教程(原始教程文档)后,你将掌握:

  • 特征(feature)是什么,以及它为什么重要——两帧图像间的匹配点本质上就是一组易识别的场景特征;
  • 常见图像特征的类型划分;
  • 使用函数 cv::cornerHarris 通过 Harris-Stephens 方法检测角点。

理论

什么是特征?

在计算机视觉中,我们经常需要在同一环境的不同帧之间寻找匹配点。原因在于:只要知道两幅图像之间的对应关系,就能利用"两幅"图像联合提取信息,例如估算相机运动、恢复场景三维结构等。

这里所说的"匹配点",一般指场景中容易被识别的特征(features)。那么,什么样的特征才算合格?最基本的准则是:

  • 特征必须是唯一可识别的——在场景中独一无二、便于在两帧之间稳定对应。

图像特征的类型

常见的图像特征(或特征载体)大致分三类:

  • 边缘(Edges)
  • 角点(Corners),也称"兴趣点(interest points)"
  • 团块(Blobs),也称"感兴趣区域(regions of interest)"

本教程聚焦研究 角点 这一类特征。

为什么角点如此特殊?

角点是两条边缘的交汇处,它代表图像中两个边缘方向同时发生变化的点。正因如此,该点在图像两个方向上的梯度都会出现较大的变化——这一特性正是我们用来检测它的依据。

它是如何工作的?

既然角点对应图像梯度的剧烈变化,我们就围绕"梯度变化"做文章。考虑一幅灰度图像 I,我们用一个小窗口 w(x,y) 在图像上滑动,窗口在 x 方向位移 u、在 y 方向位移 v,逐点计算窗口内强度(灰度)的变化量:

E(u,v) = Σ w(x,y) · [ I(x+u, y+v) − I(x,y) ]²

其中:

  • w(x,y) 是位置 (x,y) 处的窗口函数;
  • I(x,y) 是位置 (x,y) 处的灰度强度;
  • I(x+u, y+v) 是窗口移动到 (x+u, y+v) 后的灰度强度。

由于我们要找的是"包含角点的窗口",也就是灰度强度变化极大的窗口,因此需要最大化上面的能量项,尤其是其中求和部分:

Σ [ I(x+u, y+v) − I(x,y) ]²

对内部项做 泰勒展开(只保留一阶导数),可得近似:

E(u,v) ≈ Σ [ I(x,y) + u·Iₓ + v·I_y − I(x,y) ]²

约去 I(x,y) 并展开、合并同类项后:

E(u,v) ≈ Σ ( u²·Iₓ² + 2uv·Iₓ·I_y + v²·I_y² )

将其写成矩阵形式:

E(u,v) ≈ [u v] · M · [u v]ᵀ

其中协方差矩阵 M 定义为(窗口加权求和):

M = Σ w(x,y) · [ Iₓ² IₓI_y ; IₓI_y I_y² ]

也就是说,能量函数最终可表达为二次型:

E(u,v) ≈ [u v] · M · [u v]ᵀ

对每个窗口计算一个响应得分,用于判断它是否可能包含角点:

R = det(M) − k · (trace(M))²

其中,k 是 Harris 自由参数(经验取值约为 0.04~0.06),并且:

  • det(M) = λ₁ · λ₂
  • trace(M) = λ₁ + λ₂

(λ₁、λ₂ 是矩阵 M 的两个特征值。)

直观理解:

  • 若 R 绝对值很小(两个特征值都小),说明该区域灰度近似恒定——属于平坦区域
  • 若 R 为负值(一个特征值远大于另一个),说明该区域只在一个方向有梯度变化——属于边缘
  • 若 R 为较大的正值(两个特征值都较大),说明该区域在两个方向都存在明显梯度变化——这就是我们想要的角点

因此,当某个窗口的响应 R 大于某一设定阈值时,该窗口中心即被判定为角点

cornerHarris 函数签名与参数

modules/imgproc/include/opencv2/imgproc.hpp 中,函数声明如下:

CV_EXPORTS_W void cornerHarris( InputArray src, OutputArray dst, int blockSize,
                                int ksize, double k, int borderType = BORDER_DEFAULT );

各参数含义:

参数 含义 示例取值 / 说明
src 输入图像,单通道 8 位或浮点图像 实际使用前通常用 cvtColor(..., COLOR_BGR2GRAY) 转灰度
dst 输出角点响应图,类型为 CV_32FC1,尺寸与 src 相同 每个像素存一个 R 得分
blockSize 协方差矩阵求和时的邻域尺寸(blockSize × blockSize) 常见取 2、3
ksize Sobel 算子(求导)的孔径参数 必须为奇数,常见 3;取 -1 时内部改用 Scharr 算子
k Harris 检测器的自由参数 经验范围 0.04~0.06,本教程代码取 0.04
borderType 像素外插方法(见 BorderTypes),不支持 BORDER_WRAP 默认 BORDER_DEFAULT

OpenCV 对函数的说明与本文理论推导一致:对每个像素 (x,y),在其 blockSize × blockSize 邻域上计算 2×2 梯度协方差矩阵 M,再计算特征值

dst(x,y) = det(M) − k·(tr M)²

最终图像中的角点可视为该响应图的局部极大值。因此,"阈值化 + 局部极大值抑制"是使用 cornerHarris 输出的标准后处理手段。

兼容性提示:本教程及示例适用于 OpenCV ≥ 3.0

完整示例代码

教程配套的完整演示程序见 cornerHarris_Demo.cpp(C++)与 cornerHarris_Demo.py(Python)。两者的流程完全一致:加载图像 → 转灰度 → 用 trackbar 交互式调节阈值 → 在响应图上圈出角点。

C++ 版本(核心逻辑)

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

using namespace cv;
using namespace std;

Mat src, src_gray;
int thresh = 200;
int max_thresh = 255;

const char* source_window = "Source image";
const char* corners_window = "Corners detected";

void cornerHarris_demo( int, void* );

int main( int argc, char** argv )
{
    CommandLineParser parser( argc, argv, "{@input | building.jpg | 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 );

    namedWindow( source_window );
    createTrackbar( "Threshold: ", source_window, &thresh, max_thresh, cornerHarris_demo );
    imshow( source_window, src );

    cornerHarris_demo( 0, 0 );

    waitKey();
    return 0;
}

void cornerHarris_demo( int, void* )
{
    int blockSize = 2;
    int apertureSize = 3;
    double k = 0.04;

    Mat dst = Mat::zeros( src.size(), CV_32FC1 );
    cornerHarris( src_gray, dst, blockSize, apertureSize, k );

    Mat dst_norm, dst_norm_scaled;
    normalize( dst, dst_norm, 0, 255, NORM_MINMAX, CV_32FC1, Mat() );
    convertScaleAbs( dst_norm, dst_norm_scaled );

    for( int i = 0; i < dst_norm.rows ; i++ )
    {
        for( int j = 0; j < dst_norm.cols; j++ )
        {
            if( (int) dst_norm.at<float>(i,j) > thresh )
            {
                circle( dst_norm_scaled, Point(j,i), 5, Scalar(0), 2, 8, 0 );
            }
        }
    }

    namedWindow( corners_window );
    imshow( corners_window, dst_norm_scaled );
}

编译运行方式(默认读入 building.jpg,该路径通过 samples::findFilesamples/data 目录中解析):

# 以命令行参数指定输入图像
./cornerHarris_Demo <image_path>

Python 版本(核心逻辑)

import cv2 as cv
import numpy as np
import argparse

source_window = 'Source image'
corners_window = 'Corners detected'
max_thresh = 255

def cornerHarris_demo(val):
    thresh = val

    # Detector parameters
    blockSize = 2
    apertureSize = 3
    k = 0.04

    # Detecting corners
    dst = cv.cornerHarris(src_gray, blockSize, apertureSize, k)

    # Normalizing
    dst_norm = np.empty(dst.shape, dtype=np.float32)
    cv.normalize(dst, dst_norm, alpha=0, beta=255, norm_type=cv.NORM_MINMAX)
    dst_norm_scaled = cv.convertScaleAbs(dst_norm)

    # Drawing a circle around corners
    for i in range(dst_norm.shape[0]):
        for j in range(dst_norm.shape[1]):
            if int(dst_norm[i,j]) > thresh:
                cv.circle(dst_norm_scaled, (j,i), 5, (0), 2)

    # Showing the result
    cv.namedWindow(corners_window)
    cv.imshow(corners_window, dst_norm_scaled)

parser = argparse.ArgumentParser(description='Code for Harris corner detector tutorial.')
parser.add_argument('--input', help='Path to input image.', default='building.jpg')
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)

cv.namedWindow(source_window)
thresh = 200  # initial threshold
cv.createTrackbar('Threshold: ', source_window, thresh, max_thresh, cornerHarris_demo)
cv.imshow(source_window, src)
cornerHarris_demo(thresh)

cv.waitKey()

代码逐段讲解

1. 加载图像并转为灰度

cornerHarris 要求单通道输入,因此先用 cvtColor(src, src_gray, COLOR_BGR2GRAY) 把 BGR 彩色图转成灰度图;若图像为空(路径错误或文件缺失),程序打印用法并退出。

2. 设置检测器参数并执行检测

int blockSize = 2;      // 梯度协方差矩阵的邻域求和尺寸
int apertureSize = 3;   // Sobel 求导孔径
double k = 0.04;        // Harris 自由参数
Mat dst = Mat::zeros( src.size(), CV_32FC1 );
cornerHarris( src_gray, dst, blockSize, apertureSize, k );

注意输出 dstCV_32FC1(单通道 32 位浮点)的响应图,数值范围通常远小于 0~255,直接可视化会很暗,因此需要归一化。

3. 归一化并转成可显示的 8 位图像

normalize( dst, dst_norm, 0, 255, NORM_MINMAX, CV_32FC1, Mat() );
convertScaleAbs( dst_norm, dst_norm_scaled );
  • normalize(..., NORM_MINMAX) 把响应图线性拉伸到 [0, 255] 区间,方便与 trackbar 的阈值(0~255)直接比较;
  • convertScaleAbs 将浮点数据取绝对值并转换为 CV_8U,得到可显示的灰度响应图。

4. 阈值化并圈出角点

for( int i = 0; i < dst_norm.rows ; i++ )
    for( int j = 0; j < dst_norm.cols; j++ )
        if( (int) dst_norm.at<float>(i,j) > thresh )
            circle( dst_norm_scaled, Point(j,i), 5, Scalar(0), 2, 8, 0 );

对归一化后的响应图逐像素比较:凡是响应值大于当前阈值的像素,就在该位置 (j,i) 画一个半径 5、线宽 2 的黑色小圆圈。注意这只是一个"像素级"的简单阈值响应(未做非极大值抑制),主要用于直观演示;实际工程中通常会先做局部极大值提取。

5. 交互式阈值调节

createTrackbar( "Threshold: ", source_window, &thresh, max_thresh, cornerHarris_demo );

在源图像窗口上创建一个 0~255 的滑动条,滑块变化即触发 cornerHarris_demo 重新检测,实现阈值实时调节——阈值越低检测出的"角点"越多(噪声也越多),阈值越高只保留最强的角点。

源码级剖析:cornerHarris 在 OpenCV 内部如何工作

Harris 角点检测的实现集中在 modules/imgproc/src/corner.cpp。公开入口 cv::cornerHarris 会先尝试 OpenCL(ocl_cornerMinEigenValVecs)与 IPP(ipp_cornerHarris,见 corner.cpp)两条加速路径,最终统一落到核心函数 cornerEigenValsVecs,其内部步骤清晰对应本文的数学推导:

  1. 计算梯度:当 aperture_size > 0 时用 Sobel 分别求 Iₓ、I_y;当 aperture_size < 0 时改用 Scharr(corner.cpp)。注意内部还按孔径与深度施加了 scale 缩放因子,对 8 位图像额外乘 255 以补偿量化幅度。

  2. 构造协方差数据:逐像素计算三通道协方差 (Iₓ², Iₓ·I_y, I_y²),即 M 矩阵的三个独立分量,存入 CV_32FC3covcorner.cpp)。

  3. 窗口求和:用 boxFilterblockSize × blockSize 的盒式窗口对协方差各通道做均值/求和滤波(corner.cpp),这一步实现了公式中 Σ w(x,y)·[...] 的邻域聚合——这也正是 blockSize 越大、角点响应越平滑的原因。

  4. 计算响应得分op_type == HARRIS 时调用 calcHarris,对每个像素执行:

    dst = a*c - b*b - k*(a + c)*(a + c);
    

    其中 a = ΣIₓ²b = ΣIₓI_yc = ΣI_y²,即 det(M) − k·trace(M)² 的精确展开(corner.cpp)。

同一份协方差流水线还服务于一族的角点/特征值函数(corner.cpp):

  • cornerMinEigenVal:取两个特征值中较小者 min(λ₁, λ₂) 作为响应(对应 MINEIGENVAL 分支);
  • cornerEigenValsAndVecs:输出完整的特征值与特征向量(对应 EIGENVALSVECS 分支,输出为 6 通道 CV_32FC(6));
  • cornerHarris:使用 R = det(M) − k·trace(M)²(对应 HARRIS 分支)。

这些 API 的完整参数说明与公式注释均可查阅 imgproc.hpp;OpenCL 路径与 CPU 路径的结果一致性由 modules/imgproc/test/ocl/test_imgproc.cppOCL_OFF / OCL_ON 两组调用对比验证。

结果说明

在源图像(例如教程自带的建筑图片 building.jpg)上运行上述程序后:

  • 左窗口显示原始图像;
  • 拖动阈值滑块时,右窗口 "Corners detected" 实时刷新;
  • 凡是响应值 R 超过阈值的像素位置(即图像中强角点,例如建筑的窗角、门框角、墙体转角)都会被一个黑色小圆圈标出。

当阈值过高时圆圈数量骤减甚至消失;当阈值过低时平坦区域与边缘处的伪响应也会被圆圈标出。建议从 200 附近开始调节,观察建筑结构中真正"两方向梯度都剧烈变化"的交点被选中,即可直观理解 Harris 响应得分的含义。

延伸阅读

  • 本教程的"兄弟篇"**Shi-Tomasi 角点检测(Good Features to Track)**基于最小特征值选取角点,是 Harris 的改进变体,见 good_features_to_track.markdown(本教程末尾亦以 @next_tutorial 指向它);
  • 想了解"更通用"的角点检测框架(将 Harris 得分作为 cornerEigenValsAndVecs 输出的组合),可阅读 generic_corner_detector.markdown
  • 同系列特征与跟踪教程目录见 table_of_content_features.markdown
  • 在真实项目中,对响应图做完阈值化后通常还要配合 cornerSubPix 做亚像素级精化、或用 goodFeaturesToTrack 做质量排序与均匀化,以保证后续特征匹配的稳定性。
登录后查看全文
热门项目推荐
相关项目推荐