OpenCV C++案例实战十二《图像全景拼接》

OpenCV C++案例实战十二《图像全景拼接》

  • 前言
  • 一、OpenCV Stitcher
    • 1.功能源码
    • 2.效果
  • 二、图像全景拼接
    • 1.特征检测
    • 2.计算单应性矩阵
    • 3.透视变换
    • 4.图像拼接
    • 5.功能源码
    • 6.效果
  • 三、图像融合
    • 1.效果
  • 四、源码
  • 总结


前言

本文将使用OpenCV C++ 进行图像全景拼接。目前使用OpenCV对两幅图像进行拼接大致可以分为两类。
一、使用OpenCV内置API Stitcher 进行拼接。
二、使用特征检测算法匹配两幅图中相似的点、计算变换矩阵、最后对其进行透视变换就可以了。

一、OpenCV Stitcher

image_left
OpenCV C++案例实战十二《图像全景拼接》_第1张图片
image_right
OpenCV C++案例实战十二《图像全景拼接》_第2张图片
原图如图所示。本案例的需求是将上述两幅图片拼接成一幅图像。首先使用OpenCV提供的Stitcher进行拼接。关于Stitcher的具体原理请大家自行查找相关资料。

1.功能源码

bool OpenCV_Stitching(Mat image_left, Mat image_right)
{
	//将待拼接图片放进容器里面
	vector<Mat>images;
	images.push_back(image_left);
	images.push_back(image_right);

	//创建Stitcher模型
	Ptr<Stitcher>stitcher = Stitcher::create();

	Mat result;
	Stitcher::Status status = stitcher->stitch(images, result);// 使用stitch函数进行拼接

	if (status != Stitcher::OK) return false;

	imshow("OpenCV图像全景拼接", result);

	return true;
}

2.效果

OpenCV C++案例实战十二《图像全景拼接》_第3张图片
这就是使用OpenCV 内置Stitcher拼接出来的效果。

二、图像全景拼接

1.特征检测

使用方法二进行图像全景拼接。由于我们需要用到SIFT或SURF特征检测算子,故我们需要提前配置nonfree模块,关于如何配置nonfree模块请大家自行查阅资料,网上教程也有很多。

目前网上教程大致流程归为:

1、使用特征检测算子提取两幅图像的关键点,然后进行特征描述子匹配。我这里使用的是SURF算子。当然SIFT等其他特征检测算子也可以。

	//创建SURF特征检测器
	int Hessian = 800;
	Ptr<SURF>detector = SURF::create(Hessian);

	//进行图像特征检测、特征描述
	vector<KeyPoint>keypoint_left, keypoint_right;
	Mat descriptor_left, descriptor_right;
	detector->detectAndCompute(image_left, Mat(), keypoint_left, descriptor_left);
	detector->detectAndCompute(image_right, Mat(), keypoint_right, descriptor_right);

	//使用FLANN算法进行特征描述子的匹配
	FlannBasedMatcher matcher;
	vector<DMatch>matches;
	matcher.match(descriptor_left, descriptor_right, matches);

OpenCV C++案例实战十二《图像全景拼接》_第4张图片
如图为使用FLANN算法进行特征描述子匹配的结果。我们需要把那些匹配程度高的关键点筛选出来用以下面计算两幅图像的单应性矩阵。

2、筛选出匹配程度高的关键点

	double Max = 0.0;
	for (int i = 0; i < matches.size(); i++)
	{
		//float distance –>代表这一对匹配的特征点描述符(本质是向量)的欧氏距离,数值越小也就说明两个特征点越相像。
		double dis = matches[i].distance;
		if (dis > Max)
		{
			Max = dis;
		}
	}

	//筛选出匹配程度高的关键点
	vector<DMatch>goodmatches;
	vector<Point2f>goodkeypoint_left, goodkeypoint_right;
	for (int i = 0; i < matches.size(); i++)
	{
		double dis = matches[i].distance;
		if (dis < 0.15*Max)
		{
			/*
			以右图做透视变换
			左图->queryIdx:查询点索引(查询图像)
			右图->trainIdx:被查询点索引(目标图像)
			*/
			//注:对image_right图像做透视变换,故goodkeypoint_left对应queryIdx,goodkeypoint_right对应trainIdx
			//int queryIdx –>是测试图像的特征点描述符(descriptor)的下标,同时也是描述符对应特征点(keypoint)的下标。
			goodkeypoint_left.push_back(keypoint_left[matches[i].queryIdx].pt);
			//int trainIdx –> 是样本图像的特征点描述符的下标,同样也是相应的特征点的下标。
			goodkeypoint_right.push_back(keypoint_right[matches[i].trainIdx].pt);
			goodmatches.push_back(matches[i]);
		}
	}

如图为image_left筛选出来的关键点。
OpenCV C++案例实战十二《图像全景拼接》_第5张图片
如图为image_right筛选出来的关键点。
OpenCV C++案例实战十二《图像全景拼接》_第6张图片
从上图可以看出,我们已经筛选出image_left,image_right共有的关键点部分。接下来,我们需要使用这两个点集计算两幅图的单应性矩阵。

2.计算单应性矩阵

3、计算单应性变换矩阵

	//获取图像right到图像left的投影映射矩阵,尺寸为3*3
	//注意顺序,srcPoints对应goodkeypoint_right,dstPoints对应goodkeypoint_left
	H = findHomography(goodkeypoint_right, goodkeypoint_left, RANSAC);

3.透视变换

4、根据计算出来的单应性矩阵对image_right进行透视变换

	//对image_right进行透视变换
	warpPerspective(image_right, WarpImg, H, Size(image_right.cols + image_left.cols, image_right.rows));
	imshow("透视变换", WarpImg);

OpenCV C++案例实战十二《图像全景拼接》_第7张图片
如图所示为image_right进行透视变换得到的结果。

4.图像拼接

5、根据上述操作,我们已经得到了经透视变换的WarpImg,接下来只需将image_left与WarpImg拼接起来就可以了。

	//将image_left拷贝到透视变换后的图片上,完成图像拼接
	image_left.copyTo(DstImg(Rect(0, 0, image_left.cols, image_left.rows)));
	imshow("图像全景拼接", DstImg);

5.功能源码

bool Image_Stitching(Mat image_left, Mat image_right, Mat& H, Mat & WarpImg, Mat &DstImg, bool draw)
{
	//创建SURF特征检测器
	int Hessian = 800;
	Ptr<SURF>detector = SURF::create(Hessian);

	//进行图像特征检测、特征描述
	vector<KeyPoint>keypoint_left, keypoint_right;
	Mat descriptor_left, descriptor_right;
	detector->detectAndCompute(image_left, Mat(), keypoint_left, descriptor_left);
	detector->detectAndCompute(image_right, Mat(), keypoint_right, descriptor_right);

	//使用FLANN算法进行特征描述子的匹配
	FlannBasedMatcher matcher;
	vector<DMatch>matches;
	matcher.match(descriptor_left, descriptor_right, matches);

	double Max = 0.0;
	for (int i = 0; i < matches.size(); i++)
	{
		//float distance –>代表这一对匹配的特征点描述符(本质是向量)的欧氏距离,数值越小也就说明两个特征点越相像。
		double dis = matches[i].distance;
		if (dis > Max)
		{
			Max = dis;
		}
	}

	//筛选出匹配程度高的关键点
	vector<DMatch>goodmatches;
	vector<Point2f>goodkeypoint_left, goodkeypoint_right;
	for (int i = 0; i < matches.size(); i++)
	{
		double dis = matches[i].distance;
		if (dis < 0.15*Max)
		{
			/*
			以右图做透视变换
			左图->queryIdx:查询点索引(查询图像)
			右图->trainIdx:被查询点索引(目标图像)
			*/
			//注:对image_right图像做透视变换,故goodkeypoint_left对应queryIdx,goodkeypoint_right对应trainIdx
			//int queryIdx –>是测试图像的特征点描述符(descriptor)的下标,同时也是描述符对应特征点(keypoint)的下标。
			goodkeypoint_left.push_back(keypoint_left[matches[i].queryIdx].pt);
			//int trainIdx –> 是样本图像的特征点描述符的下标,同样也是相应的特征点的下标。
			goodkeypoint_right.push_back(keypoint_right[matches[i].trainIdx].pt);
			goodmatches.push_back(matches[i]);
		}
	}

	//绘制特征点
	if (draw)
	{
		Mat result;
		drawMatches(image_left, keypoint_left, image_right, keypoint_right, goodmatches, result);
		imshow("特征匹配", result);

		Mat temp_left = image_left.clone();
		for (int i = 0; i < goodkeypoint_left.size(); i++)
		{
			circle(temp_left, goodkeypoint_left[i], 3, Scalar(0, 255, 0), -1);
		}
		imshow("goodkeypoint_left", temp_left);

		Mat temp_right = image_right.clone();
		for (int i = 0; i < goodkeypoint_right.size(); i++)
		{
			circle(temp_right, goodkeypoint_right[i], 3, Scalar(0, 255, 0), -1);
		}
		imshow("goodkeypoint_right", temp_right);
	}

	//findHomography计算单应性矩阵至少需要4个点
	/*
	计算多个二维点对之间的最优单映射变换矩阵H(3x3),使用MSE或RANSAC方法,找到两平面之间的变换矩阵
	*/
	if (goodkeypoint_left.size() < 4 || goodkeypoint_right.size() < 4) return false;


	//获取图像right到图像left的投影映射矩阵,尺寸为3*3
	//注意顺序,srcPoints对应goodkeypoint_right,dstPoints对应goodkeypoint_left
	H = findHomography(goodkeypoint_right, goodkeypoint_left, RANSAC);

	//对image_right进行透视变换
	warpPerspective(image_right, WarpImg, H, Size(image_right.cols + image_left.cols, image_right.rows));
	imshow("透视变换", WarpImg);

	DstImg = WarpImg.clone();
	//将image_left拷贝到透视变换后的图片上,完成图像拼接
	image_left.copyTo(DstImg(Rect(0, 0, image_left.cols, image_left.rows)));
	imshow("图像全景拼接", DstImg);

	return true;
}

