2D Features检测
图像特征是指那些在图像中具有唯一可识别性的点或点集合。比如边缘、角点(交叉点)、斑点等。
角点检测
基于梯度计算的Harris角点检测
1. 原理: 对于图像的每一个像素点(x,y),对应一个以该像素为中心的窗口w(x,y),然后该像素平移(u,v)得到新的像素点(x+u,y+v),而E(u,v)就是窗口中所有像素的加权和乘以不同位置像素的灰度差值。
详解链接: https://blog.csdn.net/weixin_34910922/article/details/119045533
上面公式中的w(x, y)可以理解为一个高斯滤波器,相当于计算窗口处所有像素点的加权和,所得结果为一个常数。而Ix和Iy等于Sobel梯度算子求得x和y方向上的梯度值。
opencv中的角点检测API说明:
void cv::cornerHarris ( InputArray src, //Input single-channel 8-bit or floating-point image.
OutputArray dst,// 角点检测后的图像其像素值等于R值, CV_32FC1 类型
int blockSize, // 角点检测中要考虑的领域大小
int ksize, // Sobel求导中使用的窗口大小
double k, // 角点检测方程中的自由参数,取值参数为 [0,04 - 0.06].
int borderType = BORDER_DEFAULT
)
2. harris角点检测的算法流程:
链接: https://blog.csdn.net/weixin_34910922/article/details/119045903
3. harris角点检测算法的特点:
(1)具有旋转不变性,但是不具有尺度不变性,可以考虑使用SIFT角点检测,其具有旋转和尺度不变性。
(2)由于在检测角点的过程中使用了梯度计算,所以速度相对较慢以及易受到噪声的干扰;
(3)在进行harris角点计算的过程中使用到了经验参数k,所以准确性高度依赖于k。基于此,Shi-Tomasi被提出。
4. 实现案例代码:
以上案例为棋盘格角点检测案例,实现代码如下所示:
#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)
{
src = imread("C:/Users/hp/Desktop/1.jpg");
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); // 相当于只是执行了将CV_32FC1转化为CV_8UC1类型
// 可以使用dst_norm.convertTo(dst_norm_scaled,CV_8UC1);替代
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);
}
基于梯度计算的Shi-Tomasi角点检测(Harris角点的加强版,opencv中对应的函数同样能够用于harris检测)
Shi-Tomasi角点检测算法与harris角点检测算法基本一致,核心的差异如下红色所示:
opencv中的检测函数:
void cv::goodFeaturesToTrack ( InputArray image, // Input 8-bit or floating-point 32-bit, single-channel image.
OutputArray corners, // Output vector of detected corners.cv::Point2f或cv::Mat类型,如果输出为向量,那么按照每个角点的质量进行降序排列
int maxCorners, // 设置返回最大检测角点的数量,当maxCorners等于0时,表示返回检测到的所有角点。
double qualityLevel, // 质量水平系数(小于1.0的正数,一般在0.01-0.1之间),使用cornerMinEigenVal or cornerHarris计算每个像素的评估分数,然后使用该分数乘以qualityLevel,那么如果
double minDistance,// 最小距离,用于在检测出的所有角点向量中进行非极大值抑制。
InputArray mask = noArray(), // mask=0的点忽略,只在mask=1的像素点出检测角点
int blockSize = 3, // 使用的邻域数,表示在计算角点时参与运算的区域大小
bool useHarrisDetector = false, // false ='Shi Tomasi metric', 如果为TRUE表示使用harris角点检测
double k = 0.04 // Harris角点检测时使用
)
案例代码,依然是棋盘格角点检测:
void goodFeaturesToTrack_Demo( int, void* )
{
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;
Mat copy = src.clone();
goodFeaturesToTrack( src_gray,
corners,
maxCorners, // 如果等于0,表示返回检测到的全部角点
qualityLevel,
minDistance,
Mat(),
blockSize,
gradientSize,
useHarrisDetector,
k );
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 );
}
namedWindow( source_window );
imshow( source_window, copy );
}
上图Shi-Tomasi的全部检测结果
上图harris的全部检测结果,很明显检测效果不如Shi-Tomasi的检测结果。
对角点进行亚像素精度重定位
基本原理:
在整数坐标的角点处,取指定大小的区域,并使用构建的最小二乘法公式取不断的迭代求取亚像素坐标角点。原理有点类似与knn聚类算法中求取聚类中心点坐标。所以在使用cornerSubPix时,应该提前使用harris/Shi-Tomasi或其他角点检测方法将角点检测出来作为输入。
原理详解推荐连接: 很详细易于理解: https://xueyayang.github.io/pdf_posts/%E4%BA%9A%E5%83%8F%E7%B4%A0%E8%A7%92%E7%82%B9%E7%9A%84%E6%B1%82%E6%B3%95.pdf
opencv提供的函数:
void cv::cornerSubPix(InputArray image, // Input single-channel, 8-bit or float image.
InputOutputArray corners, // 角点(既作为输入也作为输出)
Size winSize, // 是计算亚像素角点时考虑的区域的大小,大小为NXN; N=(winSize*2+1)
Size zeroZone, // 搜索区域中间的dead region边长的一半,有时用于避免自相关矩阵的奇异性;真正搜索区域为 [zeroZone * 2+1 , winSize *2+1];如果值设为(-1,-1)则表示没有这个区域;
TermCriteria criteria // 用于表示计算亚像素时停止迭代的标准,可选的值有cv::TermCriteria::MAX_ITER (指定迭代次数)、
// cv::TermCriteria::EPS--指定上一次迭代计算结果与当前次计算结果的差值若小于e,则停止(可以是两者其一,或两者均选)
// 变化的最小值已经达到最小时停止迭代。二者均使用cv::TermCriteria()构造函数进行指定。
)
案例代码:
void goodFeaturesToTrack_Demo(int, void*)
{
maxCorners = 250;
vector<Point2f> corners,corners1;
double qualityLevel = 0.01;
double minDistance = 10;
int blockSize = 3, gradientSize = 3;
bool useHarrisDetector = false;
double k = 0.04;
Mat copy = src.clone();
goodFeaturesToTrack(src_gray,
corners,
maxCorners,
qualityLevel,
minDistance,
Mat(),
blockSize,
gradientSize,
useHarrisDetector,
k);
cout << "** Number of corners detected: " << corners.size() << endl;
int radius = 4;
for (size_t i = 0; i < corners.size(); i++)
{
corners1.push_back(corners[i]);
circle(copy, corners[i], radius, Scalar(rng.uniform(0, 255), rng.uniform(0, 256), rng.uniform(0, 256)), FILLED);
}
namedWindow(source_window);
imshow(source_window, copy);
Size winSize = Size(5, 5);
Size zeroZone = Size(-1, -1);
TermCriteria criteria = TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 40, 0.001); // 创建迭代停止精度变量值
cornerSubPix(src_gray, corners, winSize, zeroZone, criteria);
for (size_t i = 0; i < corners.size(); i++)
{
cout << " -- Refined Corner [" << i << "] (" << corners[i].x << "," << corners[i].y << ")" << " "<< "(" << corners1[i].x << ", " << corners1[i].y << ")" << endl;
}
图二是亚像素角点坐标与整型角点坐标的对比。
通过使用 cv::cornerEigenValsAndVecs 和cv::cornerMinEigenVal 实现角点检测器
函数解释1:
void cv::cornerEigenValsAndVecs ( InputArray src, // Input single-channel 8-bit or floating-point image.
OutputArray dst, // Image to store the results. It has the same size as src and the type CV_32FC(6) .
int blockSize, // 对标harris角点检测原理中的M特征矩阵的计算
int ksize, // sobel算子的尺度
int borderType = BORDER_DEFAULT
)
// 该函数用于计算λ1,λ2。 对标harris
函数解释1:
void cv::cornerMinEigenVal ( InputArray src, // Input single-channel 8-bit or floating-point image.
OutputArray dst, // 存储最下的min(λ1,λ2)
int blockSize,
int ksize = 3,
int borderType = BORDER_DEFAULT
)
// 该函数用于计算λ1,λ2中的最小值,对标Shi-Tomasi
可以参考官网代码,有助于加深对harris和Shi-Tomasi的理解。
图像特征提取与匹配
图像特征提取通常的目的就是用于图像的识别分类以及图像之间的匹配。
---------图像特征提取通常包含: SIFT,SURF,BRIEF,ORB等斑点特征方法; 斑点:与周围有着颜色和灰度差别的区域
---------图像特征匹配方法包含: Brute-Force暴力匹配法,基于FLANN的匹配法,可见前面两种都是继承自DescriptorMatcher ()类;
opencv相关图像特征检测类
opencv中提供了图像特征检测的公共类接口,所有的特征检测类都是继承自Feature2D接口类:
----------该接口类提供了两类核心的虚函数detect和compute函数,前者负责检测特征点,后者负责计算检测到特征点的描述器。同时也提供了两者同时实现的detectAndCompute方法。
具体详细的方法如下所示:
SURF特征检测实现
-
基础原理:
谈到SURF特征点检测就必须先谈论SIFT特征点检测算法,关于SIFT的原理可以参考文章:SIFT原理详解.由于需要在DOG图像中进行检测,所以速度较慢。
基于SIFT算法的特征生成一般包含如下步骤:
(1)构建尺度空间,检测极值点,获得尺度不变性;
(2)特征点过滤并精确定位;
(3)为特征点分配方向值;
(4)生成特征描述子; -
SURF特征检测与关键点描述符计算,特征点匹配的基本步骤:
step1: 调用 cv::xfeatures2d::SURF类的静态方法creat()创建SURF类实例;
step2:调用类成员函数detect进行关键点检测;
step3: 调用cv::drawKeypoints 绘制检测到的关键点;
step4: 调用compute成员函数计算关键点描述器; -
函数参数:
// 创建SURF类实例
static Ptr<SURF> cv::xfeatures2d::SURF::create ( double hessianThreshold = 100,
int nOctaves = 4,
int nOctaveLayers = 3,
bool extended = false,
bool upright = false
)
// 检测关键点
virtual void cv::Feature2D::detect ( InputArray image,
std::vector< KeyPoint > & keypoints,
InputArray mask = noArray()
)
// 计算关键点的描述器
virtual void cv::Feature2D::compute ( InputArray image,
std::vector< KeyPoint > & keypoints,
OutputArray descriptors
)
// 创建关键点对匹配类的实例
static Ptr<DescriptorMatcher> cv::DescriptorMatcher::create ( const String & descriptorMatcherType )
// 支持BruteForce (it uses L2 )
// BruteForce-L1
// BruteForce-Hamming
// BruteForce-Hamming(2)
// FlannBased
// 调用关键点对配准方法,在DescriptorMatcher类中还提供了其他很多种配准方法的实现如:KNNmatch等
void cv::DescriptorMatcher::match ( InputArray queryDescriptors,
InputArray trainDescriptors,
std::vector< DMatch > & matches,
InputArray mask = noArray()
) const
// 找到k个最佳匹配关键点对
void knnMatch( InputArray queryDescriptors, //查询描述器
InputArray trainDescriptors, //训练描述器
CV_OUT std::vector<std::vector<DMatch> >& matches, //输出配对DMatch类向量,注意会输出两个相似的点
int k, // 最佳配对对数
InputArray mask=noArray(),
bool compactResult=false ) const;
// 绘制关键点
void cv::drawKeypoints ( InputArray image,
const std::vector< KeyPoint > & keypoints,
InputOutputArray outImage,
const Scalar & color = Scalar::all(-1),
DrawMatchesFlags flags = DrawMatchesFlags::DEFAULT
)
//绘制从两个图像中找到的关键点匹配项.
void cv::drawMatches ( InputArray img1,
const std::vector< KeyPoint > & keypoints1,
InputArray img2,
const std::vector< KeyPoint > & keypoints2,
const std::vector< DMatch > & matches1to2,
InputOutputArray outImg,
const Scalar & matchColor = Scalar::all(-1),
const Scalar & singlePointColor = Scalar::all(-1),
const std::vector< char > & matchesMask = std::vector< char >(),
DrawMatchesFlags flags = DrawMatchesFlags::DEFAULT
)
内容补充:
(1)KeyPoint类的关键属性:
CV_PROP_RW Point2f pt; //关键点坐标x,y
CV_PROP_RW float size; //!< 关键点的直径
CV_PROP_RW float angle; //计算出的关键点的方向(0°至360°)(如果不适用则为-1)
CV_PROP_RW float response; //最强的关键点被选中的反应。可用于进一步的分类或子抽样
CV_PROP_RW int octave; // 代表从第几层金字塔提取出的关键点
CV_PROP_RW int class_id; // 关键点归属的类别标签
(2)DMatch类的属性;
CV_PROP_RW int queryIdx; // 查询描述符索引
CV_PROP_RW int trainIdx; //训练描述符索引
CV_PROP_RW int imgIdx; //训练图像索引
CV_PROP_RW float distance; // 查询关键点与配对关键点两个描述符向量之间的距离(欧式距离等),距离越小表示越相似。
- 案例代码:
#include <iostream>
#include "opencv2/core.hpp"
#ifdef HAVE_OPENCV_XFEATURES2D
#include "opencv2/highgui.hpp"
#include "opencv2/features2d.hpp"
#include "opencv2/xfeatures2d.hpp"
using namespace cv;
using namespace cv::xfeatures2d;
using std::cout;
using std::endl;
int main(int argc, char* argv[])
{
Mat img1 = imread("C:\\Users\\hp\\Desktop\\box.png", IMREAD_GRAYSCALE);
Mat img2 = imread("C:\\Users\\hp\\Desktop\\R-C.png", IMREAD_GRAYSCALE);
if (img1.empty() || img2.empty())
{
cout << "Could not open or find the image!\n" << endl;
return -1;
}
//-- Step 1: Detect the keypoints using SURF Detector, compute the descriptors
int minHessian = 400;
Ptr<SURF> detector = SURF::create(minHessian);
std::vector<KeyPoint> keypoints1, keypoints2;
Mat descriptors1, descriptors2;
detector->detectAndCompute(img1, noArray(), keypoints1, descriptors1);
detector->detectAndCompute(img2, noArray(), keypoints2, descriptors2);
// Step2: 使用BRUTEFORCE或者FLANNBASED配准实例
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create(DescriptorMatcher::FLANNBASED);
// Step2.1: 分别使用knnMatch和match方法执行配准
std::vector< DMatch > matches; // 这里需要了解DMatch类中属性的含义
matcher->match(descriptors1, descriptors2, matches);
Mat img_matche;
drawMatches(img1, keypoints1, img2, keypoints2, matches, img_matche);
imshow("Matches", img_matche);
// Step2.2: 分别使用knnMatch和match方法执行配准
std::vector< std::vector<DMatch> > knn_matches;
matcher->knnMatch(descriptors1, descriptors2, knn_matches, 2); // 需要了解knnMatch返回值knn_matches的结构
//Step3: 过滤掉哪些与配准点距离过大的匹配关键点。
const float ratio_thresh = 0.7f;
std::vector<DMatch> good_matches;
for (size_t i = 0; i < knn_matches.size(); i++)
{
if (knn_matches[i][0].distance < ratio_thresh * knn_matches[i][1].distance) // knn_matches[i][0]和knn_matches[i][1]代表什么,解释:相当于是与查询关键点最加匹配的两个距离最近的关键点。
{
good_matches.push_back(knn_matches[i][0]);
}
}
//-- Draw matches
Mat img_matches;
drawMatches(img1, keypoints1, img2, keypoints2, good_matches, img_matches, Scalar::all(-1),
Scalar::all(-1), std::vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
//-- Show detected matches
imshow("Filter Matches", img_matches);
waitKey();
return 0;
}
#else
int main()
{
std::cout << "This tutorial code needs the xfeatures2d contrib module to be run." << std::endl;
return 0;
}
#endif