大家好,欢迎来到IT知识分享网。
图像拼接
一、基本介绍
(image alignment)和图像融合是图像拼接的两个关键技术。图像拼接是计算机视觉中的重要分支,它是将两幅以上的具有部分重叠的图像进行无缝拼接从而得到较高分辨率或宽视角的图像。
二、图像拼接整体流程:
●根据给定图像/集,实现特征匹配
●通过匹配特征计算图像之间的变换结构利用图像变换结构,实现图像映射
●针对叠加后的图像,采用APAP之类的算法,对齐特征点
● 通过图割方法,自动选取拼接缝
●根据multi-band bleing策略实现融合
在执行RANSAC之后,我们只能在图像中看到正确的匹配,因为RANSAC找到了一个与大多数点相关的单应矩阵,并将不正确的匹配作为异常值丢弃
(1)单应矩阵(Homography):
有了两组相关点,接下来就需要建立两组点的转换关系,也就是图像变换关系。
单应性是两个空间之间的映射,常用于表示同一场景的两个图像之间的对应关系,可以匹配大部分相关的特征点,并且能实现图像投影,使一张图通过投影和另一张图实现大面积的重合。
设2个图像的匹配点分别是$X=[x,y]^T$,$X’=[x’,y’]^T$,则必须满足公式:X′=HX 且由于两向量共线,所以X′timesHX=0
其中,$H$ 为8参数的变换矩阵,可知四点确定一个H$$begin{pmatrix}x’ \y’\1 end{pmatrix} =begin{pmatrix}h{11} & h{12} & h_{13}\h{21} & h{22} & h_{23}\h{31} & h{32} & 1end{pmatrix}begin{pmatrix}x\y\1\end{pmatrix} $$
令 h=(h11:h12:h13:h21:h22:h23:h31:h32:h33)T 则有:Bh=0 N个点对给出2N个线性约束。undersethmin║Bh║2,║h║=1
(2)用RANSAC方法估算H:
1、首先检测两边图像的角点
2、在角点之间应用方差归一化相关,收集相关性足够高的对,形成一组候选匹配。
3、选择四个点,计算H
4、选择与单应性一致的配对。如果对于某些阈值:Dist(Hp、q) <ε,则点对(p, q)被认为与单应性H一致
5、重复34步,直到足够多的点对满足H
6、使用所有满足条件的点对,通过公式重新计算H
7、图像变形和融合
8、最后一步是将所有输入图像变形并融合到一个符合的输出图像中。基本上,我们可以简单地将所有输入的图像变形到一个平面上,这个平面名为复合全景平面。
(3)图像变形步骤
首先计算每个输入图像的变形图像坐标范围,得到输出图像大小,可以很容易地通过映射每个源图像的四个角并且计算坐标(x,y)的最小值和最大值确定输出图像的大小。最后,需要计算指定参考图像原点相对于输出全景图的偏移量的偏移量xoffset和偏移量yoffset。
下一步是使用上面所述的反向变形,将每个输入图像的像素映射到参考图像定义的平面上,分别执行点的正向变形和反向变形。
平滑过渡(transition smoothing)图像融合方法包括 羽化(feathering), 金字塔(pyramid), 梯度(gradient)
(4)图形融合
最后一步是在重叠区域融合像素颜色,以避免接缝。最简单的可用形式是使用羽化(feathering),它使用加权平均颜色值融合重叠的像素。我们通常使用alpha因子,通常称为alpha通道,它在中心像素处的值为1,在与边界像素线性递减后变为0。
当输出拼接图像中至少有两幅重叠图像时,我们将使用如下的alpha值来计算其中一个像素处的颜色
假设两个图像 $I1,I2$,在输出图像中重叠,每个像素点$(x,y)$在图像 I_i(x,y)=(alpha iR,alpha iG,alpha iB,alpha j,),其中(R,G,B)是像素的颜色值,我们将在缝合后的输出图像中计算(x, y)的像素值:
[(α1R,α1G,α1B,α1)(α2R,α2G,α2B,α2)]/(α1α2)
三、实验内容:
(一)图像拼接处理:
很多错误匹配的特征点的,这回导致配准后的结果出现两幅图片对不齐的结果,所以这里要做的是删除这些错误的匹配点。RANSAC(Random Sample Consensus),它是根据一组包含异常数据的样本数据集,计算出数据的数学模型参数,得到有效样本数据的算法。
代码实现:
#include <iostream> #include<opencv2/highgui/highgui.hpp> #include<opencv2/imgproc/imgproc.hpp> #include <opencv2/opencv.hpp> #include<opencv2/xfeatures2d.hpp> #include<opencv2/core/core.hpp> using namespace cv; using namespace std; using namespace cv::xfeatures2d;//只有加上这句命名空间,SiftFeatureDetector and SiftFeatureExtractor才可以使用 int main() { //Create SIFT class pointer Ptr<Feature2D> f2d = xfeatures2d::SIFT::create(); //SiftFeatureDetector siftDetector; //Loading images Mat img_1 = imread("101200.jpg"); Mat img_2 = imread("101201.jpg"); if (!img_1.data || !img_2.data) { cout << "Reading picture error!" << endl; return false; } //Detect the keypoints double t0 = getTickCount();//当前滴答数 vector<KeyPoint> keypoints_1, keypoints_2; f2d->detect(img_1, keypoints_1); f2d->detect(img_2, keypoints_2); cout << "The keypoints number of img1 is:" << keypoints_1.size() << endl; cout << "The keypoints number of img2 is:" << keypoints_2.size() << endl; //Calculate descriptors (feature vectors) Mat descriptors_1, descriptors_2; f2d->compute(img_1, keypoints_1, descriptors_1); f2d->compute(img_2, keypoints_2, descriptors_2); double freq = getTickFrequency(); double tt = ((double)getTickCount() - t0) / freq; cout << "Extract SIFT Time:" <<tt<<"ms"<< endl; //画关键点 Mat img_keypoints_1, img_keypoints_2; drawKeypoints(img_1,keypoints_1,img_keypoints_1,Scalar::all(-1),0); drawKeypoints(img_2, keypoints_2, img_keypoints_2, Scalar::all(-1), 0); //imshow("img_keypoints_1",img_keypoints_1); //imshow("img_keypoints_2",img_keypoints_2); //Matching descriptor vector using BFMatcher BFMatcher matcher; vector<DMatch> matches; matcher.match(descriptors_1, descriptors_2, matches); cout << "The number of match:" << matches.size()<<endl; //绘制匹配出的关键点 Mat img_matches; drawMatches(img_1, keypoints_1, img_2, keypoints_2, matches, img_matches); //imshow("Match image",img_matches); //计算匹配结果中距离最大和距离最小值 double min_dist = matches[0].distance, max_dist = matches[0].distance; for (int m = 0; m < matches.size(); m++) { if (matches[m].distance<min_dist) { min_dist = matches[m].distance; } if (matches[m].distance>max_dist) { max_dist = matches[m].distance; } } cout << "min dist=" << min_dist << endl; cout << "max dist=" << max_dist << endl; //筛选出较好的匹配点 vector<DMatch> goodMatches; for (int m = 0; m < matches.size(); m++) { if (matches[m].distance < 0.6*max_dist) { goodMatches.push_back(matches[m]); } } cout << "The number of good matches:" <<goodMatches.size()<< endl; //画出匹配结果 Mat img_out; //红色连接的是匹配的特征点数,绿色连接的是未匹配的特征点数 //matchColor – Color of matches (lines and connected keypoints). If matchColor==Scalar::all(-1) , the color is generated randomly. //singlePointColor – Color of single keypoints(circles), which means that keypoints do not have the matches.If singlePointColor == Scalar::all(-1), the color is generated randomly. //CV_RGB(0, 255, 0)存储顺序为R-G-B,表示绿色 drawMatches(img_1, keypoints_1, img_2, keypoints_2, goodMatches, img_out, Scalar::all(-1), CV_RGB(0, 0, 255), Mat(), 2); imshow("good Matches",img_out); //RANSAC匹配过程 vector<DMatch> m_Matches; m_Matches = goodMatches; int ptCount = goodMatches.size(); if (ptCount < 100) { cout << "Don't find enough match points" << endl; return 0; } //坐标转换为float类型 vector <KeyPoint> RAN_KP1, RAN_KP2; //size_t是标准C库中定义的,应为unsigned int,在64位系统中为long unsigned int,在C++中为了适应不同的平台,增加可移植性。 for (size_t i = 0; i < m_Matches.size(); i++) { RAN_KP1.push_back(keypoints_1[goodMatches[i].queryIdx]); RAN_KP2.push_back(keypoints_2[goodMatches[i].trainIdx]); //RAN_KP1是要存储img01中能与img02匹配的点 //goodMatches存储了这些匹配点对的img01和img02的索引值 } //坐标变换 vector <Point2f> p01, p02; for (size_t i = 0; i < m_Matches.size(); i++) { p01.push_back(RAN_KP1[i].pt); p02.push_back(RAN_KP2[i].pt); } /*vector <Point2f> img1_corners(4); img1_corners[0] = Point(0,0); img1_corners[1] = Point(img_1.cols,0); img1_corners[2] = Point(img_1.cols, img_1.rows); img1_corners[3] = Point(0, img_1.rows); vector <Point2f> img2_corners(4);*/ ////求转换矩阵 //Mat m_homography; //vector<uchar> m; //m_homography = findHomography(p01, p02, RANSAC);//寻找匹配图像 //求基础矩阵 Fundamental,3*3的基础矩阵 vector<uchar> RansacStatus; Mat Fundamental = findFundamentalMat(p01, p02, RansacStatus, FM_RANSAC); //重新定义关键点RR_KP和RR_matches来存储新的关键点和基础矩阵,通过RansacStatus来删除误匹配点 vector <KeyPoint> RR_KP1, RR_KP2; vector <DMatch> RR_matches; int index = 0; for (size_t i = 0; i < m_Matches.size(); i++) { if (RansacStatus[i] != 0) { RR_KP1.push_back(RAN_KP1[i]); RR_KP2.push_back(RAN_KP2[i]); m_Matches[i].queryIdx = index; m_Matches[i].trainIdx = index; RR_matches.push_back(m_Matches[i]); index++; } } cout << "RANSAC后匹配点数" <<RR_matches.size()<< endl; Mat img_RR_matches; drawMatches(img_1, RR_KP1, img_2, RR_KP2, RR_matches, img_RR_matches); imshow("After RANSAC",img_RR_matches); //等待任意按键按下 waitKey(0); }
结果显示:
很明显图中的大部分错误点被删除掉。接下来就可以对长焦进行矫正了,使用单映性矩阵计算,这里注意OpenCv有findFundamentalMat和findHomography两种方法,不要搞混,为了清楚区别基本矩阵和单映性矩阵的区别,请看:
单应矩阵、基本矩阵、本质矩阵
vector<cv::Point2f> Tele_point, Wide_point; for (int i = 0; i < InlierMatches.size(); i++) { Tele_point.push_back(key_points_1[InlierMatches[i].queryIdx].pt); Wide_point.push_back(key_points_2[InlierMatches[i].trainIdx].pt); } cv::Mat Homography = cv::findHomography(Tele_point, Wide_point, CV_RANSAC); //计算将p2投影到p1上的单映性矩阵 cv::Mat Registration; warpPerspective(Tele, Registration, Homography, cv::Size(Wide.cols, Wide.rows)); cv::imwrite("C:/Users/lxy/Desktop/re.jpg", Registration);
接下来将两幅图片融合在一起
cv::Mat Stitch(Wide.rows,Wide.cols,CV_8UC3); Wide.copyTo(Stitch(cv::Rect(0, 0, Wide.cols, Wide.rows))); cv::Rect Mask_center; Mask_center.y = Wide.rows / 2 - Wide.rows / (4);//根据广角和长焦的焦距参数决定的,lz使用的27mm和52mm的两个镜头 Mask_center.x = Wide.cols / 2 - Wide.cols / (4); Mask_center.width = Wide.cols / (2); Mask_center.height = Wide.rows / (2); Registration(Mask_center).copyTo(Stitch(Mask_center)); cv::imwrite("C:/Users/lxy/Desktop/Stitch.jpg", Stitch);
拼接结果:
(二)图像拼接产生重影:
原因分析:
1.拍摄因素:在处理图像的技术领域中,不管是对图像进行怎样的处理,都首先要通过拍摄设备拍取图像,作为后续图像处理的第一步,图像的拍摄方式,对图像拼接有着十分重要的影响。按照相机的拍摄情况,可以将拍摄图像的方式分为三类。
2.图像比例:拍摄的图片必须要包含超过一定比例的重叠区域,当重叠区域过小时,图像会由于匹配的特征点对不相机绕着相机的垂直轴旋转,然后每旋转一定的角度拍摄一张图片。 此外拍摄的图片必须要包含超过一定比例的重叠区域,当重叠区域过小时,图像会由于匹配的特征点对不足,而导致拼接失败。当重叠区域过大时,则需要多张图像进行拼接才能获取到更宽视野的图像,影响拼接的实时性。一般情况下,重叠区域占整幅图像的40%- -50%, 图像能够找到足够配准的特征点对。
3.水平移动相机获得。水平移动相机拍摄的情况是在确定要拍摄的平面后,保持相机的姿态,平行于该平面进行移动。通过这种拍摄方式拍摄出的图片都位于同一平面上,拍摄时相机距离拍摄的目标越远,则目标越小。由于采用水平移动相机的方式对拍摄的要求非常苛刻,因此在现实应用中很少有采用这种拍摄方进行拍摄。
小结:
理想情况下,无论何种拍摄方式,相邻图像的重叠区域都应该具有相同的特征,但是实际操作过程中,一方面由于拍摄图像的存在一定的时间差,且相机的轨迹很难控制,使得图像间光照强度可能存在差异,另一方面,拍摄图像过程中相机存在视差,图像重叠区域中目标并不能完全重合,此外,若拍摄场景中存在运动目标,由于拍摄时存在时间差,图像间重叠区域中必然存在差异。在图像拼接过程中,不管是何种原因导致了的图像重叠区域存在不同,这必然使得最终的拼接结果存在重影现象。
(三)全景图像拼接:
数据集:
代码实现:
from pylab import * from numpy import * from PIL import Image #If you have PCV installed, these imports should work from PCV.geometry import homography, warp import sift from PCV.tools.imtools import get_imlist """ This is the panorama example from section 3.3. """ #set paths to data folder #featname = ['C://Users//Garfield//Desktop//towelmatch//' + str(i + 1) + 'out_sift_1.txt' for i in range(5)] #imname = ['C://Users//Garfield//Desktop//towelmatch//' + str(i + 1) + '.jpg' for i in range(5)] imname = ['C://Users//Desktop//towelmatch//pinjie//' + str(i + 1) + '.jpg' for i in range(5)] download_path = "C://Users//Desktop//towelmatch//pinjie//" # set this to the path where you downloaded the panoramio images imlist = get_imlist(download_path) l = {} d = {} featname = ['out_sift_1.txt' for imna in imlist] for i, imna in enumerate(imlist): sift.process_image(imna, featname[i]) l[i], d[i] = sift.read_features_from_file(featname[i]) #extract features and match #l = {} #d matches = {} for i in range(4): matches[i] = sift.match(d[i + 1], d[i]) #visualize the matches (Figure 3-11 in the book) #sift匹配可视化 for i in range(4): im1 = array(Image.open(imname[i])) im2 = array(Image.open(imname[i + 1])) figure() sift.plot_matches(im2, im1, l[i + 1], l[i], matches[i], show_below=True) #function to convert the matches to hom. points #将匹配转换成齐次坐标点的函数 def convert_points(j): ndx = matches[j].nonzero()[0] fp = homography.make_homog(l[j + 1][ndx, :2].T) ndx2 = [int(matches[j][i]) for i in ndx] tp = homography.make_homog(l[j][ndx2, :2].T) # switch x and y - TODO this should move elsewhere fp = vstack([fp[1], fp[0], fp[2]]) tp = vstack([tp[1], tp[0], tp[2]]) return fp, tp #estimate the homographies #估计单应性矩阵 model = homography.RansacModel() fp, tp = convert_points(1) H_12 = homography.H_from_ransac(fp, tp, model)[0] # im 1 to 2 # im1 到 im2 的单应性矩阵 fp, tp = convert_points(0) H_01 = homography.H_from_ransac(fp, tp, model)[0] # = {} tp, fp = convert_points(2) # NB: reverse order H_32 = homography.H_from_ransac(fp, tp, model)[0] # im 3 to 2 tp, fp = convert_points(3) # NB: reverse order H_43 = homography.H_from_ransac(fp, tp, model)[0] # im 4 to 3 #warp the images #扭曲图像 delta = 2000 # for padding and translation 用于填充和平移 im1 = array(Image.open(imname[1]), "uint8") im2 = array(Image.open(imname[2]), "uint8") im_12 = warp.panorama(H_12, im1, im2, delta, delta) im1 = array(Image.open(imname[0]), "f") im_02 = warp.panorama(dot(H_12, H_01), im1, im_12, delta, delta) im1 = array(Image.open(imname[3]), "f") im_32 = warp.panorama(H_32, im1, im_02, delta, delta) im1 = array(Image.open(imname[4]), "f") im_42 = warp.panorama(dot(H_32, H_43), im1, im_32, delta, 2 * delta) imsave('C://Users//Desktop//towelmatch//result.jpg', array(im_42, "uint8")) figure() imshow(array(im_42, "uint8")) axis('off') show()
运行结果:
室内:
室外:
全景图像拼接选取的五张图片由于拍摄角度的转换存在光线问题,导致拼接不完整,不能选取比较灰暗的图像进行。选取室外五张屋顶图片进行运行,比最开始的五张效果好了一些,环境问题建筑物少,没有太多角特征点所以拼接效果不是很完美。
总结:
图像拼接是把来自多个不同视角相机的图像变换到同一视角下,无缝拼接成一张宽视野图像(比如360度全景图,甚至360度*180度的球面全景)。需要注意的是,由于相机各自的指向角度不一样,因此两图片中来自同样场景的部分并不能够通过平移图像而完全重合。事实上什么情况下不同位置、视角的相机可以变换到同一视角下拼接起来也是久为人知的,在两种情况下图像可拼接,一是各相机几何中心重合;二是各相机位置任意,但场景是一个平面。一种特殊情况,场景为远景时,可以近似的等价于一个平面场景,从而也是可拼的。在满足上述两种情况之一时,存在一个单应性变换,能够将一个相机的图像变换到另一个相机的视角下从而可以进行拼接。实际应用中为了创建出完美的全景图,有很多的问题需要考虑。最典型的问题有两个,一个是如何解决不同照片中曝光不一致的问题;一个是如何在拼接缝处完美平滑的融合两张图像的问题。第一个由曝光补偿算法来解决,大体思路是估计两张图间的曝光差异,然后进行补偿。在本次实验过程中,不停选取图片进行处理,对图像特征的要求较高才能达到很好的拼接效果。
参考链接:https://blog.csdn.net/yangpan011/article/details/81209724
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/33084.html