6.效果

OpenCV C++案例实战十二《图像全景拼接》_第8张图片
如上图所示,我们已经完成了图像拼接。但是从上图可以看出,拼接效果有明显的拼接缝,故我们还需要对拼接结果进行图像融合,使拼接效果看起来更自然。

三、图像融合

OpenCV C++案例实战十二《图像全景拼接》_第9张图片
如上图所示,我们需要对红线区域进行融合。即将image_left与WarpImg重叠区域进行像素融合,使拼接效果过渡更加自然。

//图像融合,使得拼接自然
bool OptimizeSeam(Mat H, Mat& image_left, Mat& WarpImg, Mat& DstImg)
{
	//透视变换左上角(0,0,1)
	Mat V2 = (Mat_<double>(3, 1) << 0.0, 0.0, 1.0);
	Mat V1 = H * V2;
	int left_top = V1.at<double>(0, 0) / V1.at<double>(2, 0);
	if (left_top < 0)left_top = 0;

	//透视变换左下角(0,src.rows,1)
	V2 = (Mat_<double>(3, 1) << 0.0, image_left.rows, 1.0);
	V1 = H * V2;
	int left_bottom = V1.at<double>(0, 0) / V1.at<double>(2, 0);
	if (left_bottom < 0)left_bottom = 0;

	int start = MAX(left_top, left_bottom);//开始位置,即重叠区域的左边界  

	double Width = (image_left.cols - start);//重叠区域的宽度  

	//line(WarpImg, Point(start, 0), Point(start, WarpImg.rows), Scalar(0, 0, 255), 2);
	//line(WarpImg, Point(image_left.cols, 0), Point(image_left.cols, WarpImg.rows), Scalar(0, 0, 255), 2);
	
	//图像加权融合,通过改变alpha修改image_left与WarpImg像素权重,达到融合效果
	double alpha = 1.0;
	for (int i = 0; i < DstImg.rows; i++)
	{
		for (int j = start; j < image_left.cols; j++)
		{
			for (int c = 0; c < 3; c++)
			{
				//如果图像WarpImg像素为0,则完全拷贝image_left
				if (WarpImg.at<Vec3b>(i, j)[c] == 0)
				{
					alpha = 1.0;
				}
				else
				{
					double l = Width - (j - start); //重叠区域中某一像素点到拼接缝的距离
					alpha = l / Width;
				}
				DstImg.at<Vec3b>(i, j)[c] = image_left.at<Vec3b>(i, j)[c] * alpha + WarpImg.at<Vec3b>(i, j)[c] * (1.0 - alpha);
			}
		}
	}

	imshow("图像融合", DstImg);
	return true;
}

