前面介绍模板匹配的时候已经提到模板匹配时一种基于灰度的匹配方法,而基于特征的匹配方法有FAST、SIFT、SURF等,上面两篇文章已经介绍过使用Surf算法进行特征点检测以及使用暴力匹配(BruteForceMatcher)和最近邻匹配(FLANN)两种匹配方法。接下来将更深一步介绍利用Surf检测到的特征点以及匹配对进行图像匹配.
利用Surf算法进行图像匹配其一般流程为:检测物体特征点->计算特征点描述子->使用BurteForceMatcher或FLANN进行特征点匹配->匹配到的特征点进行透视变换findHomography()->透视矩阵变换perspectiveTransform()->绘制匹配物体轮廓
透视变换
findHomography()
这个函数的作用是在图像原平面和目标图像平面之间寻找并返回一个透视变换矩阵H,如下:
所以反向投影误差
最小化。如果参数metchod被设置为默认值0,改函数使用所有的点以简单的最小二乘法计算一个初始的单应估计。可以简单理解为透视变换矩阵就是把一幅图像从一个空间变换到另一个空间所需要进行旋转平移而进行加权的矩阵。
但是并不是所有的点匹配对(srcPoints_i, dstPoints_i)都适合使用刚性的透视变换(有些异常值),这样变换得到的透视矩阵误差较大。在这种情况下可以使用两种鲁棒的方法RANSAC和LMeDS,尝试使用很多对应匹配点的随机子集,使用该子集和最简单的最小二乘法估计单应性矩阵,然后计算得到的透视变换矩阵的质量/好坏(quality/goodness),然后使用最佳子集来计算单应性矩阵的初始估计矩阵和内在值/异常值(inliers/outliers)的掩码。
不管方法是否具有鲁棒性,使用Levenberg-Marquardt方法进一步精确计算单应性矩阵(如果方法具有鲁棒性仅使用内在值(inline))来减少再投影误差。
RANSAC几乎可以处理任意比例的异常值,但是它需要一个阈值来区分内在值还是异常值。LMeDS不需要任何阈值,但是只有在内在值比例大于50%的情况下才能准确计算。如果没有太多异常值,噪声有比较小的情况下使用默认方法即可
函数用来查找初始的内在和外在矩阵,透视变换矩阵确定了一个比例.无论何时如果不能估计H矩阵则函数将返回一个空矩阵
Mat cv::findHomography ( InputArray srcPoints,
InputArray dstPoints,
int method = 0,
double ransacReprojThreshold = 3,
OutputArray mask = noArray(),
const int maxIters = 2000,
const double confidence = 0.995
)
srcPoints:原平面对应点,可以是CV_32FC2或vector类型的矩阵。
dstPoints:目标平面对应点,可以是CV_32FC2或vector类型的矩阵
method:用于计算矩阵的方法,可选方法上面已经介绍过有默认值0,CV_RANSAC和CV_LMEDS
ransacReprojThreshold:有默认值3,区分内在值还是异常值的阈值点,只在RANSAC方法有用,当||dstPoints-convertPointsHomogeneous(H*srcPoints)||>ransacReprojThreshold,这个点就会被认为是异常值(outlier).如果srcPoints和dstPoints是以像素为单位,则参数的取值范围一般在1-10之间。
mask:可选参数,有默认值noArray(),通过鲁棒性方法(RANSAC或LMEDS)设置输出掩码。
函数另一种定义形式如下:
Mat cv::findHomography ( InputArray srcPoints,
InputArray dstPoints,
OutputArray mask,
int method = 0,
double ransacReprojThreshold = 3
)
求得的透视矩阵是一个3x3的变换矩阵。
perspectiveTransform()
函数能够进行向量透视矩阵变换。
void cv::perspectiveTransform ( InputArray src,
OutputArray dst,
InputArray m
)
src:双通道或三通道浮点型原图像或数组,每个元素都是二维或三维可被转换的向量。
dst:目标数组或图像,与原图像有相同的尺寸和类型
m:变换矩阵,为3x3或4x4的浮点型矩阵
#include
#include
#include
#include
#include
#include
#include
using namespace std;
using namespace cv;
int main()
{
Mat image_object = imread("match_src.jpg", IMREAD_GRAYSCALE);
Mat image_scene = imread("match_dst.jpg", IMREAD_GRAYSCALE);
//判断图像是否加载成功
if (image_object.empty() || image_scene.empty())
{
cout << "图像加载失败";
return -1;
}
else
cout << "图像加载成功..." << endl << endl;
//检测特征点
const int minHessian = 700;
SurfFeatureDetector detector(minHessian);
vector keypoints_object, keypoints_scene;
detector.detect(image_object, keypoints_object);
detector.detect(image_scene, keypoints_scene);
//计算特征点描述子
SurfDescriptorExtractor extractor;
Mat descriptors_object, descriptors_scene;
extractor.compute(image_object, keypoints_object, descriptors_object);
extractor.compute(image_scene, keypoints_scene, descriptors_scene);
//使用FLANN进行特征点匹配
FlannBasedMatcher matcher;
vector matches;
matcher.match(descriptors_object, descriptors_scene, matches);
//计算匹配点之间最大和最小距离
double max_dist = 0;
double min_dist = 100;
for (int i = 0; i < descriptors_object.rows; i++)
{
double dist = matches[i].distance;
if (dist < min_dist)
{
min_dist = dist;
}
else if (dist > max_dist)
{
max_dist = dist;
}
}
printf("Max dist: %f \n", max_dist);
printf("Min dist: %f \n", min_dist);
//绘制“好”的匹配点
vector good_matches;
for (int i = 0; i < descriptors_object.rows; i++)
{
if (matches[i].distance<2*min_dist)
{
good_matches.push_back(matches[i]);
}
}
Mat image_matches;
drawMatches(image_object, keypoints_object, image_scene, keypoints_scene, good_matches, image_matches,
Scalar::all(-1), Scalar::all(-1), vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
//定位“好”的匹配点
vector obj;
vector scene;
for (int i = 0; i < good_matches.size(); i++)
{
//DMathch类型中queryIdx是指match中第一个数组的索引,keyPoint类型中pt指的是当前点坐标
obj.push_back(keypoints_object[good_matches[i].queryIdx].pt);
scene.push_back(keypoints_scene[good_matches[i].trainIdx].pt);
}
Mat H = findHomography(obj, scene, CV_RANSAC);
vector obj_corners(4), scene_corners(4);
obj_corners[0] = cvPoint(0, 0);
obj_corners[1] = cvPoint(image_object.cols, 0);
obj_corners[2] = cvPoint(image_object.cols, image_object.rows);
obj_corners[3] = cvPoint(0, image_object.rows);
perspectiveTransform(obj_corners, scene_corners, H);
//绘制角点之间的直线
line(image_matches, scene_corners[0] + Point2f(image_object.cols, 0),
scene_corners[1] + Point2f(image_object.cols, 0), Scalar(0, 0, 255), 2);
line(image_matches, scene_corners[1] + Point2f(image_object.cols, 0),
scene_corners[2] + Point2f(image_object.cols, 0), Scalar(0, 0, 255), 2);
line(image_matches, scene_corners[2] + Point2f(image_object.cols, 0),
scene_corners[3] + Point2f(image_object.cols, 0), Scalar(0, 0, 255), 2);
line(image_matches, scene_corners[3] + Point2f(image_object.cols, 0),
scene_corners[0] + Point2f(image_object.cols, 0), Scalar(0, 0, 255), 2);
//输出图像
namedWindow("匹配图像", WINDOW_AUTOSIZE);
imshow("匹配图像", image_matches);
waitKey(0);
return 0;
}
程序说明
在定位匹配点中用到了DMatch的queryIdx、trainIdx成员变量和keyPoint的成员变量pt,做个说明:
DMatch有三个构造函数,其中一组如下:
cv::DMatch::DMatch ( int _queryIdx, //在对描述子匹配时,第一组特征点的索引
int _trainIdx, //在对描述子匹配时,第二组特征点的索引
int _imgIdx, //多个图像中图像的索引
float _distance //两个特征向量间的欧氏距离,越小表明匹配度越高
)
对于keyPoint类有两种构造形式如下:
cv::KeyPoint::KeyPoint ( Point2f _pt,
float _size,
float _angle = -1,
float _response = 0,
int _octave = 0,
int _class_id = -1
)
cv::KeyPoint::KeyPoint ( float x,
float y,
float _size,
float _angle = -1,
float _response = 0,
int _octave = 0,
int _class_id = -1
)
两种形式在本质上是一样的,只是第一种形式中的特征点坐标pt在第二种形式中以x和y的形式给出。
pt关键点坐标
size是关键点邻域直径
angle特征点方向,范围为[0,360),负值表示不使用
response关键点检测器对于关键点的响应程度
octave关键点位于图像金字塔的层
class_id用于聚类的id
运行结果