全景拼接是将多幅图像拼接成一幅大尺度图像
下面我们来解释一下上面这些步骤具体是做什么的
步骤一和步骤二过程是运用SIFT局部描述算子检测图像中的关键点和特征,SIFT特征是基于物体上的一些局部外观的兴趣点而与影像的大小和旋转无关。对于光线、噪声、些微视角改变的容忍度也相当高,所以用来检测要拼接图像的特征及关键点就很有优势。SIFT特征具体原理可参见博主之前写过的一篇文章SIFT算法原理,这里就不赘述了。
而接下来即步骤三是找到重叠的图片部分,连接所有图片之后就可以形成一个基本的全景图了。匹配图片最常用的方式是采用RANSAC(RANdom SAmple Consensus, 随机抽样一致),用此排除掉不符合大部分几何变换的匹配。之后利用这些匹配的点来**“估算单应矩阵” (Homography Estimation)**,也就是将其中一张图像通过关联性和另一张匹配。
RANSAC是一种迭代算法,用来从观测数据中估算出数学模型的参数,此基础上便可以分离内群与离群数据。简单来说就是一般来讲观测的数据里经常会出现很多噪音,比如说像SIFT匹配有时就会因为不同地方有类似的图案导致匹配错误。而RANSAC就是通过反复取样,也就是从整个观测数据中随机抽一些数据估算模型参数之后看和所有数据误差有多大,然后取误差最小视为最好以及分离内群与离群数据。RANSAC可以鲁棒的估计模型参数。
单应性矩阵可以由两幅图像(或者平面)中对应点对计算出来。每个对应点可以写出两个方程,分别对应与x和y坐标。因此,计算单应性矩阵H至少需要4对匹配点,过程如下:
那么就可以每次从所有的匹配点中选出4对,计算单应性矩阵,然后选出内点个数最多的作为最终的结果。计算距离方法如下:
估计单应矩阵 H后,我们需要将两个图像拼接在一起。这里我们采用透视变换,输入想要扭曲的图像,单应矩阵( H ),还有输出图像的形状。我们通过获取两个图像的宽度之和然后使用第二个图像的高度确定输出图像的导出形状。透视变换(Perspective Transformation)是将图片投影到一个新的视平面(Viewing Plane),也称作投影映射(Projective Mapping)。透视变换如下图:
步骤一:检测并提取图像的特征和关键点
def detectAndDescribe(self, image):
#将图像转换为灰度
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
#OpenCV 3.X的用法(使用高斯差分(DoG)关键点检测器和SIFT特征提取器)
if self.isv3:
# 检测并从图像中提取特征
descriptor = cv2.xfeatures2d.SIFT_create()
(kps, features) = descriptor.detectAndCompute(image, None)
# OpenCV 2.4.X的用法
else:
# 检测图像中的关键点
detector = cv2.FeatureDetector_create("SIFT")
kps = detector.detect(gray)
# 从图像中提取特征
extractor = cv2.DescriptorExtractor_create("SIFT")
(kps, features) = extractor.compute(gray, kps)
# 将KeyPoint对象的关键点转换为NumPy
#数组
kps = np.float32([kp.pt for kp in kps])
# 返回一个关键点和功能元组
return (kps, features)
步骤二:匹配两个图像之间的描述符
def matchKeypoints(self, kpsA, kpsB, featuresA, featuresB,
ratio, reprojThresh):
#计算原始匹配并初始化实际列表
#匹配
matcher = cv2.DescriptorMatcher_create("BruteForce")
rawMatches = matcher.knnMatch(featuresA, featuresB, 2)
matches = []
步骤三:使用RANSAC算法使用我们匹配的特征向量估计单应矩阵
# 循环原始匹配(从两个图像循环描述符,计算距离,并找到每对描述符的最小距离。)
# 利用David Lowe的比率 测试变量和RANSAC重新投影阈值。
for m in rawMatches:
#确保距离在每个距离之内
if len(m) == 2 and m[0].distance < m[1].distance * ratio:
matches.append((m[0].trainIdx, m[0].queryIdx))
#计算单应性需要至少4个匹配
if len(matches) > 4:
#构造两组点
ptsA = np.float32([kpsA[i] for (_, i) in matches])
ptsB = np.float32([kpsB[i] for (i, _) in matches])
#计算两组点之间的单应性
(H, status) = cv2.findHomography(ptsA, ptsB, cv2.RANSAC,
reprojThresh)
#返回匹配以及单应矩阵
#和每个匹配点的状态
return (matches, H, status)
#否则,不能计算单应性
return None
步骤四:拼接图像
(matches, H, status) = M
result = cv2.warpPerspective(imageA, H,
(imageA.shape[1] + imageB.shape[1], imageA.shape[0]))
result[0:imageB.shape[0], 0:imageB.shape[1]] = imageB
# 显示可视化关键点匹配
if showMatches:
vis = self.drawMatches(imageA, imageB, kpsA, kpsB, matches,
status)
# 可视化
return (result, vis)
# 返回拼接图像
return result
显示关键点匹配结果
def drawMatches(self, imageA, imageB, kpsA, kpsB, matches, status):
# 初始化输出可视化图像
(hA, wA) = imageA.shape[:2]
(hB, wB) = imageB.shape[:2]
vis = np.zeros((max(hA, hB), wA + wB, 3), dtype="uint8")
vis[0:hA, 0:wA] = imageA
vis[0:hB, wA:] = imageB
# 循环匹配
for ((trainIdx, queryIdx), s) in zip(matches, status):
# 仅在关键点成功时才处理匹配
# 匹配
if s == 1:
ptA = (int(kpsA[queryIdx][0]), int(kpsA[queryIdx][1]))
ptB = (int(kpsB[trainIdx][0]) + wA, int(kpsB[trainIdx][1]))
cv2.line(vis, ptA, ptB, (0, 255, 0), 1)
# 返回可视化结果
return vis
三、测试图片及结果
1、测试组一:
2、结果
关键点匹配
拼接图片
2、测试组二:
关键点匹配图:
拼接结果:
3、测试组三:
关键点匹配:
结果:
4、测试组四:
结果:
注:以上图片均拍自集美大学
由以上结果可看出拼接结果左边为原始图像,而右边是扭曲的图像,扭曲的图像相对模糊一些,而且拼接的缝还是蛮清楚的,这是还需要改进的地方,尽量融合图像,使图像更具整体性。
参考:
1、OpenCV panorama stitching
2、Panorama Stitching