图像拼接是指在相同场景条件下,将多个图像的交叠部位结合起来而形成完整的宽视角图像的处理过程。其整个拼接过程从本质上可以分为六个步骤,如图1所示
-
图像采集
全景图像在拼接的过程中,首先需要选取原始待拼接图像。通常而言,待拼接图像是通过相机、摄像头等设备采集而来的。
待拼接图像在拍摄时,必须要保证拍摄的多张图像之间存在一定的交叠,这样在拼接的过程中,才能够做到有效的配准。此外,为了满足图像无缝融合的要求,待拼接图像在拍摄的过程中,必须处于同一环境之中,同时拍摄时要保证平行的连续拍摄,否则将大大影响配准的精度。 -
图像特征提取
图像特征信息提取,实质上指的是对图像本质特性的采集。在图像拼接中,图像的灰度值是一个典型的图像特征,其他的还有边缘值等特征。针对这些不同的特征,所采取的采集方式也各不一致。通常情况下,图像基本上具备三大特征:一是区域方面的特征;二是边缘方面的特征;三是点方面的特征。
1、区域特征
所谓的图像区域特征,实质上就是特定图像区域中图像信息的基本特性,一般而言,图像区域内的像素信息值就是该区域特征的具体表现。由于提取图像区域特征不必预先对图像进行处理,因此有较高的运算效率,但是其计算精度极易受到光照、噪声等因素的干扰。基于此特点,区域特征对于两张相邻图像的像素信息差别不是很大时有较好的拼接效果。
2、边缘特征
所谓的图像边缘特征,指的是在某一个方向上,像素的变化值,该变化值的计算公式如下:
常用的边缘检测算法有三种,Roberts检测法、Canny检测法及Sobel检测法。
3、点特征
图像点特征指的是部分区域内相邻像素的差值,其中差值最大的称为局部极值点,也就是特征点。虽然点特征的获取过程较为复杂,但是点特征较为稳定,不会受到图像尺度变化、旋转等外界环境因素的影响。 -
图像特征信息的匹配
1 基于区域特征的图像匹配
采用对区域特征这一图像特征信息进行提取的方法,可以计算出两幅图像之间的空间位置关系,随后将求解的空间位置进行坐标的统一化。一般情况下,图像的匹配程度取决于NC(Normalized Correlation)值,NC 值的表达式如式3-2所示,NC值越高,则代表匹配程度越高。基于灰度信息的图像匹配过中,为了消除拼接之后遗留下来的缝隙和痕迹,必须采用一定的融合算法,来实现无缝拼接。
特征匹配算法的核心在于图像待匹配区域内特征信息域的提取,提取后的特
征信息域将被映射至一个高维空间中进行匹配,这在很大程度上降低了计算难度。实际操作过程中,由于特征信息区域受外界因素的影响较小,因而能达到较好的匹配效果,其相关运算公式如下:
2 基于频域的图像匹配
建立在频域之上的图像匹配,就是将传统的物理空间计算转变为频域空间计
算。频域图像匹配在计算过程中,以频域空间来替代时域空间,这个过程通过对相邻两幅图像的重叠区域内设置一条尽量远离边界的过渡线来实现,该过渡线要与边界点足够远,这是由于边界点作为结构的组成不可随意移动,只要稍有移动便会影响图像的拼接。
3 基于特征点的图像匹配
根据特征点进行图像匹配,首先要提取出图像的特征信息,然后匹配两个图像的特征信息。由于在图像匹配的整个过程中,特征点十分关键,因此特征点的提取显得非常重要,在特征点提取时,必须注意两点:一是需要尽可能多的提取出多个特征点;二是在提取的过程中,要对特征点的特征进行判断,提取具备典型图像特征的特征点。除此之外,特征点在提取的过程中还必须考虑时间因素。 -
图像统一坐标变换
一般情况下,几何变换主要包括以下几类:平移变换、刚性变换、仿射变换
及投影变换。其他更为复杂的变换形式都可以由上面的基本变换通过组合来获
得。
图 2.2 图像变换的模型
1、刚性变换
垂直方向与水平方向都保持原有形态的变换称作刚性变换。由于这个特性,
在进行参数确定时,其垂直方向与水平方向的形变参数皆为0。
刚性变换的变换矩阵为:
因此对于刚性变换,只需要求解其中的三个参数,就可以得到变换矩阵。
2、仿射变换
仿射变换(Affine Transformation 或 Affine Map)是一种二维坐标之间的线性变换,它保持了二维图形的“平直性”和“平行性”。换句话说,经过仿射变换后的图形,直线仍是直线,平行线仍是平行线,且直线上的点的位置顺序也没有变化。进行仿射变换时,解出变换矩阵的6个参数后,便可获得相应的仿射变换矩阵。在此就不一一练出各个变换计算公式。
3、投影变换
直线在变换后第二幅图像上所映射的还是直线,但平行关系不存在了,这种变换就是投影变换。 -
图像融合
图像融合的定义是图像经过配准后,通过变换将待处理的两幅图像保持在同一个坐标系内,同时应用相应的方式进行融合而得到一个新图像的过程。
图像融合的方法主要有以下几种:
1、直接平均法
直接平均法的原理相对简单,其将图像重叠区域内像素点灰度值的平均值直
接作为拼接后图像中该点的灰度值。这种方式的优势在于易于操作,但不足之处是这样所得的图像效果不佳,并且可以看出较为明显的拼接痕迹。
下面为直接平均法的具体的算法步骤,将融合后的图像用f表示,f1和f2则表示待处理的两幅图像:
2、加权平均法
假设两幅待拼接图片分别为f1和f2,f为融合后的图像,那么重叠区域处
的像素灰度值的表达式改为:
-
合成图像
通过上述这些步骤基本就生成了我们需要的合成得图像。
以下为surf图像拼接实现代码:
#include <iostream>
#include <stdio.h>
#include "opencv2/core.hpp"
#include "opencv2/core/utility.hpp"
#include "opencv2/core/ocl.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/features2d.hpp"
#include "opencv2/calib3d.hpp"
#include "opencv2/imgproc.hpp"
#include"opencv2/flann.hpp"
#include"opencv2/xfeatures2d.hpp"
#include"opencv2/ml.hpp"
using namespace cv;
using namespace std;
using namespace cv::xfeatures2d;
using namespace cv::ml;
void OptimizeSeam(Mat& img1, Mat& trans, Mat& dst);
typedef struct
{
Point2f left_top;
Point2f left_bottom;
Point2f right_top;
Point2f right_bottom;
}four_corners_t;
four_corners_t corners;
void CalcCorners(const Mat& H, const Mat& src)
{
double v2[] = { 0, 0, 1 };//左上角
double v1[3];//变换后的坐标值
Mat V2 = Mat(3, 1, CV_64FC1, v2); //列向量
Mat V1 = Mat(3, 1, CV_64FC1, v1); //列向量
V1 = H * V2;
//左上角(0,0,1)
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;
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;
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;
corners.right_bottom.x = v1[0] / v1[2];
corners.right_bottom.y = v1[1] / v1[2];
}
int main()
{
float start_time = getTickCount();
string name = "1";
Mat a = imread(name + "(2).jpg", 1);//右图
Mat b = imread(name + "(1).jpg", 1);//左图
if (a.empty() || b.empty()) {
cout << "could not find a or b image,please press exit to exit.." << endl;
if(waitKey()==27)return 0;
}
Ptr<SURF> surf; //创建方式和OpenCV2中的不一样,并且要加上命名空间xfreatures2d
//否则即使配置好了还是显示SURF为未声明的标识符
surf = SURF::create(800);
BFMatcher matcher; //实例化一个暴力匹配器
Mat c, d;
vector<KeyPoint>key1, key2;
vector<DMatch> matches; //DMatch是用来描述匹配好的一对特征点的类,包含这两个点之间的相关信息
//比如左图有个特征m,它和右图的特征点n最匹配,这个DMatch就记录它俩最匹配,并且还记录m和n的
//特征向量的距离和其他信息,这个距离在后面用来做筛选
surf->detectAndCompute(a, Mat(), key1, c);//输入图像,输入掩码,输入特征点,输出Mat,存放所有特征点的描述向量
surf->detectAndCompute(b, Mat(), key2, d);//这个Mat行数为特征点的个数,列数为每个特征向量的尺寸,SURF是64(维)
matcher.match(d, c, matches); //匹配,数据来源是特征向量,结果存放在DMatch类型里面
//sort函数对数据进行升序排列
sort(matches.begin(), matches.end()); //筛选匹配点,根据match里面特征对的距离从小到大排序
vector< DMatch > good_matches;
int ptsPairs = std::min(50, (int)(matches.size() * 0.15));
cout << ptsPairs << endl;
for (int i = 0; i < ptsPairs; i++)
{
good_matches.push_back(matches[i]);//距离最小的50个压入新的DMatch
}
Mat outimg; //drawMatches这个函数直接画出摆在一起的图
drawMatches(b, key2, a, key1, good_matches, outimg, Scalar::all(-1), Scalar::all(-1), vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS); //绘制匹配点
imwrite(name + "match.jpg", outimg);
imshow("桌面", outimg);
///图像配准及融合
vector<Point2f> imagePoints1, imagePoints2;
for (int i = 0; i<good_matches.size(); i++)
{
imagePoints2.push_back(key2[good_matches[i].queryIdx].pt);
imagePoints1.push_back(key1[good_matches[i].trainIdx].pt);
}
//获取图像1到图像2的投影映射矩阵 尺寸为3*3
Mat homo = findHomography(imagePoints1, imagePoints2, CV_RANSAC);
也可以使用getPerspectiveTransform方法获得透视变换矩阵,不过要求只能有4个点,效果稍差
//Mat homo=getPerspectiveTransform(imagePoints1,imagePoints2);
cout << "变换矩阵为:\n" << homo << endl << endl; //输出映射矩阵
//计算配准图的四个顶点坐标
CalcCorners(homo, a);
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 imageTransform1, imageTransform2;
warpPerspective(a, imageTransform1, homo, Size(MAX(corners.right_top.x, corners.right_bottom.x), b.rows));
//warpPerspective(a, imageTransform2, adjustMat*homo, Size(b.cols*1.3, b.rows*1.8));
imshow("直接经过透视矩阵变换", imageTransform1);
//imwrite(name+"trans.jpg", imageTransform1);
//创建拼接后的图,需提前计算图的大小
int dst_width = imageTransform1.cols; //取最右点的长度为拼接图的长度
int dst_height = b.rows;
Mat dst(dst_height, dst_width, CV_8UC3);
dst.setTo(0);
imageTransform1.copyTo(dst(Rect(0, 0, imageTransform1.cols, imageTransform1.rows)));
b.copyTo(dst(Rect(0, 0, b.cols, b.rows)));
//imwrite(name + "b_dst", dst);
imshow(name + "b_dst", dst);
OptimizeSeam(b, imageTransform1, dst);
imshow(name + "dst", dst);
imwrite(name + "dst.jpg", dst);
float time = (getTickCount() - start_time) / (getTickFrequency());
cout << "time: " << time <<" s"<< endl;
waitKey();
return 0;
}
//优化两图的连接处,使得拼接自然
void OptimizeSeam(Mat& img1, Mat& trans, Mat& dst)
{
int start = MIN(corners.left_top.x, corners.left_bottom.x);//开始位置,即重叠区域的左边界
double processWidth = img1.cols - start;//重叠区域的宽度
int rows = dst.rows;
int cols = img1.cols; //注意,是列数*通道数
double alpha = 1;//img1中像素的权重
for (int i = 0; i < rows; i++)
{
uchar* p = img1.ptr<uchar>(i); //获取第i行的首地址
uchar* t = trans.ptr<uchar>(i);
uchar* d = dst.ptr<uchar>(i);
for (int j = start; j < cols; j++)
{
//如果遇到图像trans中无像素的黑点,则完全拷贝img1中的数据
if (t[j * 3] == 0 && t[j * 3 + 1] == 0 && t[j * 3 + 2] == 0)
{
alpha = 1;
}
else
{
//img1中像素的权重,与当前处理点距重叠区域左边界的距离成正比,实验证明,这种方法确实好
alpha = (processWidth - (j - start)) / processWidth;
}
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);
}
}
}
拼接原图:
配准效果图:
拼接效果图: