您的位置:首页 > 运维架构

OpenCV 双目匹配流程

2015-03-22 18:22 316 查看

一. 读取图像

读入两幅需要匹配的图像。

//-- Step 1: 读入图像
Mat image1 = imread("./images/parliament1.bmp", 0);
Mat image2 = imread("./images/parliament2.bmp", 0);


二. 特征检测

特征检测的方法很多,这里以 SURF 法为例。

//-- Step 2: 用 SURF 法检测特征关键点
int minHessian = 400;
SurfFeatureDetector detector(minHessian);
vector<KeyPoint> keypoints1, keypoints2;
detector.detect(image1, keypoints1);
detector.detect(image2, keypoints2);


OpenCV 中定义了 KeyPoint 类来描述特征关键点,所有要定义两个 KeyPoint 对象用于保存两幅图像的特征关键点。

KeyPoint 类

class KeyPoint
{
Point2f pt; // 特征点坐标
float size; // 重要特征点邻域直径
float angle; //特征点的方向,值为[零,三百六十),负值表示不使用
float response; // 对应最强特征点
int octave; //特征点所在的图像金字塔的组
int class_id; //用于聚类的id
}


输入为 Mat 类图像,输出为 KeyPoint 类关键点。

三. 提取特征描述子

OpenCV 引入了一个通用类,用于提取不同的特征点描述子。结果是一个 Mat 类矩阵,矩阵的没一行是一个N维描述子的向量,该向量描绘了特征点周围强度的样式。两个特征点越相似,他们的特征向量也越靠近。

//-- Step 3: 提取特征描述子
SurfDescriptorExtractor extractor;
Mat descriptors1, descriptors2;
extractor.compute(image1, keypoints1, descriptors1);
extractor.compute(image2, keypoints2, descriptors2);


为何有了关键点还要提取特征点描述子呢?因为关键点只是表明我们找到了一个“好的特征”,但是我们还未去很好的描述它,只是记录了位置,角度等信息。而描述子相当于对特征进行编码(例如与邻域的差异),通过对特征进行编码就可以与不同尺度,不同方向的其他图像进行比较。

四. 特征点描述子匹配

一个特征对应一个描述子向量,向量的距离越近,特征点越相似,因此可以遍历图一中的所有特征点,在图2中找到与其匹配(距离最近)的特征点。这也是 BruteFoceMatcher 中实现的最基本策略。

当然,也可以使用其他方法进行特征点描述子的匹配,如Flann 法。

//-- Step 4: 用 FLANN 法匹配特征描述子
FlannBasedMatcher matcher;
vector< DMatch > matches;
matcher.match(descriptors1, descriptors2, matches);


OpenCV 定义了一个 Dmatch 结构体,用于表示一对匹配的描述子。Dmatch 包含指向第一个向量和第二个向量的索引,同时包含一个表示描述子距离的浮点数,如下所示。

struct DMatch
{
// 三个构造函数
DMatch() : queryIdx(-1), trainIdx(-1), imgIdx(-1), distance(std::numeric_limits<float>::max()) {}

DMatch( int _queryIdx, int _trainIdx, float _distance ) : queryIdx(_queryIdx), trainIdx(_trainIdx), imgIdx(-1), distance(_distance) {}

DMatch( int _queryIdx, int _trainIdx, int _imgIdx, float _distance ) : queryIdx(_queryIdx), trainIdx(_trainIdx), imgIdx(_imgIdx), distance(_distance) {}

int queryIdx; // 查询图像的特征描述子索引
int trainIdx; // 训练(模板)图像的特征描述子索引
int imgIdx; // 训练图像的索引(若有多个)
float distance; // 两个特征向量之间的欧氏距离,越小表明匹配度越高。
bool operator<( const DMatch &m ) const; // < 操作符重载
};


五. 匹配结果筛选(可选)

在第四步中得到了匹配结果,但并非所有的匹配都是优质匹配,因此要进行筛选,选出最好的一部分匹配结果。

选出最好的几个,也就是 Dmatch 中距离最小的几个。

选出 Dmatch 中距离小于某些阈值的匹配。

其中方法1可以调用
nth_element
函数实现,如下所示:

//-- Step 5: 筛选匹配
nth_element(matches.begin(),  // 初始位置
matches.begin() + 24, // 排序元素位置
matches.end()); // 终止位置
matches.erase(matches.begin() + 25, matches.end());//移除第25位之后的元素


方法2可以这样实现:

//-- Step 5: 筛选匹配
double max_dist = 0; // 匹配中的最小距离
double min_dist = 100;// 匹配中的最大距离
for (int i = 0; i < descriptors1.rows; i++)
{
double dist = matches[i].distance;
if (dist < min_dist) min_dist = dist;
if (dist > max_dist) max_dist = dist;
}
vector< DMatch > good_matches; // 筛选出的好的匹配
for (int i = 0; i < descriptors1.rows; i++)
{
if (matches[i].distance <= max(2 * min_dist, 0.02))
{
good_matches.push_back(matches[i]);
}
}


