角点 (corners) 的定义有两个版本:一是 两条边缘的交点,二是 邻域内具有两个主方向的特征点。
一般而言,角点是边缘曲线上曲率为极大值的点,或者 图像亮度发生剧烈变化的点。例如,从人眼角度来看,下图的 $E$ 和 $F$ 便是典型的角点
1 检测思路
在图像中定义一个局部小窗口,然后沿各个方向移动这个窗口,则会出现 a) b) c) 三种情况,分别对应平坦区、边缘和角点
a) 窗口内的图像强度,在窗口向各个方向移动时,都没有发生变化,则窗口内都是 “平坦区”,不存在角点
b) 窗口内的图像强度,在窗口向某一个 (些) 方向移动时,发生较大变化;而在另一些方向不发生变化,那么,窗口内可能存在 “边缘”
c) 窗口内的图像强度,在窗口向各个方向移动时,都发生了较大的变化,则认为窗口内存在 “角点”
a) flat region b) edge c) corner
2 Harris 角点
2.1 公式推导
图像在点 $(x,y) $ 处的灰度值为 $I(x, y)$,当在 $x$ 方向上平移 $u$,且 $y$ 方向上平移 $v$ 时,图像灰度值的变化为
$ qquad E(u,v) = sumlimits_{x,y} , underbrace{w(x,y)}_text{window function} ; [underbrace{I(x u, y v)}_text{shifted intensity} - underbrace{I(x, y)}_text{intensity}]^2 $
$I(x,y)$ 的偏导数分别记为 $I_x$ 和 $I_y$,则上式用一阶泰勒级数近似展开
$ qquad sumlimits_{x,y} ; [I(x u, y v) - I(x, y)]^2 approx sumlimits_{x,y} ; [I(x, y) uI_x vI_y - I(x, y)]^2 = sumlimits_{x,y} ; [u^2I_x^2 2uvI_x I_y v^2I_y^2 ] $
写成矩阵形式
$ qquad E(u,v) approx begin{bmatrix} u & v end{bmatrix} left ( displaystyle sum_{x,y} w(x,y) begin{bmatrix} I_x^{2} & I_{x}I_{y} \ I_xI_{y} & I_{y}^{2} end{bmatrix} right ) begin{bmatrix} u \ v end{bmatrix}$
则有
$ qquad E(u,v) approx begin{bmatrix} u & v end{bmatrix} M begin{bmatrix} u \ v end{bmatrix}$, 假定 $ M = displaystyle sum_{x,y} w(x,y) begin{bmatrix} I_x^{2} & I_{x}I_{y} \ I_xI_{y} & I_{y}^{2} end{bmatrix}$
2.2 判别方法
定义一个角点响应值$qquad R = det(M) - k(trace(M))^{2} = lambda_{1} lambda_{2} - k (lambda_{1} lambda_{2})^2 $
根据响应值的大小,判断小窗口内是否包含角点:
1) “平坦区”:|R| 小的区域,即 $lambda_1$ 和 $lambda_2$ 都小;
2) “边缘”: R <0 的区域,即 $lambda_1 >> lambda_2$ 或反之;
3) “角点”: R 大的区域,即 $lambda_1$ 和 $lambda_2$ 都大且近似相等
为了便于直观理解,绘制成 $lambda_1-lambda_2$ 平面如下图:
2.3 cornerHarris()
OpenCV 中 Harris 角点检测的函数为:
代码语言:javascript复制void cv::cornerHarris (
InputArray src, // 输入图像 (单通道,8位或浮点型)
OutputArray dst, // 输出图像 (类型 CV_32FC1,大小同 src)
int blockSize, // 邻域大小
int ksize, // Sobel 算子的孔径大小
double k, // 经验参数,取值范围 0.04 ~ 0.06
int borderType = BORDER_DEFAULT // 边界模式
)
2.4 代码示例
代码语言:javascript复制#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
using namespace cv;
// Harris corner parameters
int kThresh = 150;
int kBlockSize = 2;
int kApertureSize = 3;
double k = 0.04;
int main()
{
// read image
Mat src, src_gray;
src = imread("building.jpg");
if(src.empty())
return -1;
cvtColor(src, src_gray, COLOR_BGR2GRAY);
Mat dst, dst_norm, dst_norm_scaled;
// Harris corner detect
cornerHarris(src_gray, dst, kBlockSize, kApertureSize, k);
normalize(dst, dst_norm, 0, 255, NORM_MINMAX, CV_32FC1);
convertScaleAbs(dst_norm, dst_norm_scaled);
// draw detected corners
for(int j=0; j < dst_norm.rows; j )
{
for(int i=0; i<dst_norm.cols;i )
{
if((int)dst_norm.at<float>(j,i) > kThresh)
{
circle(src, Point(i, j), 2, Scalar(0,255,0));
}
}
}
imshow("harris corner", src);
waitKey(0);
}
检测结果:
3 Shi-Tomasi 角点
Shi-Tomasi 角点是 Harris 角点的改进,在多数情况下,其检测效果要优于 Harris。二者的区别在于,Shi-Tomasi 选取 $lambda_1$ 和 $lambda_2$ 中的最小值,作为新的角点响应值 $R$
$qquad R = min(lambda_1, lambda_2) $
则相应的 $lambda_1-lambda_2$ 平面为:
3.1 goodFeaturesToTrack()
OpenCV 中 Shi-Tomasi 角点检测函数为:
代码语言:javascript复制void cv::goodFeaturesToTrack (
InputArray image, // 输入图像 (单通道,8位或浮点型32位)
OutputArray corners, // 检测到的角点
int maxCorners, // 最多允许返回的角点数量
double qualityLevel, //
double minDistance, // 角点间的最小欧拉距离
InputArray mask = noArray(), //
int blockSize = 3, //
bool useHarrisDetector = false, //
double k = 0.04 //
)
3.2 代码示例
代码语言:javascript复制#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
using namespace cv;
using namespace std;
int kMaxCorners = 1000;
double kQualityLevel = 0.1;
double kMinDistance = 1;
int main()
{
// read image
Mat src, src_gray;
src = imread("building.jpg");
if (src.empty())
return -1;
cvtColor(src, src_gray, COLOR_BGR2GRAY);
// Shi-Tomasi corner detect
vector<Point2f> corners;
goodFeaturesToTrack(src_gray, corners, kMaxCorners, kQualityLevel, kMinDistance);
// draw and show detected corners
for (size_t i = 0; i < corners.size(); i )
{
circle(src, corners[i], 2.5, Scalar(0, 255, 0));
}
imshow("Shi-Tomasi corner", src);
waitKey(0);
}
检测结果:
4 角点检测的实现
分析 cornerHarris() 函数的源码,复现求解步骤:Sobel 算子求解 dx 和 dy -> 矩阵 M -> boxFilter -> 每个像素的角点响应值 R
C 代码实现如下:
代码语言:javascript复制#include <iostream>
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
using namespace cv;
using namespace std;
int kApertureSize = 3;
int kBlockSize = 2;
double k = 0.04;
int kThresh = 150;
int main()
{
// read image
Mat src, src_gray;
src = imread("chessboard.png");
if (src.empty())
return -1;
cvtColor(src, src_gray, COLOR_BGR2GRAY);
// determine scale
double scale = (double)(1 << (kApertureSize - 1)) * kBlockSize;
scale *= 255.0;
scale = 1.0 / scale;
// 1) dx, dy
Mat Dx, Dy;
Sobel(src_gray, Dx, CV_32F, 1, 0, kApertureSize, scale);
Sobel(src_gray, Dy, CV_32F, 0, 1, kApertureSize, scale);
// 2) cov matrix
Size size = src_gray.size();
Mat cov(size, CV_32FC3);
for (int i = 0; i < size.height; i )
{
float* cov_data = cov.ptr<float>(i);
const float* dxdata = Dx.ptr<float>(i);
const float* dydata = Dy.ptr<float>(i);
for (int j=0; j < size.width; j )
{
float dx = dxdata[j];
float dy = dydata[j];
cov_data[j * 3] = dx * dx;
cov_data[j * 3 1] = dx * dy;
cov_data[j * 3 2] = dy * dy;
}
}
// 3) box filter
boxFilter(cov, cov, cov.depth(), Size(kBlockSize, kBlockSize), Point(-1,-1), false);
// 4) R
Mat dst(size,CV_32FC1);
Size size_cov = cov.size();
for (int i = 0; i < size_cov.height; i )
{
const float* ptr_cov = cov.ptr<float>(i);
float* ptr_dst = dst.ptr<float>(i);
for (int j=0; j < size_cov.width; j )
{
float a = ptr_cov[j * 3];
float b = ptr_cov[j * 3 1];
float c = ptr_cov[j * 3 2];
ptr_dst[j] = (float)(a * c - b * b - k * (a c) * (a c));
}
}
#if HARRIS_OPENCV // compare with cornerHarris()
cornerHarris(src_gray, dst, kBlockSize, kApertureSize, k);
#endif
// 5) normalization
Mat dst_norm, dst_norm_scaled;
normalize(dst, dst_norm, 0, 255, NORM_MINMAX, CV_32FC1);
convertScaleAbs(dst_norm, dst_norm_scaled);
// 6) drawing corners
for (int j = 0; j < dst_norm.rows; j )
{
for (int i = 0; i < dst_norm.cols; i )
{
if ((int)dst_norm.at<float>(j, i) > 150)
{
circle(src, Point(i, j), 2, Scalar(0, 255, 0));
}
}
}
imshow("Harris corner", src);
waitKey(0);
}
检测结果:将求得的角点响应值$R$,输出 txt 文件,与 cornerHarris() 输出的 $R$ 进行比较,结果几乎完全相同 (只有几处小数点后7位的值不同)
5 亚像素角点检测
亚像素角点的提取函数 cornerSubPix(),常用于相机标定中,定义如下:
5.1 cornerSubpix()
代码语言:javascript复制void cv::cornerSubPix(
InputArray image, // 输入图象(单通道,8位或浮点型)
InputOutputArray corners, // 亚像素精度的角点坐标
Size winSize, // 搜索窗口尺寸的 1/2
Size zeroZone, //
TermCriteria criteria // 迭代终止准则
)
5.2 代码示例
代码语言:javascript复制#include <iostream>
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
using namespace cv;
using namespace std;
int kMaxCorners = 40;
double kQualityLevel = 0.01;
double kMinDistance = 50;
int main()
{
// 1) read image
Mat src, src_gray;
src = imread("chessboard.png");
if (src.empty())
return -1;
cvtColor(src, src_gray, COLOR_BGR2GRAY);
// 2) Shi-Tomasi corner detect
vector<Point2f> corners;
goodFeaturesToTrack(src_gray, corners, kMaxCorners, kQualityLevel, kMinDistance);
// 3) draw and show detected corners
for (size_t i = 0; i < corners.size(); i )
{
circle(src, corners[i], 3, Scalar(0, 255, 0));
}
imshow("Shi-Tomasi corner", src);
TermCriteria criteria = TermCriteria(TermCriteria::EPS TermCriteria::COUNT, 40, 0.001);
// 4) find corner positions in subpixel
cornerSubPix(src_gray, corners, Size(5, 5), Size(-1, -1), criteria);
// 5) output subpixel corners
for (size_t i = 0; i < corners.size(); i )
{
cout << "Corner[" << i << "]: (" << corners[i].x << "," << corners[i].y << ")" << endl;
}
waitKey(0);
}
输入棋盘格5行8列,对应7x4个角点,图像的分辨率为 600*387,则所有角点的理论坐标如下表:
角点的图象坐标值输出如下:
参考资料:
《图像局部不变性特征与描述》 第 3 章
https://www.cnblogs.com/ronny/p/4009425.html http://www.cse.psu.edu/~rtc12/CSE486/ OpenCV Tutorials / feature2d module / Harris corner detector OpenCV-Python Tutorials / Feature Detection and Description / Shi-Tomasi Corner Detector & Good Features to Track OpenCV Tutorials / feature2d module / Detecting corners location in subpixels