OpenCV Harris 角点检测实战详解:cornerHarris 原理、参数调参与完整示例
本教程围绕 OpenCV 图像特征检测中的经典问题——Harris 角点(兴趣点)检测展开。你将理解"特征"在计算机视觉中的含义、角点为何能作为稳定特征,掌握 Harris-Stephens 方法的完整数学推导,并学会使用 cv::cornerHarris 在 C++ 示例 与 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::findFile 在 samples/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 );
注意输出 dst 是 CV_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,其内部步骤清晰对应本文的数学推导:
-
计算梯度:当
aperture_size > 0时用 Sobel 分别求 Iₓ、I_y;当aperture_size < 0时改用 Scharr(corner.cpp)。注意内部还按孔径与深度施加了scale缩放因子,对 8 位图像额外乘 255 以补偿量化幅度。 -
构造协方差数据:逐像素计算三通道协方差
(Iₓ², Iₓ·I_y, I_y²),即 M 矩阵的三个独立分量,存入CV_32FC3的cov(corner.cpp)。 -
窗口求和:用
boxFilter以blockSize × blockSize的盒式窗口对协方差各通道做均值/求和滤波(corner.cpp),这一步实现了公式中Σ w(x,y)·[...]的邻域聚合——这也正是blockSize越大、角点响应越平滑的原因。 -
计算响应得分:
op_type == HARRIS时调用calcHarris,对每个像素执行:dst = a*c - b*b - k*(a + c)*(a + c);其中
a = ΣIₓ²、b = ΣIₓI_y、c = Σ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.cpp 中 OCL_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做质量排序与均匀化,以保证后续特征匹配的稳定性。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00