1.效果

OpenCV C++案例实战十二《图像全景拼接》_第10张图片
如图为最终融合效果,基本看不出拼接缝啦。

四、源码

#include
#include
#include
#include
using namespace std;
using namespace cv;
using namespace cv::xfeatures2d;

//1、使用特征检测算法找到两张图像中相似的点,计算变换矩阵
//2、将图像right透视变换后得到的图片与图像left拼接


bool Image_Stitching(Mat image_left, Mat image_right, Mat& H, Mat & WarpImg, Mat &DstImg, bool draw)
{
	//创建SURF特征检测器
	int Hessian = 800;
	Ptr<SURF>detector = SURF::create(Hessian);

	//进行图像特征检测、特征描述
	vector<KeyPoint>keypoint_left, keypoint_right;
	Mat descriptor_left, descriptor_right;
	detector->detectAndCompute(image_left, Mat(), keypoint_left, descriptor_left);
	detector->detectAndCompute(image_right, Mat(), keypoint_right, descriptor_right);

	//使用FLANN算法进行特征描述子的匹配
	FlannBasedMatcher matcher;
	vector<DMatch>matches;
	matcher.match(descriptor_left, descriptor_right, matches);

	double Max = 0.0;
	for (int i = 0; i < matches.size(); i++)
	{
		//float distance –>代表这一对匹配的特征点描述符(本质是向量)的欧氏距离,数值越小也就说明两个特征点越相像。
		double dis = matches[i].distance;
		if (dis > Max)
		{
			Max = dis;
		}
	}

	//筛选出匹配程度高的关键点
	vector<DMatch>goodmatches;
	vector<Point2f>goodkeypoint_left, goodkeypoint_right;
	for (int i = 0; i < matches.size(); i++)
	{
		double dis = matches[i].distance;
		if (dis < 0.15*Max)
		{
			/*
			以右图做透视变换
			左图->queryIdx:查询点索引(查询图像)
			右图->trainIdx:被查询点索引(目标图像)
			*/
			//注:对image_right图像做透视变换,故goodkeypoint_left对应queryIdx,goodkeypoint_right对应trainIdx
			//int queryIdx –>是测试图像的特征点描述符(descriptor)的下标,同时也是描述符对应特征点(keypoint)的下标。
			goodkeypoint_left.push_back(keypoint_left[matches[i].queryIdx].pt);
			//int trainIdx –> 是样本图像的特征点描述符的下标,同样也是相应的特征点的下标。
			goodkeypoint_right.push_back(keypoint_right[matches[i].trainIdx].pt);
			goodmatches.push_back(matches[i]);
		}
	}

	//绘制特征点
	if (draw)
	{
		Mat result;
		drawMatches(image_left, keypoint_left, image_right, keypoint_right, goodmatches, result);
		imshow("特征匹配", result);

		Mat temp_left = image_left.clone();
		for (int i = 0; i < goodkeypoint_left.size(); i++)
		{
			circle(temp_left, goodkeypoint_left[i], 3, Scalar(0, 255, 0), -1);
		}
		imshow("goodkeypoint_left", temp_left);

		Mat temp_right = image_right.clone();
		for (int i = 0; i < goodkeypoint_right.size(); i++)
		{
			circle(temp_right, goodkeypoint_right[i], 3, Scalar(0, 255, 0), -1);
		}
		imshow("goodkeypoint_right", temp_right);
	}

	//findHomography计算单应性矩阵至少需要4个点
	/*
	计算多个二维点对之间的最优单映射变换矩阵H(3x3),使用MSE或RANSAC方法,找到两平面之间的变换矩阵
	*/
	if (goodkeypoint_left.size() < 4 || goodkeypoint_right.size() < 4) return false;


	//获取图像right到图像left的投影映射矩阵,尺寸为3*3
	//注意顺序,srcPoints对应goodkeypoint_right,dstPoints对应goodkeypoint_left
	H = findHomography(goodkeypoint_right, goodkeypoint_left, RANSAC);

	//对image_right进行透视变换
	warpPerspective(image_right, WarpImg, H, Size(image_right.cols + image_left.cols, image_right.rows));
	imshow("透视变换", WarpImg);

	DstImg = WarpImg.clone();
	//将image_left拷贝到透视变换后的图片上,完成图像拼接
	image_left.copyTo(DstImg(Rect(0, 0, image_left.cols, image_left.rows)));
	imshow("图像全景拼接", DstImg);

	return true;
}


