github代码
一、Harris角点 cornerHarris() R = det(M) - k*(trace(M))^2
算法基本思想是使用一个固定窗口在图像上进行任意方向上的滑动,
比较滑动前与滑动后两种情况,窗口中的像素灰度变化程度,
如果存在任意方向上的滑动,都有着较大灰度变化,
那么我们可以认为该窗口中存在角点。
图像特征类型:
边缘 (Edges 物体边缘)
角点 (Corners 感兴趣关键点( interest points) 边缘交叉点 )
斑点(Blobs 感兴趣区域( regions of interest ) 交叉点形成的区域 )
为什么角点是特殊的?
因为角点是两个边缘的连接点(交点)它代表了两个边缘变化的方向上的点。
图像梯度有很高的变化。这种变化是可以用来帮助检测角点的。
G = SUM( W(x,y) * [I(x+u, y+v) -I(x,y)]^2 )
[u,v] 是窗口的偏移量
(x,y) 是窗口内所对应的像素坐标位置,窗口有多大,就有多少个位置
w(x,y)是窗口函数,最简单情形就是窗口内的所有像素所对应的w权重系数均为1。
设定为以窗口中心为原点的二元正态分布
泰勒展开(I(x+u, y+v) 相当于 导数)
G = SUM( W(x,y) * [I(x,y) + u*Ix + v*Iy - I(x,y)]^2)
= SUM( W(x,y) * (u*u*Ix*Ix + v*v*Iy*Iy))
= SUM(W(x,y) * [u v] * [Ix^2 Ix*Iy] * [u
Ix*Iy Iy^2] v] )
= [u v] * SUM(W(x,y) * [Ix^2 Ix*Iy] ) * [u 应为 [u v]为常数 可以拿到求和外面
Ix*Iy Iy^2] v]
= [u v] * M * [u
v]
则计算 det(M) 矩阵M的行列式的值 取值为一个标量,写作det(A)或 | A |
矩阵表示的空间的单位面积/体积/..
trace(M) 矩阵M的迹 矩阵M的对角线元素求和,
用字母T来表示这种算子,他的学名叫矩阵的迹
M的两个特征值为 lamd1 lamd2
det(M) = lamd1 * lamd2
trace(M) = lamd1 + lamd2
R = det(M) - k*(trace(M))^2
其中k是常量,一般取值为0.04~0.06,
R大于一个阈值的话就认为这个点是 角点
因此可以得出下列结论:
>特征值都比较大时,即窗口中含有角点
>特征值一个较大,一个较小,窗口中含有边缘
>特征值都比较小,窗口处在平坦区域
https://blog.csdn.net/woxincd/article/details/60754658
#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> #include <stdio.h> #include <stdlib.h> using namespace cv; using namespace std; //全局变量 Mat src, src_gray; int thresh = 200;//阈值 R大于一个阈值的话就认为这个点是 角点 int max_thresh = 255; char* source_window = "Source image"; char* corners_window = "Corners detected"; //函数声明 滑动条回调函数 void cornerHarris_demo( int, void* ); //主函数 int main( int argc, char** argv ) { string imageName("../../common/data/building.jpg"); // 图片文件名路径(默认值) if( argc > 1) { imageName = argv[1];//如果传递了文件 就更新 } src = imread( imageName ); if( src.empty() ) { cout << "can't load image " << endl; return -1; } // 转换成灰度图 cvtColor( src, src_gray, CV_BGR2GRAY ); //创建一个窗口 和 滑动条 namedWindow( source_window, CV_WINDOW_AUTOSIZE ); createTrackbar( "Threshold阈值: ", source_window, &thresh, max_thresh, cornerHarris_demo ); imshow( source_window, src );//显示图像 cornerHarris_demo( 0, 0 );//初始化 滑动条回调函数 waitKey(0); return(0); } // 滑动条回调函数 void cornerHarris_demo( int, void* ) { Mat dst, dst_norm, dst_norm_scaled; dst = Mat::zeros( src.size(), CV_32FC1 ); /// 检测参数 int blockSize = 2;// 滑动窗口大小 int apertureSize = 3;// Sobel算子的大小(默认值为3) 用来计算 梯度 Ix^2 Ix*Iy Iy^2 double k = 0.04;// R = det(M) - k*(trace(M))^2 一般取值为0.04~0.06 /// 检测角点 cornerHarris( src_gray, dst, blockSize, apertureSize, k, BORDER_DEFAULT ); /// 归一化 得到 R值 的 图像大小矩阵 normalize( dst, dst_norm, 0, 255, NORM_MINMAX, CV_32FC1, Mat() ); convertScaleAbs( dst_norm, dst_norm_scaled ); /// 在检测到的角点处 画圆 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) > thresh )// R大于一个阈值的话就认为这个点是 角点 { circle( dst_norm_scaled, Point( i, j ), 5, Scalar(0), 2, 8, 0 ); } } } /// 显示结果 namedWindow( corners_window, CV_WINDOW_AUTOSIZE ); imshow( corners_window, dst_norm_scaled ); }
二、Shi-Tomasi 算法 goodFeaturesToTrack()
是Harris 算法的改进。
Harris 算法最原始的定义是将矩阵 M 的行列式值与 M 的迹相减,
再将差值同预先给定的阈值进行比较。
后来Shi 和Tomasi 提出改进的方法,
若两个特征值中较小的一个大于最小阈值,则会得到强角点。
M 对角化>>> M的两个特征值为 lamd1 lamd2
R = mini(lamd1,lamd2) > 阈值 认为是角点
#include "opencv2/highgui.hpp" #include "opencv2/imgproc.hpp" #include <iostream> #include <stdio.h> #include <stdlib.h> using namespace cv; using namespace std; //全局变量 Mat src, src_gray; int maxCorners = 23;//角点数量 int maxTrackbar = 100; RNG rng(12345);//随机数 char* source_window = "Image"; //函数声明 滑动条回调函数 void goodFeaturesToTrack_Demo( int, void* ); //主函数 int main( int argc, char** argv ) { string imageName("../../common/data/building.jpg"); // 图片文件名路径(默认值) if( argc > 1) { imageName = argv[1];//如果传递了文件 就更新 } src = imread( imageName ); if( src.empty() ) { cout << "can't load image " << endl; return -1; } // 转换成灰度图 cvtColor( src, src_gray, COLOR_BGR2GRAY ); //创建一个窗口 和 滑动条 namedWindow( source_window, WINDOW_AUTOSIZE ); createTrackbar( "Max corners:", source_window, &maxCorners, maxTrackbar, goodFeaturesToTrack_Demo ); imshow( source_window, src );//显示图像 goodFeaturesToTrack_Demo( 0, 0 );//初始化 滑动条回调函数 waitKey(0); return(0); } // 滑动条回调函数 void goodFeaturesToTrack_Demo( int, void* ) { if( maxCorners < 1 ) { maxCorners = 1; } vector<Point2f> corners;//角点容器 double qualityLevel = 0.01; double minDistance = 10; int blockSize = 3;//滑窗大小 bool useHarrisDetector = false; double k = 0.04;// harris 角点阈值 Mat copy; copy = src.clone(); goodFeaturesToTrack( src_gray, corners, maxCorners, qualityLevel, minDistance, Mat(), blockSize, useHarrisDetector, k ); cout<<"** Number of corners detected: "<<corners.size()<<endl; int r = 4;//圆半径 for( size_t i = 0; i < corners.size(); i++ ) { circle( copy, corners[i], r, Scalar(rng.uniform(0,255), rng.uniform(0,255), rng.uniform(0,255)), -1, 8, 0 ); } namedWindow( source_window, WINDOW_AUTOSIZE ); imshow( source_window, copy ); }
三、FAST角点检测算法 ORB特征检测中使用的就是这种角点检测算法
FAST角点检测算法 ORB特征检测中使用的就是这种角点检测算法
周围区域灰度值 都较大 或 较小
若某像素与其周围邻域内足够多的像素点相差较大,则该像素可能是角点。
该算法检测的角点定义为在像素点的周围邻域内有足够多的像素点与该点处于不同的区域。
应用到灰度图像中,即有足够多的像素点的灰度值大于该点的灰度值或者小于该点的灰度值。
p点附近半径为3的圆环上的16个点,
一个思路是若其中有连续的12( (FAST-9,当然FAST-10、FAST-11、FAST-12、FAST-12)
)个点的灰度值与p点的灰度值差别超过某一阈值,
则可以认为p点为角点。
之后可进行非极大值抑制
这一思路可以使用机器学习的方法进行加速。
对同一类图像,例如同一场景的图像,可以在16个方向上进行训练,
得到一棵决策树,从而在判定某一像素点是否为角点时,
不再需要对所有方向进行检测,
而只需要按照决策树指定的方向进行2-3次判定即可确定该点是否为角点。
#include "opencv2/highgui/highgui.hpp" #include <opencv2/features2d/features2d.hpp> #include "opencv2/imgproc/imgproc.hpp" #include <iostream> #include <stdio.h> #include <stdlib.h> using namespace cv; using namespace std; //全局变量 Mat src, src_gray; int thresh = 50;//阈值 R大于一个阈值的话就认为这个点是 角点 int max_thresh = 200; char* source_window = "Source image"; char* corners_window = "Corners detected"; //函数声明 滑动条回调函数 void cornerFast_demo( int, void* ); //主函数 int main( int argc, char** argv ) { string imageName("../../common/data/building.jpg"); // 图片文件名路径(默认值) if( argc > 1) { imageName = argv[1];//如果传递了文件 就更新 } src = imread( imageName ); if( src.empty() ) { cout << "can't load image " << endl; return -1; } // 转换成灰度图 cvtColor( src, src_gray, CV_BGR2GRAY ); //创建一个窗口 和 滑动条 namedWindow( source_window, CV_WINDOW_AUTOSIZE ); createTrackbar( "Threshold阈值: ", source_window, &thresh, max_thresh, cornerFast_demo ); imshow( source_window, src );//显示图像 cornerFast_demo( 0, 0 );//初始化 滑动条回调函数 waitKey(0); return(0); } // 滑动条回调函数 void cornerFast_demo( int, void* ) { Mat dst = src.clone(); //cv::FastFeatureDetector fast(50); // 检测的阈值为50 std::vector<KeyPoint> keyPoints; //fast.detect(src_gray, keyPoints); // 检测角点 FAST(src_gray, keyPoints,thresh); // 画角点 drawKeypoints( dst, keyPoints, dst, Scalar(0,0,255), DrawMatchesFlags::DRAW_OVER_OUTIMG); // 显示结果 namedWindow( corners_window, CV_WINDOW_AUTOSIZE ); imshow( corners_window, dst ); }
四、通过自定义 R的计算方法和自适应阈值 来定制化检测角点
计算 M矩阵
计算判断矩阵 R
设置自适应阈值
阈值大小为 判断矩阵 最小值和最大值之间 百分比
阈值为 最小值 + (最大值-最小值)× 百分比
百分比 = myHarris_qualityLevel/max_qualityLevel
#include "opencv2/imgcodecs.hpp" #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> #include <stdio.h> #include <stdlib.h> using namespace cv; using namespace std; // 全局变量 Mat src, src_gray; Mat myHarris_dst; Mat myHarris_copy; Mat Mc;// Harris角点角点相关 判断矩阵R Mat myShiTomasi_dst; Mat myShiTomasi_copy; // Shi-Tomasi 角点检测算法 int myShiTomasi_qualityLevel = 50;// Shi-Tomasi 角点检测算法 阈值 int myHarris_qualityLevel = 50; // Harris角点角点检测算法 阈值 int max_qualityLevel = 100; // 最大阈值 百分比 myHarris_qualityLevel/max_qualityLevel // 阈值为 最小值 + (最大值-最小值)× 百分比 double myHarris_minVal; double myHarris_maxVal; // Harris角点 判断矩阵的最小最大值 double myShiTomasi_minVal; double myShiTomasi_maxVal;// Shi-Tomasi 角点 判断矩阵的最小最大值 RNG rng(12345);//随机数 产生 随机颜色 const char* myHarris_window = "My Harris corner detector"; const char* myShiTomasi_window = "My Shi Tomasi corner detector"; //函数声明 void myShiTomasi_function( int, void* ); void myHarris_function( int, void* ); int main( int argc , char** argv ) { string imageName("../../common/data/building.jpg"); // 图片文件名路径(默认值) if( argc > 1) { imageName = argv[1];//如果传递了文件 就更新 } src = imread( imageName ); if( src.empty() ) { cout << "can't load image " << endl; return -1; } // 转换成灰度图 cvtColor( src, src_gray, COLOR_BGR2GRAY ); int blockSize = 3; // 滑动窗口大小 int apertureSize = 3;// Sobel算子的大小(默认值为3) 用来计算 梯度 Ix^2 Ix*Iy Iy^2 //============== Harris角点========================== myHarris_dst = Mat::zeros( src_gray.size(), CV_32FC(6) );//得到的 矩阵M的特征值 Mc = Mat::zeros( src_gray.size(), CV_32FC1 );//定制化的 判断矩阵 R // 计算 矩阵M的特征值 cornerEigenValsAndVecs( src_gray, myHarris_dst, blockSize, apertureSize, BORDER_DEFAULT ); // 计算判断矩阵 R for( int j = 0; j < src_gray.rows; j++ ) { for( int i = 0; i < src_gray.cols; i++ ) { float lambda_1 = myHarris_dst.at<Vec6f>(j, i)[0]; float lambda_2 = myHarris_dst.at<Vec6f>(j, i)[1]; Mc.at<float>(j,i) = lambda_1*lambda_2 - 0.04f*pow( ( lambda_1 + lambda_2 ), 2 ); } } // 判断矩阵 R 的最低值和最高值 minMaxLoc( Mc, &myHarris_minVal, &myHarris_maxVal, 0, 0, Mat() ); //创建一个窗口 和 滑动条 namedWindow( myHarris_window, WINDOW_AUTOSIZE ); createTrackbar( " Quality Level:", myHarris_window, &myHarris_qualityLevel, max_qualityLevel, myHarris_function ); myHarris_function( 0, 0 ); //===================Shi-Tomasi 角点检测算法=============================== myShiTomasi_dst = Mat::zeros( src_gray.size(), CV_32FC1 );// Harris角点角点检测算法 判断矩阵 min(lambda_1 , lambda_2) cornerMinEigenVal( src_gray, myShiTomasi_dst, blockSize, apertureSize, BORDER_DEFAULT ); minMaxLoc( myShiTomasi_dst, &myShiTomasi_minVal, &myShiTomasi_maxVal, 0, 0, Mat() ); //创建一个窗口 和 滑动条 namedWindow( myShiTomasi_window, WINDOW_AUTOSIZE ); createTrackbar( " Quality Level:", myShiTomasi_window, &myShiTomasi_qualityLevel, max_qualityLevel, myShiTomasi_function ); myShiTomasi_function( 0, 0 ); waitKey(0); return(0); } // Shi-Tomasi 角点检测算法 滑动条回调函数 void myShiTomasi_function( int, void* ) { if( myShiTomasi_qualityLevel < 1 ) { myShiTomasi_qualityLevel = 1; } // 自适应阈值 // 阈值为 最小值 + (最大值-最小值)× 百分比 // 百分比 = yShiTomasi_qualityLevel/max_qualityLevel float thresh = myShiTomasi_minVal + ( myShiTomasi_maxVal - myShiTomasi_minVal ) * myShiTomasi_qualityLevel/max_qualityLevel; myShiTomasi_copy = src.clone(); for( int j = 0; j < src_gray.rows; j++ ) { for( int i = 0; i < src_gray.cols; i++ ) { if( myShiTomasi_dst.at<float>(j,i) > thresh) { // 在角点处画圆圈 circle( myShiTomasi_copy, Point(i,j), 4, Scalar( rng.uniform(0,255), rng.uniform(0,255), rng.uniform(0,255) ), -1, 8, 0 ); } } } imshow( myShiTomasi_window, myShiTomasi_copy ); } // Harris角点 滑动条回调函数 void myHarris_function( int, void* ) { if( myHarris_qualityLevel < 1 ) { myHarris_qualityLevel = 1; } // 自适应阈值 // 阈值为 最小值 + (最大值-最小值)× 百分比 // 百分比 = myHarris_qualityLevel/max_qualityLevel float thresh = myHarris_minVal + ( myHarris_maxVal - myHarris_minVal ) * myHarris_qualityLevel/ max_qualityLevel; myHarris_copy = src.clone(); for( int j = 0; j < src_gray.rows; j++ ) { for( int i = 0; i < src_gray.cols; i++ ) { if( Mc.at<float>(j,i) > thresh ) { // 在角点处画圆圈 circle( myHarris_copy, Point(i,j), 4, Scalar( rng.uniform(0,255), rng.uniform(0,255), rng.uniform(0,255) ), -1, 8, 0 ); } } } imshow( myHarris_window, myHarris_copy ); }
五、亚像素级的角点检测 补偿
使用OpenCV函数 cornerSubPix
寻找更精确的角点位置 (不是整数类型的位置,而是更精确的浮点类型位置).
除了利用Harris进行角点检测和利用Shi-Tomasi方法进行角点检测外,
还可以使用cornerEigenValsAndVecs()函数和cornerMinEigenVal()函数自定义角点检测函数。
如果对角点的精度有更高的要求,可以用cornerSubPix()函数将角点定位到子像素,从而取得亚像素级别的角点检测效果。
使用cornerSubPix()函数在goodFeaturesToTrack()的角点检测基础上将角点位置精确到亚像素级别
常见的亚像素级别精准定位方法有三类:
1. 基于插值方法
2. 基于几何矩寻找方法
3. 拟合方法 - 比较常用
拟合方法中根据使用的公式不同可以分为
1. 高斯曲面拟合与
2. 多项式拟合等等。
以高斯拟合为例:
窗口内的数据符合二维高斯分布
Z = n / (2 * pi * 西格玛^2) * exp(-P^2/(2*西格玛^2))
P = sqrt( (x-x0)^2 + (y-y0)^2)
x,y 原来 整数点坐标
x0,y0 亚像素补偿后的 坐标 需要求取
ln(Z) = n0 + x0/(西格玛^2)*x + y0/(西格玛^2)*y - 1/(2*西格玛^2) * x^2 - 1/(2*西格玛^2) * y^2
n0 + n1*x + n2*y + n3*x^2 + n3 * y^2
对窗口内的像素点 使用最小二乘拟合 得到上述 n0 n1 n2 n3
则 x0 = - n1/(2*n3)
y0 = - n2/(2*n3)
#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> #include <stdio.h> #include <stdlib.h> using namespace cv; using namespace std; /// 全局变量 Mat src, src_gray; int maxCorners = 10; int maxTrackbar = 50;//需要检测的角点数量 RNG rng(12345);//随机数 产生随机颜色 char* source_window = "Image"; char* refineWindow = "refinement"; /// 滑动条 回调函数 声明 亚像素级的角点检测 void goodFeaturesToTrack_Demo( int, void* ); //主函数 int main( int argc, char** argv ) { string imageName("../../common/data/building.jpg"); // 图片文件名路径(默认值) if( argc > 1) { imageName = argv[1];//如果传递了文件 就更新 } src = imread( imageName ); if( src.empty() ) { cout << "can't load image " << endl; return -1; } // 转换成灰度图 cvtColor( src, src_gray, CV_BGR2GRAY ); //创建一个窗口 和 滑动条 namedWindow( source_window, CV_WINDOW_AUTOSIZE ); createTrackbar( "Max corners:", source_window, &maxCorners, maxTrackbar, goodFeaturesToTrack_Demo); imshow( source_window, src ); goodFeaturesToTrack_Demo( 0, 0 ); waitKey(0); return(0); } // 亚像素级的角点检测 void goodFeaturesToTrack_Demo( int, void* ) { if( maxCorners < 1 ) { maxCorners = 1; } /// Parameters for Shi-Tomasi algorithm vector<Point2f> corners;//角点容器 double qualityLevel = 0.01; double minDistance = 10; int blockSize = 3;//滑窗大小 bool useHarrisDetector = false; double k = 0.04;// harris 角点阈值 /// 复制源图像 Mat copy, refineSrcCopy; copy = src.clone(); refineSrcCopy = src.clone(); /// 检测 Shi-Tomasi 角点 goodFeaturesToTrack( src_gray, corners, maxCorners,//最多角点数量 qualityLevel, minDistance, Mat(), blockSize, useHarrisDetector, k ); /// 显示 角点 cout<<"** Number of corners detected: "<<corners.size()<<endl; int r = 4;//圆半径 for( int i = 0; i < corners.size(); i++ ) { circle( copy, corners[i], r, Scalar(rng.uniform(0,255), rng.uniform(0,255), rng.uniform(0,255)), -1, 8, 0 ); } /// 显示图 namedWindow( source_window, CV_WINDOW_AUTOSIZE ); imshow( source_window, copy ); /// 设置亚像素 检测补偿算法 参数 Size winSize = Size( 5, 5 );//滑动窗大小 Size zeroZone = Size( -1, -1 );// 用于避免自相关矩阵的奇异性 // 角点精准化迭代过程的终止条件 TermCriteria criteria = TermCriteria( CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 40, 0.001 );// 最大迭代次数 40 和 误差变换 0.001 /// 亚像素修正 cornerSubPix( src_gray, // 输入图像 corners, // 输入角点的初始坐标以及精准化后的坐标用于输出 winSize, // 搜索窗口边长的一半 如果winSize=Size(5,5) 实际为 5*2+1 = 11 11*11的窗口 zeroZone, // 搜索区域中间的dead region边长的一半,有时用于避免自相关矩阵的奇异性。如果值设为(-1,-1)则表示没有这个区域。 criteria // 角点精准化迭代过程的终止条件。也就是当迭代次数超过criteria.maxCount, // 或者角点位置变化小于criteria.epsilon时,停止迭代过程。 ); /// 显示 修正后的角点坐标 for( int i = 0; i < corners.size(); i++ ) { // 标示出角点 circle( refineSrcCopy, corners[i], r, Scalar(255,0,255), -1, 8, 0 ); // 输出角点坐标 cout<<" -- Refined Corner ["<<i<<"] ("<<corners[i].x<<","<<corners[i].y<<")"<<endl; } namedWindow( refineWindow, CV_WINDOW_AUTOSIZE ); imshow( refineWindow, refineSrcCopy ); }
六、斑点检测原理 SIFT SURF
SIFT定位算法关键步骤的说明
http://www.cnblogs.com/ronny/p/4028776.html
SIFT原理与源码分析 https://blog.csdn.net/xiaowei_cqu/article/details/8069548
该算法大概可以归纳为三步:
1)高斯差分金字塔的构建;
2)特征点的搜索;
3)特征描述。
DoG尺度空间构造(Scale-space extrema detection)
关键点搜索与定位(Keypoint localization)
方向赋值(Orientation assignment)
关键点描述(Keypoint descriptor)
OpenCV实现:特征检测器FeatureDetector
SIFT中LoG和DoG的比较
SURF算法与源码分析、上 加速鲁棒特征(SURF)
www.cnblogs.com/ronny/p/4045979.html
https://blog.csdn.net/abcjennifer/article/details/7639681
通过在不同的尺度上利用积分图像可以有效地计算出近似Harr小波值,
简化了二阶微分模板的构建,搞高了尺度空间的特征检测的效率。
在以关键点为中心的3×3×3像素邻域内进行非极大值抑制,
最后通过对斑点特征进行插值运算,完成了SURF特征点的精确定位。
而SURF特征点的描述,则也是充分利用了积分图,用两个方向上的Harr小波模板来计算梯度,
然后用一个扇形对邻域内点的梯度方向进行统计,求得特征点的主方向。
// SURF放在另外一个包的xfeatures2d里边了,在github.com/Itseez/opencv_contrib 这个仓库里。
// 按说明把这个仓库编译进3.0.0就可以用了。
opencv2中SurfFeatureDetector、SurfDescriptorExtractor、BruteForceMatcher在opencv3中发生了改变。
具体如何完成特征点匹配呢?示例如下:
//寻找关键点
int minHessian = 700;
Ptr<SURF>detector = SURF::create(minHessian);
detector->detect( srcImage1, keyPoint1 );
detector->detect( srcImage2, keyPoints2 );
//绘制特征关键点
Mat img_keypoints_1; Mat img_keypoints_2;
drawKeypoints( srcImage1, keypoints_1, img_keypoints_1, Scalar::all(-1), DrawMatchesFlags::DEFAULT );
drawKeypoints( srcImage2, keypoints_2, img_keypoints_2, Scalar::all(-1), DrawMatchesFlags::DEFAULT );
//显示效果图
imshow("特征点检测效果图1", img_keypoints_1 );
imshow("特征点检测效果图2", img_keypoints_2 );
//计算特征向量
Ptr<SURF>extractor = SURF::create();
Mat descriptors1, descriptors2;
extractor->compute( srcImage1, keyPoint1, descriptors1 );
extractor->compute( srcImage2, keyPoints2, descriptors2 );
//使用BruteForce进行匹配
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce");
std::vector< DMatch > matches;
matcher->match( descriptors1, descriptors2, matches );
//绘制从两个图像中匹配出的关键点
Mat imgMatches;
drawMatches( srcImage1, keyPoint1, srcImage2, keyPoints2, matches, imgMatches );//进行绘制
//显示
imshow("匹配图", imgMatches );
3.x的特征检测:
算法:SURF,SIFT,BRIEF,FREAK
类:cv::xfeatures2d::SURF
cv::xfeatures2d::SIFT
cv::xfeatures::BriefDescriptorExtractor
cv::xfeatures2d::FREAK
cv::xfeatures2d::StarDetector
需要进行以下几步
加入opencv_contrib
包含opencv2/xfeatures2d.hpp
using namepsace cv::xfeatures2d
使用create(),detect(),compute(),detectAndCompute()
七、二进制字符串特征描述子
注意到在两种角点检测算法里,我们并没有像SIFT或SURF那样提到特征点的描述问题。
事实上,特征点一旦检测出来,无论是斑点还是角点描述方法都是一样的,
可以选用你认为最有效的特征描述子。
比较有代表性的就是 浮点型特征描述子(sift、surf 欧氏距离匹配)和
二进制字符串特征描述子 (字符串汉明距离 匹配 )。
1 浮点型特征描述子(sift、surf 欧氏距离匹配):
像SIFT与SURF算法里的,用梯度统计直方图来描述的描述子都属于浮点型特征描述子。
但它们计算起来,算法复杂,效率较低.
SIFT特征采用了128维的特征描述子,由于描述子用的浮点数,所以它将会占用512 bytes的空间。
类似地,对于SURF特征,常见的是64维的描述子,它也将占用256bytes的空间。
如果一幅图像中有1000个特征点(不要惊讶,这是很正常的事),
那么SIFT或SURF特征描述子将占用大量的内存空间,对于那些资源紧张的应用,
尤其是嵌入式的应用,这样的特征描述子显然是不可行的。而且,越占有越大的空间,
意味着越长的匹配时间。
我们可以用PCA、LDA等特征降维的方法来压缩特征描述子的维度。
还有一些算法,例如LSH,将SIFT的特征描述子转换为一个二值的码串,
然后这个码串用汉明距离进行特征点之间的匹配。这种方法将大大提高特征之间的匹配,
因为汉明距离的计算可以用异或操作然后计算二进制位数来实现,在现代计算机结构中很方便。
2 二进制字符串特征描述子 (字符串汉明距离 匹配 ):
如BRIEF。后来很多二进制串描述子ORB,BRISK,FREAK等都是在它上面的基础上的改进。
【A】 BRIEF: Binary Robust Independent Elementary Features
http://www.cnblogs.com/ronny/p/4081362.html
它需要先平滑图像,然后在特征点周围选择一个Patch,在这个Patch内通过一种选定的方法来挑选出来nd个点对。
然后对于每一个点对(p,q),我们来比较这两个点的亮度值,
如果I(p)>I(q)则这个点对生成了二值串中一个的值为1,
如果I(p)<I(q),则对应在二值串中的值为-1,否则为0。
所有nd个点对,都进行比较之间,我们就生成了一个nd长的二进制串。
对于nd的选择,我们可以设置为128,256或512,这三种参数在OpenCV中都有提供,
但是OpenCV中默认的参数是256,这种情况下,非匹配点的汉明距离呈现均值为128比特征的高斯分布。
一旦维数选定了,我们就可以用汉明距离来匹配这些描述子了。
对于BRIEF,它仅仅是一种特征描述符,它不提供提取特征点的方法。
所以,如果你必须使一种特征点定位的方法,如FAST、SIFT、SURF等。
这里,我们将使用CenSurE方法来提取关键点,对BRIEF来说,CenSurE的表现比SURF特征点稍好一些。
总体来说,BRIEF是一个效率很高的提取特征描述子的方法,
同时,它有着很好的识别率,但当图像发生很大的平面内的旋转。
关于点对的选择:
设我们在特征点的邻域块大小为S×S内选择nd个点对(p,q),Calonder的实验中测试了5种采样方法:
1)在图像块内平均采样;
2)p和q都符合(0,1/25 * S^2)的高斯分布;
3)p符合(0,1/25 * S^2)的高斯分布,而q符合(0,1/100 *S^2)的高斯分布;
4)在空间量化极坐标下的离散位置随机采样
5)把p固定为(0,0),q在周围平均采样
【B】BRISK算法
BRISK算法在特征点检测部分没有选用FAST特征点检测,而是选用了稳定性更强的AGAST算法。
在特征描述子的构建中,BRISK算法通过利用简单的像素灰度值比较,
进而得到一个级联的二进制比特串来描述每个特征点,这一点上原理与BRIEF是一致的。
BRISK算法里采用了邻域采样模式,即以特征点为圆心,构建多个不同半径的离散化Bresenham同心圆,
然后再每一个同心圆上获得具有相同间距的N个采样点。
【C】ORB算法 Oriented FAST and Rotated BRIEF
http://www.cnblogs.com/ronny/p/4083537.html
ORB算法使用FAST进行特征点检测,然后用BREIF进行特征点的特征描述,
但是我们知道BRIEF并没有特征点方向的概念,所以ORB在BRIEF基础上引入了方向的计算方法,
并在点对的挑选上使用贪婪搜索算法,挑出了一些区分性强的点对用来描述二进制串。
通过构建高斯金字塔 来实现 尺度不变性
利用灰度质心法 来实现 记录方向
灰度质心法假设角点的灰度与质心之间存在一个偏移,这个向量可以用于表示一个方向。
【D】FREAK算法 Fast Retina KeyPoint,即快速视网膜关键点。
根据视网膜原理进行点对采样,中间密集一些,离中心越远越稀疏。
并且由粗到精构建描述子,穷举贪婪搜索找相关性小的。
42个感受野,一千对点的组合,找前512个即可。这512个分成4组,
前128对相关性更小,可以代表粗的信息,后面越来越精。匹配的时候可以先看前16bytes,
即代表精信息的部分,如果距离小于某个阈值,再继续,否则就不用往下看了
/* 使用 orb 特征检测 匹配 使用二维特征点(Features2D)和单映射(Homography)寻找已知物体 【1】 创建新的控制台(console)项目。读入两个输入图像。 【2】 检测两个图像的关键点(尺度旋转都不发生变化的关键点) 【3】 计算每个关键点的描述向量(Descriptor) 【4】 计算两幅图像中的关键点对应的描述向量距离,寻找两图像中距离最近的描述向量对应的关键点,即为两图像中匹配上的关键点: 【5】 寻找两个点集合中的单映射变换(homography transformation): 【6】 创建内匹配点集合同时绘制出匹配上的点。用perspectiveTransform函数来通过单映射来映射点: 【7】 用 drawMatches 来绘制内匹配点. */ #include <stdio.h> #include <iostream> #include "opencv2/core/core.hpp" #include "opencv2/imgproc.hpp" #include "opencv2/features2d/features2d.hpp" #include "opencv2/highgui/highgui.hpp" #include "opencv2/calib3d/calib3d.hpp" using namespace cv; using namespace std; void readme(); // 主函数 int main( int argc, char** argv ) { /// 加载源图像 string imageName1("../../common/data/box.png"); // 图片文件名路径(默认值) string imageName2("../../common/data/box_in_scene.png"); // 图片文件名路径(默认值) if( argc > 2) { imageName1 = argv[1];//如果传递了文件 就更新 imageName2 = argv[2];//如果传递了文件 就更新 } Mat img_object = imread( imageName1, CV_LOAD_IMAGE_GRAYSCALE ); Mat img_scene = imread( imageName2, CV_LOAD_IMAGE_GRAYSCALE ); if( img_object.empty() || img_scene.empty() ) { cout << "can't load image " << endl; readme(); return -1; } //======【1】检测关键点 ================== std::vector<KeyPoint> keypoints_object, keypoints_scene; //int minHessian = 400; // SURF关键点 //SurfFeatureDetector detector( minHessian ); //detector.detect( img_object, keypoints_object ); //detector.detect( img_scene, keypoints_scene ); Ptr<FeatureDetector> orb = ORB::create();//orb 检测器 orb->detect(img_object, keypoints_object); orb->detect(img_scene, keypoints_scene); cout<< "keypoints_object size() " << keypoints_object.size() << endl; cout<< "keypoints_scene size() " << keypoints_scene.size() << endl; //======【2】计算描述子======== Mat descriptors_object, descriptors_scene; //SurfDescriptorExtractor extractor; //extractor.compute( img_object, keypoints_object, descriptors_object ); //extractor.compute( img_scene, keypoints_scene, descriptors_scene ); orb->compute(img_object, keypoints_object, descriptors_object); orb->compute(img_scene, keypoints_scene, descriptors_scene); ///* //=====【3】对描述子进行匹配 使用FLANN 匹配 // 鲁棒匹配器设置 描述子匹配器 Ptr<cv::flann::IndexParams> indexParams = makePtr<flann::LshIndexParams>(6, 12, 1); // LSH index parameters Ptr<cv::flann::SearchParams> searchParams = makePtr<flann::SearchParams>(50); // flann search parameters // instantiate FlannBased matcher Ptr<DescriptorMatcher> matcher = makePtr<FlannBasedMatcher>(indexParams, searchParams); //FlannBasedMatcher matcher; //std::vector< DMatch > matches; //matcher.match( descriptors_object, descriptors_scene, matches ); std::vector<std::vector<cv::DMatch> > matches12, matches21;// 最匹配 和 次匹配 //std::vector<cv::DMatch> matches12; matcher->knnMatch(descriptors_object, descriptors_scene, matches12, 1); // 1->2 // matcher->knnMatch(descriptors_scene, descriptors_object, matches21, 1); cout<< "match12 size() " << matches12.size() << endl; ///* double max_dist = 0; double min_dist = 100; // 找到最小的最大距离 for( int i = 0; i < descriptors_object.rows; i++ ) { double dist = matches12[i][0].distance;//1匹配上2中点对 距离 if( dist < min_dist ) min_dist = dist;//最小距离 if( dist > max_dist ) max_dist = dist;//最大距离 } printf("-- Max dist : %f \n", max_dist ); printf("-- Min dist : %f \n", min_dist ); ///* //====筛选 最优的匹配点对 距离小于 2.3 *最小距离 为好的匹配点对============ // 其次 可以在考虑 相互匹配 的 才为 好的匹配点 std::vector< DMatch > good_matches; for( int i = 0; i < descriptors_object.rows; i++ ) { if( matches12[i][0].distance < 2.3 * min_dist ) { good_matches.push_back( matches12[i][0]); } } cout<< "good_matches size() " << good_matches.size() << endl; ///* //=====显示匹配点====================== Mat img_matches; drawMatches( img_object, keypoints_object, img_scene, keypoints_scene, good_matches, img_matches, Scalar::all(-1), Scalar::all(-1), vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS ); //-- 得到2d-2d匹配点坐标 std::vector<Point2f> obj; std::vector<Point2f> scene; for( int i = 0; i < good_matches.size(); i++ ) { //-- Get the keypoints from the good matches obj.push_back( keypoints_object[ good_matches[i].queryIdx ].pt ); scene.push_back( keypoints_scene[ good_matches[i].trainIdx ].pt ); } // 求解单映射矩阵 H p1 = H * p2 平面变换 Mat H = findHomography( obj, scene, CV_RANSAC ); //-- 得到需要检测的物体的四个顶点 std::vector<Point2f> obj_corners(4); obj_corners[0] = cvPoint(0,0); obj_corners[1] = cvPoint( img_object.cols, 0 ); obj_corners[2] = cvPoint( img_object.cols, img_object.rows ); obj_corners[3] = cvPoint( 0, img_object.rows ); std::vector<Point2f> scene_corners(4);// 场景中的 该物体的顶点 // 投影过去 perspectiveTransform( obj_corners, scene_corners, H); //--在场景中显示检测到的物体 (the mapped object in the scene - image_2 ) line( img_matches, scene_corners[0] + Point2f( img_object.cols, 0), scene_corners[1] + Point2f( img_object.cols, 0), Scalar(0, 255, 0), 4 ); line( img_matches, scene_corners[1] + Point2f( img_object.cols, 0), scene_corners[2] + Point2f( img_object.cols, 0), Scalar( 0, 255, 0), 4 ); line( img_matches, scene_corners[2] + Point2f( img_object.cols, 0), scene_corners[3] + Point2f( img_object.cols, 0), Scalar( 0, 255, 0), 4 ); line( img_matches, scene_corners[3] + Point2f( img_object.cols, 0), scene_corners[0] + Point2f( img_object.cols, 0), Scalar( 0, 255, 0), 4 ); //-- 显示检测结果 imshow( "Good Matches & Object detection", img_matches ); waitKey(0); return 0; } // 用法 void readme() { std::cout << " Usage: ./SURF_descriptor <img1> <img2>" << std::endl; }
八、KAZE非线性尺度空间 特征
#include <opencv2/features2d.hpp> #include <opencv2/imgcodecs.hpp> #include "opencv2/highgui/highgui.hpp" #include <opencv2/opencv.hpp> #include <vector> #include <iostream> using namespace std; using namespace cv; // 全局变量 const float inlier_threshold = 2.5f; // 单应变换后 和匹配点差值阈值 const float nn_match_ratio = 0.8f; // 最近距离/次近距离 < 阈值 int main(int argc, char** argv) { /// 加载源图像 string imageName1("../../common/data/graf1.png"); // 图片文件名路径(默认值) string imageName2("../../common/data/graf3.png"); // 图片文件名路径(默认值) if( argc > 2) { imageName1 = argv[1];//如果传递了文件 就更新 imageName2 = argv[2];//如果传递了文件 就更新 } Mat img1 = imread( imageName1, CV_LOAD_IMAGE_GRAYSCALE ); Mat img2 = imread( imageName2, CV_LOAD_IMAGE_GRAYSCALE ); if( img1.empty() || img2.empty() ) { cout << "can't load image " << endl; return -1; } // 单应变换矩阵 Mat homography; FileStorage fs("../../common/data/H1to3p.xml", FileStorage::READ); fs.getFirstTopLevelNode() >> homography; vector<KeyPoint> kpts1, kpts2;//关键点 Mat desc1, desc2;//描述子 // 非线性尺度空间 AKAZE特征检测 Ptr<AKAZE> akaze = AKAZE::create(); // 检测 + 描述 akaze->detectAndCompute(img1, noArray(), kpts1, desc1); akaze->detectAndCompute(img2, noArray(), kpts2, desc2); // 汉明距离匹配器 BFMatcher matcher(NORM_HAMMING); // 匹配点对 二维数组 一个点检测多个匹配点(按距离远近) vector< vector<DMatch> > nn_matches; // 进行描述子匹配 matcher.knnMatch(desc1, desc2, nn_matches, 2); vector<KeyPoint> matched1, matched2, inliers1, inliers2; // matched1, matched2, 最近距离的匹配/次近距离的匹配的匹配点 // inliers1, inliers2 一个匹配点 单应变换后 和 其匹配点的 差平方和 开根号 小于 阈值的 更好的匹配点 vector<DMatch> good_matches;//较好的匹配 //====最近距离的匹配/次近距离的匹配 小于一个阈值 才认为是 匹配点对 ================ for(size_t i = 0; i < nn_matches.size(); i++) { DMatch first = nn_matches[i][0]; float dist1 = nn_matches[i][0].distance;//最近距离的匹配 float dist2 = nn_matches[i][1].distance;//次近距离的匹配 if(dist1 < nn_match_ratio * dist2) { matched1.push_back(kpts1[first.queryIdx]);// 对应图1的关键点坐标 matched2.push_back(kpts2[first.trainIdx]);// 对应图2的关键点坐标 } } //==========一个匹配点 单应变换后 和 其匹配点的 差平方和 开根号 < 阈值 为最终的匹配点对 for(unsigned i = 0; i < matched1.size(); i++) { Mat col = Mat::ones(3, 1, CV_64F);//图1 点 的 其次表达方式 (u,v,1) col.at<double>(0) = matched1[i].pt.x; col.at<double>(1) = matched1[i].pt.y; col = homography * col;// 单应变换 后 col /= col.at<double>(2);// 将第三维归一化(x,y,z)-> (x/z,y/z,z/z)->(u',v',1) // 计算 一个匹配点 单应变换后 和 其匹配点的 差平方和 开根号 double dist = sqrt( pow(col.at<double>(0) - matched2[i].pt.x, 2) + pow(col.at<double>(1) - matched2[i].pt.y, 2)); if(dist < inlier_threshold) { int new_i = static_cast<int>(inliers1.size()); inliers1.push_back(matched1[i]);// 内点 符合 单应变换的 点 inliers2.push_back(matched2[i]);// 是按 匹配点对方式存入的 good_matches.push_back(DMatch(new_i, new_i, 0));//所以匹配点对 1-1 2-2 3-3 4-4 5-5 } } // ======= 显示 =============== Mat res;//最后的图像 drawMatches(img1, inliers1, img2, inliers2, good_matches, res, Scalar::all(-1), Scalar::all(-1), vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS); imshow("res.png", res);//显示匹配后的图像 double inlier_ratio = inliers1.size() * 1.0 / matched1.size(); cout << "A-KAZE Matching Results" << endl; cout << "*******************************" << endl; cout << "# Keypoints 1: \t" << kpts1.size() << endl; cout << "# Keypoints 2: \t" << kpts2.size() << endl; cout << "# Matches: \t" << matched1.size() << endl; cout << "# Inliers: \t" << inliers1.size() << endl; cout << "# Inliers Ratio: \t" << inlier_ratio << endl; cout << endl; return 0; }
基于非线性尺度空间的KAZE特征提取方法以及它的改进AKATE
https://blog.csdn.net/chenyusiyuan/article/details/8710462
KAZE是日语‘风’的谐音,寓意是就像风的形成是空气在空间中非线性的流动过程一样,
KAZE特征检测是在图像域中进行非线性扩散处理的过程。
传统的SIFT、SURF等特征检测算法都是基于 线性的高斯金字塔 进行多尺度分解来消除噪声和提取显著特征点。
但高斯分解是牺牲了局部精度为代价的,容易造成边界模糊和细节丢失。
非线性的尺度分解有望解决这种问题,但传统方法基于正向欧拉法(forward Euler scheme)
求解非线性扩散(Non-linear diffusion)方程时迭代收敛的步长太短,耗时长、计算复杂度高。
由此,KAZE算法的作者提出采用加性算子分裂算法(Additive Operator Splitting, AOS)
来进行非线性扩散滤波,可以采用任意步长来构造稳定的非线性尺度空间。
非线性扩散滤波
Perona-Malik扩散方程:
具体地,非线性扩散滤波方法是将图像亮度(L)在不同尺度上的变化视为某种形式的
流动函数(flow function)的散度(divergence),可以通过非线性偏微分方程来描述:
AOS算法:
由于非线性偏微分方程并没有解析解,一般通过数值分析的方法进行迭代求解。
传统上采用显式差分格式的求解方法只能采用小步长,收敛缓慢。
KAZE特征检测与描述
KAZE特征的检测步骤大致如下:
1) 首先通过AOS算法和可变传导扩散(Variable Conductance Diffusion)([4,5])方法来构造非线性尺度空间。
2) 检测感兴趣特征点,这些特征点在非线性尺度空间上经过尺度归一化后的Hessian矩阵行列式是局部极大值(3×3邻域)。
3) 计算特征点的主方向,并且基于一阶微分图像提取具有尺度和旋转不变性的描述向量。
特征点检测
KAZE的特征点检测与SURF类似,是通过寻找不同尺度归一化后的Hessian局部极大值点来实现的。
非线性尺度空间的KAZE特征提取
【1】非线性尺度空间 AKAZE特征检测
【2】检测 + 描述
【3】汉明距离匹配器
【4】进行描述子匹配
【5】最近距离的匹配/次近距离的匹配 小于一个阈值 才认为是 初级匹配点对
【6】一个匹配点 单应变换后 和 其匹配点的 差平方和 开根号 < 阈值 为最终的匹配点对
【7】显示匹配
#include <opencv2/features2d.hpp> #include <opencv2/imgcodecs.hpp> #include "opencv2/highgui/highgui.hpp" #include <opencv2/opencv.hpp> #include <vector> #include <iostream> using namespace std; using namespace cv; // 全局变量 const float inlier_threshold = 2.5f; // 单应变换后 和匹配点差值阈值 const float nn_match_ratio = 0.8f; // 最近距离/次近距离 < 阈值 int main(int argc, char** argv) { /// 加载源图像 string imageName1("../../common/data/graf1.png"); // 图片文件名路径(默认值) string imageName2("../../common/data/graf3.png"); // 图片文件名路径(默认值) if( argc > 2) { imageName1 = argv[1];//如果传递了文件 就更新 imageName2 = argv[2];//如果传递了文件 就更新 } Mat img1 = imread( imageName1, CV_LOAD_IMAGE_GRAYSCALE ); Mat img2 = imread( imageName2, CV_LOAD_IMAGE_GRAYSCALE ); if( img1.empty() || img2.empty() ) { cout << "can't load image " << endl; return -1; } // 单应变换矩阵 Mat homography; FileStorage fs("../../common/data/H1to3p.xml", FileStorage::READ); fs.getFirstTopLevelNode() >> homography; vector<KeyPoint> kpts1, kpts2;//关键点 Mat desc1, desc2;//描述子 // 非线性尺度空间 AKAZE特征检测 Ptr<AKAZE> akaze = AKAZE::create(); // 检测 + 描述 akaze->detectAndCompute(img1, noArray(), kpts1, desc1); akaze->detectAndCompute(img2, noArray(), kpts2, desc2); // 汉明距离匹配器 BFMatcher matcher(NORM_HAMMING); // 匹配点对 二维数组 一个点检测多个匹配点(按距离远近) vector< vector<DMatch> > nn_matches; // 进行描述子匹配 matcher.knnMatch(desc1, desc2, nn_matches, 2); vector<KeyPoint> matched1, matched2, inliers1, inliers2; // matched1, matched2, 最近距离的匹配/次近距离的匹配的匹配点 // inliers1, inliers2 一个匹配点 单应变换后 和 其匹配点的 差平方和 开根号 小于 阈值的 更好的匹配点 vector<DMatch> good_matches;//较好的匹配 //====最近距离的匹配/次近距离的匹配 小于一个阈值 才认为是 匹配点对 ================ for(size_t i = 0; i < nn_matches.size(); i++) { DMatch first = nn_matches[i][0]; float dist1 = nn_matches[i][0].distance;//最近距离的匹配 float dist2 = nn_matches[i][1].distance;//次近距离的匹配 if(dist1 < nn_match_ratio * dist2) { matched1.push_back(kpts1[first.queryIdx]);// 对应图1的关键点坐标 matched2.push_back(kpts2[first.trainIdx]);// 对应图2的关键点坐标 } } //==========一个匹配点 单应变换后 和 其匹配点的 差平方和 开根号 < 阈值 为最终的匹配点对 for(unsigned i = 0; i < matched1.size(); i++) { Mat col = Mat::ones(3, 1, CV_64F);//图1 点 的 其次表达方式 (u,v,1) col.at<double>(0) = matched1[i].pt.x; col.at<double>(1) = matched1[i].pt.y; col = homography * col;// 单应变换 后 col /= col.at<double>(2);// 将第三维归一化(x,y,z)-> (x/z,y/z,z/z)->(u',v',1) // 计算 一个匹配点 单应变换后 和 其匹配点的 差平方和 开根号 double dist = sqrt( pow(col.at<double>(0) - matched2[i].pt.x, 2) + pow(col.at<double>(1) - matched2[i].pt.y, 2)); if(dist < inlier_threshold) { int new_i = static_cast<int>(inliers1.size()); inliers1.push_back(matched1[i]);// 内点 符合 单应变换的 点 inliers2.push_back(matched2[i]);// 是按 匹配点对方式存入的 good_matches.push_back(DMatch(new_i, new_i, 0));//所以匹配点对 1-1 2-2 3-3 4-4 5-5 } } // ======= 显示 =============== Mat res;//最后的图像 drawMatches(img1, inliers1, img2, inliers2, good_matches, res, Scalar::all(-1), Scalar::all(-1), vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS); imshow("res.png", res);//显示匹配后的图像 double inlier_ratio = inliers1.size() * 1.0 / matched1.size(); cout << "A-KAZE Matching Results" << endl; cout << "*******************************" << endl; cout << "# Keypoints 1: \t" << kpts1.size() << endl; cout << "# Keypoints 2: \t" << kpts2.size() << endl; cout << "# Matches: \t" << matched1.size() << endl; cout << "# Inliers: \t" << inliers1.size() << endl; cout << "# Inliers Ratio: \t" << inlier_ratio << endl; cout << endl; return 0; }
基于特征点的 物体跟踪
非线性尺度空间的KAZE特征提取
orb 特征检测 匹配
./planar_tracking blais.mp4 result.avi blais_bb.xml.gz
#include <opencv2/features2d.hpp> #include <opencv2/videoio.hpp> #include <opencv2/opencv.hpp> #include <vector> #include <iostream> #include <iomanip> #include "stats.h" // Stats structure definition #include "utils.h" // Drawing and printing functions using namespace std; using namespace cv; const double akaze_thresh = 3e-4; // AKAZE 检测阈值 1000个点 const double ransac_thresh = 2.5f; // 随机序列采样 内点阈值 const double nn_match_ratio = 0.8f; // 近邻匹配点阈值 const int bb_min_inliers = 100; // 最小数量的内点数 来画 包围框 const int stats_update_period = 10; // 更新频率 // 定义 跟踪类 class Tracker { public: // 默认构造函数 Tracker(Ptr<Feature2D> _detector, Ptr<DescriptorMatcher> _matcher) : detector(_detector),//特征点检测器 matcher(_matcher)//描述子匹配器 {} // 第一帧图像 void setFirstFrame(const Mat frame, vector<Point2f> bb, string title, Stats& stats); // 以后每一帧进行处理 跟踪 Mat process(const Mat frame, Stats& stats); // 返回检测器 Ptr<Feature2D> getDetector() { return detector; } protected: Ptr<Feature2D> detector;//特征点检测器 Ptr<DescriptorMatcher> matcher;//描述子匹配器 Mat first_frame, first_desc;// vector<KeyPoint> first_kp;//关键点 vector<Point2f> object_bb;//物体的 包围框 }; // 第一帧图像 void Tracker::setFirstFrame(const Mat frame, vector<Point2f> bb, string title, Stats& stats) { first_frame = frame.clone();//复制图像 detector->detectAndCompute(first_frame, noArray(), first_kp, first_desc);// 检测 + 描述 stats.keypoints = (int)first_kp.size();// 关键点数量 drawBoundingBox(first_frame, bb);// 显示 包围框 putText(first_frame, title, Point(0, 60), FONT_HERSHEY_PLAIN, 5, Scalar::all(0), 4); object_bb = bb;// 包围框 } // 处理 Mat Tracker::process(const Mat frame, Stats& stats) { vector<KeyPoint> kp;//关键点 Mat desc;//描述子 detector->detectAndCompute(frame, noArray(), kp, desc);//检测新一帧图像的 关键点和描述子 stats.keypoints = (int)kp.size();//关键点数量 vector< vector<DMatch> > matches;//匹配点 索引 二维数组 vector<KeyPoint> matched1, matched2;// 和上一帧对应 匹配的 关键点坐标 matcher->knnMatch(first_desc, desc, matches, 2);// 最近领匹配进行匹配 2个匹配 for(unsigned i = 0; i < matches.size(); i++) { // 符合近邻匹配 阈值的 才可能是 关键点 if(matches[i][0].distance < nn_match_ratio * matches[i][1].distance) { matched1.push_back(first_kp[matches[i][0].queryIdx]);//上一帧中对应的关键点 matched2.push_back( kp[matches[i][0].trainIdx]);//当前帧中对应的关键点 } } // 求出初级匹配点对符合 的 单应变换矩阵 stats.matches = (int)matched1.size();//匹配点的数量 Mat inlier_mask, homography; vector<KeyPoint> inliers1, inliers2;// 符合找到的单应矩阵 的 匹配点 vector<DMatch> inlier_matches;// 匹配点对是否符合 该单应矩阵 if(matched1.size() >= 4) {// 求解单应矩阵 需要4个匹配点对以上 homography = findHomography(Points(matched1), Points(matched2), RANSAC, ransac_thresh, inlier_mask); } if(matched1.size() < 4 || homography.empty()) { Mat res;// 匹配效果不好 未能求解到 单应矩阵 hconcat(first_frame, frame, res); stats.inliers = 0; stats.ratio = 0; return res; } // 保留 符合求出的 单应变换矩阵 的 匹配点对 for(unsigned i = 0; i < matched1.size(); i++) { if(inlier_mask.at<uchar>(i)) {//该匹配点对 符合求出的 单应矩阵 int new_i = static_cast<int>(inliers1.size()); inliers1.push_back(matched1[i]); inliers2.push_back(matched2[i]); inlier_matches.push_back(DMatch(new_i, new_i, 0)); } } stats.inliers = (int)inliers1.size();//内点数量 stats.ratio = stats.inliers * 1.0 / stats.matches;//内点比率 vector<Point2f> new_bb;//新的物体包围框 perspectiveTransform(object_bb, new_bb, homography);// 利用 单应变换矩阵 重映射 之前的包围框 Mat frame_with_bb = frame.clone();// 带有物体包围框的 图像 if(stats.inliers >= bb_min_inliers) {//内点数量超过阈值 显示 包围框 drawBoundingBox(frame_with_bb, new_bb); } // 显示匹配点对 Mat res; drawMatches(first_frame, inliers1, frame_with_bb, inliers2, inlier_matches, res, Scalar(255, 0, 0), Scalar(255, 0, 0)); return res;// 返回显示了 包围框 和 匹配点对 的图像 } // 主函数 int main(int argc, char **argv) { if(argc < 4) { cerr << "Usage: " << endl << "akaze_track input_path output_path bounding_box" << endl; return 1; } VideoCapture video_in(argv[1]);//输入视频 //输出视频 VideoWriter video_out(argv[2], (int)video_in.get(CAP_PROP_FOURCC), (int)video_in.get(CAP_PROP_FPS),//帧率 Size(2 * (int)video_in.get(CAP_PROP_FRAME_WIDTH),//尺寸 2 * (int)video_in.get(CAP_PROP_FRAME_HEIGHT))); if(!video_in.isOpened()) { cerr << "Couldn't open " << argv[1] << endl; return 1; } if(!video_out.isOpened()) { cerr << "Couldn't open " << argv[2] << endl; return 1; } vector<Point2f> bb;// 第一帧所需要跟踪物体 的 包围框 // 这里可以使用鼠标指定 FileStorage fs(argv[3], FileStorage::READ); if(fs["bounding_box"].empty()) { cerr << "Couldn't read bounding_box from " << argv[3] << endl; return 1; } // 第一帧所需要跟踪物体 的 包围框 fs["bounding_box"] >> bb; // 状态 Stats stats, akaze_stats, orb_stats; // AKAZE 特征检测 Ptr<AKAZE> akaze = AKAZE::create(); akaze->setThreshold(akaze_thresh); // ORB 特征检测 Ptr<ORB> orb = ORB::create(); orb->setMaxFeatures(stats.keypoints);//最多关键点数量 // 描述子匹配器 汉明字符串距离匹配 Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce-Hamming"); // 构造跟踪器 对象 Tracker akaze_tracker(akaze, matcher); Tracker orb_tracker(orb, matcher); // 第一帧图像 Mat frame; video_in >> frame; akaze_tracker.setFirstFrame(frame, bb, "AKAZE", stats); orb_tracker.setFirstFrame(frame, bb, "ORB", stats); Stats akaze_draw_stats, orb_draw_stats; // 总帧数 int frame_count = (int)video_in.get(CAP_PROP_FRAME_COUNT); // 跟踪后的帧 Mat akaze_res, orb_res, res_frame; for(int i = 1; i < frame_count; i++) {//处理每一帧 bool update_stats = (i % stats_update_period == 0); video_in >> frame;//捕获一帧 //=======AKAZE 跟踪 处理一帧====== akaze_res = akaze_tracker.process(frame, stats);// AKAZE 跟踪 处理一帧 akaze_stats += stats; if(update_stats) { akaze_draw_stats = stats;//更新状态 } //=======ORB 跟踪 处理一帧====== orb->setMaxFeatures(stats.keypoints);//最多关键点数量 根据上一次 检测数量 orb_res = orb_tracker.process(frame, stats);// ORB 跟踪 处理一帧 orb_stats += stats; if(update_stats) { orb_draw_stats = stats;//更新状态 } drawStatistics(akaze_res, akaze_draw_stats); drawStatistics(orb_res, orb_draw_stats); vconcat(akaze_res, orb_res, res_frame); video_out << res_frame; cout << i << "/" << frame_count - 1 << endl; } akaze_stats /= frame_count - 1; orb_stats /= frame_count - 1; printStatistics("AKAZE", akaze_stats); printStatistics("ORB", orb_stats); return 0; }