前言
图像特征类型一共可以被分为如下三种:
- 边缘
- 角点(感兴趣关键点)
- 斑点(Blobs)(感兴趣区域)
其中角点是个很特殊的存在。我们把在某一点在任意方向的一个微小变化都会引起灰度很大的变化的点称之为角点。
关于角点的具体描述可以有如下几种:
- 一阶导数(即灰度的梯度)的局部最大所对应的像素点;
- 两条及两条以上边缘的交点;
- 图像中梯度值和梯度方向的变化速率都很高的点;
- 角点处的一阶导数最大,二阶导数为零,指示物体边缘变化不连续的方向
在当前的图像处理领域,角点检测算法可归纳为三类:
< 1 > 基于灰度图像的角点检测
< 2 > 基于二值图像的角点检测
< 3 > 基于轮廓曲线的角点检测
而基于灰度图像的角点检测又可分为基于梯度、基于模板和基于模板梯度组合三类方法,其中基于模板的方法主要考虑像素领域点的灰度变化,即图像亮度的变化,将与邻点亮度对比足够大的点定义为角点。常见的基于模板的角点检测算法有Kitchen-Rosenfeld角点检测算法,Harris角点检测算法、KLT角点检测算法及SUSAN角点检测算法。和其他角点检测算法相比,SUSAN角点检测算法具有算法简单、位置准确、抗噪声能力强等特点。
正文
1、Harris角点检测
OpenCV里面的API: cornerHarris()
C++:void cornerHarris( InputArray src, //输入8bit单通道灰度Mat矩阵
OutputArray dst,
//用于保存Harris角点检测结果,32位单通道,大小与src相同
int blockSize, //滑块窗口的尺寸
int ksize, //Sobel边缘检测滤波器大小
double k, //Harris中间参数,经验值0.04~0.06
int borderType=BORDER_DEFAULT //插值类型
);
算法
通过一系数学推导,泰勒级数等推导出图像每个像素点R值,对角点响应函数R进行阈值处理:R > threshold,即提取R的局部极大值(当R大于阈值时,我们认为这个点是角点)
这里Edge<0,Corner>0,Flat~0;
代码
# include <opencv2\opencv.hpp>
# include <iostream>
using namespace std;
using namespace cv;
Mat src, dst, src_gray;
int threshold_value = 130;
int threshold_max = 255;
RNG rng(12345);
void Demo_harris(int, void*);
int main(int argc, char**argv) {
src = imread("H:\\tuku\\harris.jpg");
if (!src.data)
{
cout << "can't find the picture...";
return -1;
}
imshow("in put", src);
namedWindow("out put", 1);
cvtColor(src, src_gray, CV_BGR2GRAY);
createTrackbar("value:", "out put", &threshold_value, threshold_max, Demo_harris);
Demo_harris(0, 0);
waitKey(0);
return 0;
}
void Demo_harris(int, void*) {
dst = Mat::zeros(src_gray.size(), CV_32FC1);
Mat scaledImage;
Mat normalize_dst;
Mat result = src.clone();
cornerHarris(src_gray, dst,2, 3, 0.04, BORDER_DEFAULT);
//通过角点检测得到的dst中有负值也有较大的值,通过归一化处理
normalize(dst, normalize_dst, 0, 255, NORM_MINMAX, CV_32FC1, Mat());
//将归一化后的图线性变换成8位无符号整型
convertScaleAbs(normalize_dst, scaledImage);
for (int row = 0; row< normalize_dst.rows; row++)
{
uchar *curentRow = scaledImage.ptr(row); // 这里用指针会更快一点;用读取像素怎么会出错?;
for (int col = 0; col< normalize_dst.cols; col++)
{
//int value = scaledImage.at<int>(row, col); 当用这个的时候为啥会出错?
int value = (int)*curentRow;
if (value> threshold_value)
{ //画圆
circle(result, Point(col, row), 5, Scalar(10, 10, 255), 2, 8, 0);
circle(scaledImage, Point(col, row), 5, Scalar(0, 10, 255), 2, 8, 0);
}
curentRow++;
}
}
imshow("out put", result);
imshow("shuchu", scaledImage);
}
效果
2、shi-tomas角点检测
API:goodFeaturesToTrack()
void goodFeaturesToTrack(
InputArray image,
OutputArray corners,
int maxCorners,
double qualityLevel,
double minDistance,
InputArray mask=noArray(),
int blockSize=3,
bool useHarrisDetector=false,
double k=0.04
);
第一个参数image:8位或32位单通道灰度图像;
第二个参数corners:位置点向量,保存的是检测到的角点的坐标;
vector< Point2f >的形式;
第三个参数maxCorners:定义可以检测到的角点的数量的最大值;
第四个参数qualityLevel:检测到的角点的质量等级,角点特征值小于qualityLevel*最大特征值的点将被舍弃;
第五个参数minDistance:两个角点间最小间距,以像素为单位;
第六个参数mask:指定检测区域,若检测整幅图像,mask置为空Mat();
第七个参数blockSize:计算协方差矩阵时窗口大小;
第八个参数useHarrisDetector:是否使用Harris角点检测,为false,则使用Shi-Tomasi算子;
第九个参数k:留给Harris角点检测算子用的中间参数,一般取经验值0.04~0.06。第八个参数为false时,该参数不起作用;
算法
与harris角点检测类似,
R = m i n ( λ 1 , λ 2 ) \ R = min(λ1, λ2) R=min(λ1,λ2)
代码
# include <opencv2\opencv.hpp>
# include <iostream>
using namespace std;
using namespace cv;
Mat src, dst,src_gray;
int num_corners = 25;
int corners_max = 60;
RNG rng(12345);
void Demo_shi_tomas(int, void*);
int main(int argc, char**argv) {
src = imread("H:\\tuku\\city.jpg");
if (!src.data)
{
cout << "can't find the picture...";
return -1;
}
imshow("in put", src);
namedWindow("out put", 1);
cvtColor(src, src_gray, CV_BGR2GRAY);
createTrackbar("value:", "out put", &num_corners, corners_max, Demo_shi_tomas);
Demo_shi_tomas(0, 0);
waitKey(0);
return 0;
}
void Demo_shi_tomas(int, void*) {
if (num_corners < 5) {
num_corners = 5;
}
vector<Point2f> corners;
Mat resultImag = src_gray.clone();
cvtColor(resultImag, resultImag, CV_GRAY2BGR);
goodFeaturesToTrack(src_gray, corners, corners_max, 0.01, 10, Mat(), 3, false, 0.04);
printf("corners is :%d\n", corners.size());
for (size_t i = 0; i < corners.size(); i++) {
circle(resultImag, corners[i], 3, Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), 2, 8, 0);
}
imshow("out put", resultImag);
}
效果图
自定义角点检测函数
----- 基于harris和shi-tomas角点检测的原理
----- 通过计算λ1, λ2两个特征值来去对角响应值 R 进行计算
----- 通过R计算合适的阈值
1、 cornerEigenValsAndVecs()
- 用来求解输入图像矩阵的特征向量与特征值
C++: void cornerEigenValsAndVecs(
InputArray src,
OutputArray dst,
int blockSize,
int ksize,
int borderType=BORDER_DEFAULT );
src:输入图像矩阵,即单通道8位或者浮点类型的图像。
dst:输出矩阵,即用来存储结果的图像,大小与输入图像一致并且为CV_32FC(6)类型。
blockSize:邻域大小一般取 3
ksize:Sobel算子当中的核大小,只能取1、3、5、7。
borderType:像素扩展的方法。
对于dst的详细解释:
CV_32FC(6)类,计算自相关矩阵M的特征值和特征向量,并将它们以(λ1, λ2, x1, y1, x2,y2)的形式存储在目标图像dst中。其中λ1, λ2是M未经过排序的特征值;x1, y1是对应于λ1的特征向量;x2, y2是对应于λ2的特征向量。因此是6通道的矩阵。
2、cornerMinEigenVal()函数
- cornerMinEigenVal()函数在角点检测中计算梯度矩阵的最小特征值
API:
C++: void cornerMinEigenVal(
InputArray src,
OutputArray dst,
int blockSize,
int ksize=3,
int borderType=BORDER_DEFAULT );
除了dst必须为CV_32FC1类型以外,其它与cornerEigenValsAndVecs()函数的一致。
代码:(综合)
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using namespace std;
// 定义全局变量
const string harris_winName = "自定义角点检测";
Mat src_img, gray_img; // src_img表示原图, gray_img表示灰度图
Mat harris_dst_img, harris_response_img; // harris_dst_img存储自相关矩阵M的特征值和特征向量,harris_response_img存储响应函数的结果
double min_respense_value; // 响应函数的结果矩阵中的最小值
double max_respense_value; // 响应函数的结果矩阵中的最大值
int qualityValue = 30;
int max_qualityValue = 100; // 通过qualityValue/max_qualityValue的结果作为qualitylevel来计算阈值
RNG random_number_generator; // 定义一个随机数发生器
void self_defining_Harris_Demo(int, void*); //TrackBar回调函数声明
// 主函数
int main()
{
src_img = imread("H:\\tuku\\biaoge.png");
if (src_img.empty())
{
printf("could not load the image...\n");
return -1;
}
namedWindow("原图", CV_WINDOW_AUTOSIZE);
imshow("原图", src_img);
cvtColor(src_img, gray_img, COLOR_BGR2GRAY); //将彩色图转化为灰度图
// 计算特征值
int blockSize = 3;
int ksize = 3;
double k = 0.04;
harris_dst_img = Mat::zeros(src_img.size(), CV_32FC(6));
// 目标图像harris_dst_img存储自相关矩阵M的特征值和特征向量,
// 并将它们以(λ1, λ2, x1, y1, x2, y2)的形式存储。其中λ1, λ2是M未经过排序的特征值;
// x1, y1是对应于λ1的特征向量;x2, y2是对应于λ2的特征向量。
// 因此目标矩阵为6通道,即 CV_32FC(6)的矩阵。
harris_response_img = Mat::zeros(src_img.size(), CV_32FC1);
// harris_response_img用来存储通过每个像素值所对应的自相关矩阵所计算得到的响应值
cornerEigenValsAndVecs(gray_img, harris_dst_img, blockSize, ksize, 4);
// 该函数用来计算每个像素值对应的自相关矩阵的特征值和特征向量
// 计算响应函数值
for (int row = 0; row < harris_dst_img.rows; ++row)
{
for (int col = 0; col < harris_dst_img.cols; ++col)
{
double eigenvalue1 = harris_dst_img.at<Vec6f>(row, col)[0]; // 获取特征值1
double eigenvalue2 = harris_dst_img.at<Vec6f>(row, col)[1]; // 获取特征值2
harris_response_img.at<float>(row, col) = eigenvalue1* eigenvalue2 - k*pow((eigenvalue1 + eigenvalue2), 2);
// 通过响应公式R=λ1*λ2 - k*(λ1+λ2)*(λ1+λ2)来计算每个像素对应的响应值
}
}
minMaxLoc(harris_response_img, &min_respense_value, &max_respense_value, 0, 0, Mat()); // 寻找响应矩阵中的最小值和最大值
namedWindow(harris_winName, CV_WINDOW_AUTOSIZE);
createTrackbar("Quality Value:", harris_winName, &qualityValue, max_qualityValue, self_defining_Harris_Demo); //创建TrackBar
self_defining_Harris_Demo(0, 0);
/*shi-tomasi
// 计算最小特征值
shiTomasiRsp = Mat::zeros(src.size(), CV_32FC1);
cornerMinEigenVal(gray_src, shiTomasiRsp, blockSize, ksize, 4);
minMaxLoc(shiTomasiRsp, &shitomasi_min_rsp, &shitomasi_max_rsp, 0, 0, Mat());
namedWindow(shitomasi_win, CV_WINDOW_AUTOSIZE);
createTrackbar("Quality:", shitomasi_win, &sm_qualitylevel, max_count, CustomShiTomasi_Demo);
CustomShiTomasi_Demo(0, 0);
waitKey(0);
return 0;
}*/
// 回调函数实现
void self_defining_Harris_Demo(int, void*)
{
if (qualityValue < 10)
{
qualityValue = 10; // 控制qualitylevel的下限值
}
Mat result_img = src_img.clone(); // 输出图像
float threshold_value = min_respense_value + (((double)qualityValue) / max_qualityValue)*(max_respense_value - min_respense_value);
for (int row = 0; row <result_img.rows; row++)
{
for (int col = 0; col < result_img.cols; col++)
{
float resp_value = harris_response_img.at<float>(row, col);
if (resp_value > threshold_value)
{
circle(result_img, Point(col, row), 2, Scalar(random_number_generator.uniform(0, 255),
random_number_generator.uniform(0, 255), random_number_generator.uniform(0, 255)), 2, 8, 0);
}
}
}
imshow(harris_winName, result_img);
}
//shi-tomasi
/*void CustomShiTomasi_Demo(int, void*) {
if (sm_qualitylevel < 20) {
sm_qualitylevel = 20;
}
Mat resultImg = src.clone();
float t = shitomasi_min_rsp + (((double)sm_qualitylevel) / max_count)*(shitomasi_max_rsp - shitomasi_min_rsp);
for (int row = 0; row < src.rows; row++) {
for (int col = 0; col < src.cols; col++) {
float v = shiTomasiRsp.at<float>(row, col);
if (v > t) {
circle(resultImg, Point(col, row), 2, Scalar(0, 0, 255), 2, 8, 0);
}
}
}
imshow(shitomasi_win, resultImg);
}*/