//图像融合,使得拼接自然
bool OptimizeSeam(Mat H, Mat& image_left, Mat& WarpImg, Mat& DstImg)
{
	//透视变换左上角(0,0,1)
	Mat V2 = (Mat_<double>(3, 1) << 0.0, 0.0, 1.0);
	Mat V1 = H * V2;
	int left_top = V1.at<double>(0, 0) / V1.at<double>(2, 0);
	if (left_top < 0)left_top = 0;

	//透视变换左下角(0,src.rows,1)
	V2 = (Mat_<double>(3, 1) << 0.0, image_left.rows, 1.0);
	V1 = H * V2;
	int left_bottom = V1.at<double>(0, 0) / V1.at<double>(2, 0);
	if (left_bottom < 0)left_bottom = 0;

	int start = MAX(left_top, left_bottom);//开始位置,即重叠区域的左边界  

	double Width = (image_left.cols - start);//重叠区域的宽度  

	//line(WarpImg, Point(start, 0), Point(start, WarpImg.rows), Scalar(0, 0, 255), 2);
	//line(WarpImg, Point(image_left.cols, 0), Point(image_left.cols, WarpImg.rows), Scalar(0, 0, 255), 2);
	
	//图像加权融合,通过改变alpha修改image_left与WarpImg像素权重,达到融合效果
	double alpha = 1.0;
	for (int i = 0; i < DstImg.rows; i++)
	{
		for (int j = start; j < image_left.cols; j++)
		{
			for (int c = 0; c < 3; c++)
			{
				//如果图像WarpImg像素为0,则完全拷贝image_left
				if (WarpImg.at<Vec3b>(i, j)[c] == 0)
				{
					alpha = 1.0;
				}
				else
				{
					double l = Width - (j - start); //重叠区域中某一像素点到拼接缝的距离
					alpha = l / Width;
				}
				DstImg.at<Vec3b>(i, j)[c] = image_left.at<Vec3b>(i, j)[c] * alpha + WarpImg.at<Vec3b>(i, j)[c] * (1.0 - alpha);
			}
		}
	}

	imshow("图像融合", DstImg);
	return true;
}

bool OpenCV_Stitching(Mat image_left, Mat image_right)
{
	//将待拼接图片放进容器里面
	vector<Mat>images;
	images.push_back(image_left);
	images.push_back(image_right);

	//创建Stitcher模型
	Ptr<Stitcher>stitcher = Stitcher::create();

	Mat result;
	Stitcher::Status status = stitcher->stitch(images, result);// 使用stitch函数进行拼接

	if (status != Stitcher::OK) return false;

	imshow("OpenCV图像全景拼接", result);

	return true;
}

int main()
{

	Mat image_left = imread("left.jpg");
	Mat image_right = imread("right.jpg");
	if (image_left.empty() || image_right.empty())
	{
		cout << "No Image!" << endl;
		system("pause");
		return -1;
	}

	Mat H, WarpImg, DstImg;
	if (Image_Stitching(image_left, image_right, H, WarpImg, DstImg, false))
	{
		if (!OptimizeSeam(H, image_left, WarpImg, DstImg))
		{
			cout << "Image fusion is not possible! " << endl;
		}
	}
	else
	{
		cout << "can not stitching the image!" << endl;
	}


	if (!OpenCV_Stitching(image_left, image_right))
	{
		cout << "can not stitching the image!" << endl;
	}

	waitKey(0);
	system("pause");
	return 0;
}



总结

本文使用OpenCV C++进行图像全景拼接,关键步骤有以下几点。
1、使用特征检测算子提取两幅图像的关键点,然后进行特征描述子匹配。
2、筛选出匹配程度高的关键点计算两幅图的单应性矩阵。
3、利用计算出来的单应性矩阵对其中一张图片进行透视变换。
4、将透视变换的图片与另一张图片进行拼接。
5、将拼接得到的结果进行融合。

你可能感兴趣的:(OpenCV,C++项目实战,opencv,计算机视觉,c++)