六. 绘制匹配点(可选)

为了让匹配结果可视化,OpenCV 提供了一个绘制函数以产生由两幅图像衔接而成的图像,匹配的点由直线链接。

//-- Step 6: 绘制匹配
Mat imagematches;
drawMatches(image1, keypoints1, image2, keypoints2,
good_matches,
imagematches,
Scalar::all(-1),
Scalar::all(-1),
vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);

//-- 显示检测到的匹配
imshow("Good Matches", imagematches);


七. 关键点转换

在第二步检测得到的特征点是 KeyPoint 类型的,而在计算单应性矩阵 H 时需要输入的关键点是 Point2f 类型的,因此在计算 H 矩阵之前要先进行关键点的转换。

OpenCV 中定义了 Point_ 类模板,而 Point2f 就由此而来,所谓的 Point2f 其实就是一个 float 类型的二维数据集合(x, y),同样还有 Point2d 为 double 型;当然也有 Point3f 。

//-- Step 7:  把 keypoints 转换为 Point2f
vector<Point2f> points1, points2;
for (vector<DMatch>::const_iterator it = matches.begin(); it != matches.end(); ++it)
{
// /获得左侧 keypoints 的位置
float x = keypoints1[it->queryIdx].pt.x;
float y = keypoints1[it->queryIdx].pt.y;
points1.push_back(Point2f(x, y));
// 获得右侧 keypoints 的位置
x = keypoints2[it->trainIdx].pt.x;
y = keypoints2[it->trainIdx].pt.y;
points2.push_back(Point2f(x, y));
}


八. 计算单应性矩阵 H

在计算机视觉领域中,空间中同一平面的任意两幅图像通过单应性关联在一起(假定是针孔相机)。比如,一个物体可以通过旋转相机镜头获取两张不同的照片(这两张照片的内容不一定要完全对应,部分对应即可),我们可以把单应性设为一个二维矩阵 M,那么照片1乘以 M 就是照片2。

参考WIKI 单应性

3D 点与它在相机图像中像素之间的关系存在投影关系,可以用 3x4 的矩阵表示。其中前三列表示旋转,第四列表示平移。如果同一场景的两个视图之间仅有的区别是旋转,那么第四列就是 0,于是投影关系就变成一个 3x3 的矩阵,也就是我们要求的单应性矩阵。

//-- Step 8:  找到两幅图像之间的单应矩阵
vector<uchar> inliers(points1.size(), 0);
Mat homography = findHomography(
Mat(points1), Mat(points2), // 对应的点集
inliers,    // 输出的正确值
CV_RANSAC,  // RANSAC 法
1.);        // 到反投影点的最大距离


point1 和 point2 就是 Point2f 类型的关键点;inliers 输出正确的匹配点;CV_RANSAC 表示使用 RANSAC 方法。

九. 绘制正确匹配的点(可选)

当计算 H 矩阵的时候,有些点会引起计算误差,称为 outlier 或异常点;而其他点有助于计算出正确的估计 H,这些点称为 inlier 或正常点。findHomography 函数提供了参数输出 inlier,因此可以在每个正常点的位置上标记一个圆来显示出这些匹配到的点。

//-- Step 9: 在两幅图中分别绘出匹配到的点
// 绘制 inlier 点
vector<Point2f>::const_iterator itPts = points1.begin();
vector<uchar>::const_iterator itIn = inliers.begin();
while (itPts != points1.end())
{
// 在每个 inlier 位置画圆
if (*itIn)
circle(image1, *itPts, 3, Scalar(255, 255, 255), 2);

++itPts;
++itIn;
}

itPts = points2.begin();
itIn = inliers.begin();
while (itPts != points2.end())
{
// 在每个 inlier 位置画圆
if (*itIn)
circle(image2, *itPts, 3, Scalar(255, 255, 255), 2);

++itPts;
++itIn;
}

// 显示带点图像
namedWindow("Image 1 Homography Points");
imshow("Image 1 Homography Points", image1);
namedWindow("Image 2 Homography Points");
imshow("Image 2 Homography Points", image2);


十. 映射图1到图2

在计算过单应性矩阵之后,就可以将一幅图像按照单应性矩阵映射到另一个坐标系中,从而实现两幅图像的匹配。

//-- Step 10: 映射图1 到 图2
Mat result;
warpPerspective(image1, // 输入图像
result,         // 输出图像
homography,     // homography
Size(2 * image1.cols, image1.rows)); // 输出图像大小

// 复制图1到整幅图像的前半部份
Mat half(result, Rect(0, 0, image2.cols, image2.rows));
image2.copyTo(half);// 复制图2到图1的ROI区域

// 显示映射后的图像
namedWindow("After warping");
imshow("After warping", result);


参考

参考《OpenCV 2 计算机视觉编程手册》

参考《The OpenCV Reference Manual-2.4.9.0》

参考《The OpenCV Tutorials-2.4.9.0》

代码

GitHub
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息