目前OpenCV已经有了可以实现图像拼接的类Stitcher可以实现图像拼接,不过我想先自己利用SURF特征实现图像拼接后再去看一下Opencv自带的Stitcher类。
参考资料:
https://www.cnblogs.com/skyfsm/p/7411961.html – SURF特征检测和拼接过程
环境:Win10 + VS2015 + OpenCV3.2
分析图像时从图像中选取某些特征点并对图像进行局部分析,而非观察整幅图像,从而可以减少计算量,达到有效的分析目的。
改良版的SIFT;
1.尺度不变性:不仅在任何尺度下拍摄的物体都能检测到一致的关键点,而且每个被检测的特征点都对应一个尺度因子。
2.具有较高计算效率。
如果直接使用SURF的特征点来进行两个图片的特征点匹配,则会有许多糟糕的匹配点出现,会得到不理想的效果(瞎**匹),虽然其中也包括了良好的匹配点,但是如果不加处理会导致相匹配的点太多而得不到好的结果,因此需要对特征点进行选择,找到好的特征点。
SURF与SIFT的特征点选择类似:
SIFT的作者Lowe提出了比较最近邻距离与次近邻距离的SIFT匹配方式:取一幅图像中的一个SIFT关键点,并找出其与另一幅图像中欧式距离最近的前两个关键点,在这两个关键点中,如果最近的距离除以次近的距离得到的比率ratio少于某个阈值T,则接受这一对匹配点。因为对于错误匹配,由于特征空间的高维性,相似的距离可能有大量其他的错误匹配,从而它的ratio值比较高。显然降低这个比例阈值T,SIFT匹配点数目会减少,但更加稳定,反之亦然。
对于特征匹配的方法还有SIFT与ORB,SIFT很精确但是速度不够快,ORB没有旋转不变性和尺度不变性,因此我选择使用SURF特征检测。
我的理解是,对于一个图像的拼接,我们需要两张图片,然后对其中一张图片做变换到另一张图片的坐标系下,然后对这两张图片拼接并优化拼接。因此程序的流程是:
1.特征点提取和匹配
2.图像配准(将一张图片转换到另一张图片的坐标系下)
3.图像拷贝
4.图像融合
完整代码如下:
#include "highgui.hpp"
#include "core.hpp"
#include "features2d.hpp"
#include "xfeatures2d.hpp"
#include "calib3d.hpp"
#include "cv.hpp"
#include
using namespace std;
using namespace cv;
using namespace cv::xfeatures2d;
/*
参数 Mat作为参数传递时 使用参数如果为 &img 则如果在调用 f(img)时修改Mat的值的话外面
*/
Mat stichingWithSURF(Mat mat1, Mat mat2);
void calCorners(const Mat& H,const Mat& src);//计算变换后的角点
Mat extractFeatureAndMatch(Mat mat1,Mat mat2); //特征提取和匹配
Mat splicImg(Mat& mat1, Mat& mat2, vector goodMatchPoints, vector keyPoint1, vector keyPoint2);
void optimizeSeam(Mat &mat1,Mat& trans,Mat& dst); //优化拼接
void stichingWithStitcher(Mat mat1, Mat mat2);
typedef struct
{
//变换后的图片4个点
Point2f left_top;
Point2f left_bottom;
Point2f right_top;
Point2f right_bottom;
}four_corners_t;
four_corners_t corners;
void main()
{
Mat img1, img2;
img1 = imread("img_left.jpg");
img2 = imread("img_right.jpg");
resize(img1, img1, Size(img1.cols / 4, img1.rows / 4));
resize(img2, img2, Size(img2.cols / 4, img2.rows / 4));
Mat dst = stichingWithSURF(img1, img2);
imshow("拼好的图像", dst);
waitKey();
}
Mat stichingWithSURF(Mat mat1,Mat mat2)
{
//用SURF 是因为 SURF有旋转不变性而且比SIFT更快
/*
1.特征点提取和匹配
2.图像配准
3.图像拷贝
4.图像融合
*/
return extractFeatureAndMatch(mat1, mat2);
}
//定位图像变换之后的四个角点
void calCorners(const Mat & H, const Mat & src)
{
//H为 变换矩阵 src为需要变换的图像
//计算配准图的角点(齐次坐标系描述)
double v2[] = { 0,0,1 }; //左上角
double v1[3]; //变换后的坐标值
//构成 列向量` 这种构成方式将 向量 与 Mat 关联, Mat修改 向量也相应修改
Mat V2 = Mat(3, 1, CV_64FC1, v2);
Mat V1 = Mat(3, 1, CV_64FC1, v1);
V1 = H * V2; //元素*
cout << "0v1:" << v1[0] << endl;
cout << "V2: " << V2 << endl;
cout << "V1: " << V1 << endl;
//左上角(转换为一般的二维坐标系)
corners.left_top.x = v1[0] / v1[2];
corners.left_top.y = v1[1] / v1[2];
//左下角(0,src.rows,1)
v2[0] = 0;
v2[1] = src.rows;
v2[2] = 1;
V2 = Mat(3, 1, CV_64FC1, v2);
V1 = Mat(3, 1, CV_64FC1, v1); //列向量
V1 = H * V2;
cout << "1v1:" << v1[0] << endl;
cout << "V2: " << V2 << endl;
cout << "V1: " << V1 << endl;
corners.left_bottom.x = v1[0] / v1[2];
corners.left_bottom.y = v1[1] / v1[2];
//右上角(src.cols,0,1)
v2[0] = src.cols;
v2[1] = 0;
v2[2] = 1;
V2 = Mat(3, 1, CV_64FC1, v2);
V1 = Mat(3, 1, CV_64FC1, v1);
V1 = H * V2;
cout << "2v1:" << v1 << endl;
cout << "V2: " << V2 << endl;
cout << "V1: " << V1 << endl;
corners.right_top.x = v1[0] / v1[2];
corners.right_top.y = v1[1] / v1[2];
//右下角(src.cols,src.rows,1)
v2[0] = src.cols;
v2[1] = src.rows;
v2[2] = 1;
V2 = Mat(3, 1, CV_64FC1, v2); //列向量
V1 = Mat(3, 1, CV_64FC1, v1); //列向量
V1 = H * V2;
cout << "3v1:" << v1 << endl;
cout << "V2: " << V2 << endl;
cout << "V1: " << V1 << endl;
corners.right_bottom.x = v1[0] / v1[2];
corners.right_bottom.y = v1[1] / v1[2];
cout << endl;
cout << "left_top:" << corners.left_top << endl;
cout << "left_bottom:" << corners.left_bottom << endl;
cout << "right_top:" << corners.right_top << endl;
cout << "right_bottom:" << corners.right_bottom << endl;
}
Mat extractFeatureAndMatch(Mat mat1, Mat mat2)
{
Mat matg1, matg2;
//转化成灰度图
cvtColor(mat1, matg1,CV_RGB2GRAY);
cvtColor(mat2, matg2, CV_RGB2GRAY);
Ptr surfDetector = SURF::create(1000.0f);
vector keyPoint1, keyPoint2; //特征点
Mat imgDesc1, imgDesc2; //特征点描述矩阵
//检测 计算图像的关键点和描述
surfDetector->detectAndCompute(matg1, noArray(), keyPoint1, imgDesc1);
surfDetector->detectAndCompute(matg2, noArray(), keyPoint2, imgDesc2);
cout << "特征点描述矩阵1大小:(列*行) " << imgDesc1.cols << " * " << imgDesc1.rows << endl;
FlannBasedMatcher matcher; //匹配点
vector<vector > matchPoints; //
vector goodMatchPoints; //良好的匹配点
/*
DMatch 特征匹配相关结构
distance 两个特征向量之间的欧氏距离,越小表明匹配度越高。
*/
//knn匹配特征点 这里将2作为训练集来训练 对应到后面DMatch的trainIdx
vector train_disc(1, imgDesc2);
matcher.add(train_disc);
matcher.train();
//用1来匹配该模型(用分类器去分类1),对应到后面DMatch的quiryIdx
matcher.knnMatch(imgDesc1, matchPoints, 2);//k临近 按顺序排
cout << "total match points: " << matchPoints.size() << endl;
/*
查找集(Query Set)和训练集(Train Set),
对于每个Query descriptor,DMatch中保存了和其最好匹配的Train descriptor。
*/
//获取优秀匹配点
for (int i = 0; i < matchPoints.size(); i++)
{
if (matchPoints[i][0].distance < 0.4f*matchPoints[i][1].distance)
{
goodMatchPoints.push_back(matchPoints[i][0]);
}
}
Mat firstMatch;
//这里drawMatches 第一个图片在左边,同时也对应了DMatch的quiryIdx,第二个图片在右边,同时也对应了DMatch的trainIdx
drawMatches(mat1, keyPoint1, mat2, keyPoint2, goodMatchPoints, firstMatch);
imshow("匹配", firstMatch);
vector imagePoints1, imagePoints2;
for (int i = 0; i < goodMatchPoints.size(); i++)
{
imagePoints1.push_back(keyPoint1[goodMatchPoints[i].queryIdx].pt);
imagePoints2.push_back(keyPoint2[goodMatchPoints[i].trainIdx].pt);
}
return splicImg(mat1, mat2, goodMatchPoints, keyPoint1, keyPoint2);
}
//以图像1为准(1在左半边)
Mat splicImg(Mat & mat_left, Mat & mat2, vector goodMatchPoints ,vector keyPoint1, vector keyPoint2)
{
vector imagePoints1, imagePoints2;
for (int i = 0; i < goodMatchPoints.size(); i++)
{
//这里的queryIdx代表了查询点的目录 trainIdx代表了在匹配时训练分类器所用的点的目录
imagePoints1.push_back(keyPoint1[goodMatchPoints[i].queryIdx].pt);
imagePoints2.push_back(keyPoint2[goodMatchPoints[i].trainIdx].pt);
}
//获取图像2到图像1的投影映射矩阵 3*3
Mat homo = findHomography(imagePoints2, imagePoints1, CV_RANSAC);
cout << "变换矩阵为:\n" << homo << endl << endl; //输出映射矩阵
calCorners(homo, mat2); //计算配准图的四个顶点坐标
Mat imgTransform2;
//图像配准 warpPerspective 对图像进行透视变换 变换后矩阵的宽高都变化
warpPerspective(mat2, imgTransform2, homo, Size(MAX(corners.right_top.x, corners.right_bottom.x), mat2.rows));
imshow("直接经过透视矩阵变换得到的img2", imgTransform2);
//创建拼接后的图
int distW = imgTransform2.cols; //长宽
int distH = mat_left.rows;
Mat dst(distH, distW, CV_8UC3);
dst.setTo(0);
//构成图片
//复制img2到dist的右半部分 先复制transform2的图片(因为这个尺寸比较大,后来的图片可以覆盖到他)
imgTransform2.copyTo(dst(Rect(0, 0, imgTransform2.cols, imgTransform2.rows)));
mat_left.copyTo(dst(Rect(0, 0, mat_left.cols, mat_left.rows)));
imshow("拼接(未优化)", dst);
optimizeSeam(mat_left, imgTransform2, dst);
return dst;
}
//优化链接处
void optimizeSeam(Mat &mat_left, Mat& trans, Mat& dst)
{
int start = MIN(corners.left_bottom.x, corners.left_top.x);//重叠区域的左边界
float processW = mat_left.cols - start; //重叠区的宽度
cout << "开始值:" << start << endl;
cout << "重叠宽度:" << processW << endl;
int rows = dst.rows;
int cols = mat_left.cols;
float alpha = 1.0f; //mat1 中的像素透明度
//修改dst中的透明度
for (int i = 0; i < rows; i++)
{
//第i行地址
uchar *p = mat_left.ptr(i);
uchar *t = trans.ptr(i);
uchar *d = dst.ptr(i);
for (int j = start; j < cols; j++)
{
//遇到trans中无像素的黑点,则完全拷贝mat_left中的像素
if (t[j * 3] == 0 && t[j * 3 + 1] == 0 && t[j * 3 + 2] == 0)
{
//RGB都为0
alpha = 1;
}
else
{
//mat_left中像素的权重与当前处理点距重叠区域左边界的距离成正比
alpha = (processW - (j - start)) / processW;
}
//修改dst中的像素
d[j * 3] = p[j * 3] * alpha + t[j * 3] * (1 - alpha);
d[j * 3 + 1] = p[j * 3 + 1] * alpha + t[j * 3 + 1] * (1 - alpha);
d[j * 3 + 2] = p[j * 3 + 2] * alpha + t[j * 3 + 2] * (1 - alpha);
}
}
}
Ptr surfDetector = SURF::create(1000.0f);
vector keyPoint1, keyPoint2; //特征点
Mat imgDesc1, imgDesc2; //特征点描述矩阵
//检测 计算图像的关键点和描述
surfDetector->detectAndCompute(matg1, noArray(), keyPoint1, imgDesc1);
surfDetector->detectAndCompute(matg2, noArray(), keyPoint2, imgDesc2);
定义一个SURFDetector,参数为门限值,调整这个可以调整检测精度,越大越高,不过相应的速度也会慢;detectAndCompute函数实现了检测特征点并计算特征描述矩阵存储到imgDesc1, imgDesc2中。
3.接下来的匹配类DMatch类:这个类存储了图像特征之间的匹配的信息:
CV_PROP_RW int queryIdx; // query descriptor index 查询Index
CV_PROP_RW int trainIdx; // train descriptor index 训练Index
CV_PROP_RW int imgIdx; // train image index label?
CV_PROP_RW float distance; //特征点之间的欧氏距离
对这个类还不是很理解,不过他这三个变量 queryIdx trainIdx distance还是比较重要的。distance不用说,两个特征点之间的距离,trainIdx应该是在训练分类器时输入训练的点的Index;queryIdx是在利用分类器做回归的时候对应的Index (两个不同的图片,一个用来训练,一个用来做测试,训练与测试(分类)的对应的特征点应该是对应的)。
4.训练网络开始分类
FlannBasedMatcher matcher; //匹配点
vector<vector > matchPoints; // 可以理解成二维矩阵
vector goodMatchPoints; //良好的匹配点
//knn匹配特征点 这里将2作为训练集来训练 对应到后面DMatch的trainIdx
vector train_disc(1, imgDesc2);
matcher.add(train_disc);
matcher.train();
//用1来匹配该模型(用分类器去分类1),对应到后面DMatch的quiryIdx
matcher.knnMatch(imgDesc1, matchPoints, 2);//k临近 按顺序排
利用KNN实现分类,2代表了2个邻居。这里可以看到,为了让第一张图片出现在后面画图(画match结果)中的左边,让2图片的特征点输入进去做训练集,1图片作为测试集来实现回归(1去匹配2,因此后面2图片DMatch的index对应trainIdx,1图片对应quiryIdx),这样便可以得到两张图片对应的DMatch。
vector <vector > matchPoints
我理解成一个二维矩阵,行为当前正在做回归的点(img1中的),每列代表和当前点做回归的另一张图片(img2)的DMatch值。
5.获取优秀匹配点:
for (int i = 0; i < matchPoints.size(); i++)
{
if (matchPoints[i][0].distance < 0.4f*matchPoints[i][1].distance)
{
goodMatchPoints.push_back(matchPoints[i][0]);
}
}
根据欧氏距离选择匹配良好的点。
6.尝试着画一下匹配结果:
Mat firstMatch;
drawMatches(mat1, keyPoint1, mat2, keyPoint2, goodMatchPoints, firstMatch);
imshow("匹配", firstMatch);
这里,drawMatches 显示在窗口中时第一个图片(mat1)在左边,同时也对应了DMatch的quiryIdx,第二个图片(mat2)在右边,同时也对应了DMatch的trainIdx,这两个不能倒过来,否则可能会出现数组越界之类的恶心BUG。
7.提取到特征之后对2图像进行变换,投影到图像1下
首先在得到变换矩阵之前,先得到特征点的Point2f类型的坐标:
vector imagePoints1, imagePoints2;
for (int i = 0; i < goodMatchPoints.size(); i++)
{
//这里的queryIdx代表了查询点的目录 trainIdx代表了在匹配时训练分类器所用的点的目录
imagePoints1.push_back(keyPoint1[goodMatchPoints[i].queryIdx].pt);
imagePoints2.push_back(keyPoint2[goodMatchPoints[i].trainIdx].pt);
}
**一定要注意**queryIdx和trainIdx的对应!!
然后进行透视变换
//获取图像2到图像1的投影映射矩阵 3*3
Mat homo = findHomography(imagePoints2, imagePoints1, CV_RANSAC);
calCorners(homo, mat2); //计算配准图的四个顶点坐标
Mat imgTransform2; //变换过去之后的2图像
//图像配准 warpPerspective 对图像进行透视变换 变换后矩阵的宽高都变化
warpPerspective(mat2, imgTransform2, homo, Size(MAX(corners.right_top.x, corners.right_bottom.x), mat2.rows));
calCorners利用的是齐次坐标系计算坐标面比较方便。warpPerspective是OpenCV自带的透视变换函数。
8.变换之后进行图像的复制,构成新的图片:
//创建拼接后的图
int distW = imgTransform2.cols; //长宽
int distH = mat_left.rows; //这里对应的mat1
Mat dst(distH, distW, CV_8UC3);
dst.setTo(0);
//构成图片
//复制img2到dist的右半部分 先复制transform2的图片(因为这个尺寸比较大,后来的图片可以覆盖到他)
imgTransform2.copyTo(dst(Rect(0, 0, imgTransform2.cols, imgTransform2.rows)));
mat_left.copyTo(dst(Rect(0, 0, mat_left.cols, mat_left.rows)));
copyTo函数在不使用Mask参数时复制的话,将图片黑色部分忽略,仅复制有颜色的部分。也就是黑色会被替换掉。所以要先复制imgTransform2,这个里面会因为变换而产生许多黑色的部分,然后再复制img1(也就是在左边 ,没有变换的图像)过去覆盖掉黑色。反过来的话黑色会把它覆盖掉。
9.优化拼接
optimizeSeam函数来优化拼接,思想大概是alpha参数根据2图片(变化的图片,右侧的图片)与1重叠的位置来设置值,在重叠部分的值是由两个图片的像素值α加权得到的。注意下面的*3,因为RGB。