使用OpenCV进行图像全景拼接

图像拼接介绍

图像拼接是计算机视觉中最成功的应用之一。如今,很难找到不包含此功能的手机或图像处理API。在本文中,我们将讨论如何使用Python和OpenCV进行图像拼接。也就是,给定两张共享某些公共区域的图像,目标是“缝合”它们并创建一个全景图像场景。当然也可以是给定多张图像,但是总会转换成两张共享某些公共区域图像拼接的问题,因此本文以最简单的形式进行介绍。下图是一张拼接图的效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ErY1mKQL-1601886061420)(en-resource://database/923:1)]

图像拼接包含的技术内容:

  • 关键点检测
  • 局部不变描述符(SIFT,SURF等)
  • 特征匹配
  • 使用RANSAC进行单应性估计
  • 透视变换

本文需要拼接的两张图像如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xy4rliUX-1601886061423)(en-resource://database/925:1)]

特征检测与提取

给定上述一对图像,我们希望将它们缝合以创建全景场景。重要的是要注意,两个图像都需要有一些公共区域。当然,上面给出的两张图像是比较理想的,有时候两个图像虽然具有公共区域,但是同样还可能存在缩放、旋转、来自不同相机等因素的影响。但是无论哪种情况我们都需要检测图像中的特征点。

特征点检测

最初比较简单的方法是使用诸如Harris Corners之类的算法来提取关键点。然后,我们可以尝试基于某种相似性度量(比如欧几里得距离)来匹配相应的关键点。众所周知,角点具有角点不变的特性。这就说明检测到的角点,即使旋转图片该角点依然存在。

但是,如果我们旋转图像后进行缩放就会遇到问题,由于角点的大小不变,放大图片后,先前检测到的角就会变成一条线。

因此,需要旋转和缩放都不变的特征,那就需要用到更高级的方法。(比如SIFT, SURF和ORB)

关键点和描述符

诸如SIFT和SURF之类的方法试图解决角点检测算法的局限性。通常,角点检测器算法使用固定大小的内核来检测图像上的感兴趣区域(角)。不难看出,当我们缩放图像时,该内核可能变得太小或太大。为了解决此限制,诸如SIFT之类的方法使用高斯差分(DoD)。想法是将DoD应用于同一图像的不同缩放版本。它还使用相邻像素信息来查找和完善关键点和相应的描述符。

首先,我们需要加载2个图像,一个查询图像和一个训练图像。最初,我们首先从两者中提取关键点和描述符。通过使用OpenCV detectAndCompute()函数,我们可以一步完成它。请注意,为了使用detectAndCompute(),我们需要一个关键点检测器和描述符对象的实例。它可以是ORB,SIFT或SURF等。此外,在将图像输入给detectAndCompute()之前,我们将其转换为灰度。

我们为两个图像都设置了一组关键点和描述符。如果我们使用SIFT作为特征提取器,它将为每个关键点返回一个128维特征向量。如果选择SURF我们将获得64维特征向量。下图显示了使用SIFT和SURF得到的结果。
(上:SIFT检测关键点和描述符;下:SURF检测关键点和描述符)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kqRzCo72-1601886061425)(en-resource://database/927:0)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tHsIMNkK-1601886061428)(en-resource://database/929:0)]

特征匹配

如我们所见,两个图像都有大量特征点。现在,我们想比较两组特征,并尽可能显示更多相似性的特征点对 。使用 OpenCV , 特征点匹配需要Matcher对象。这里我们探索到两种方式:暴力匹配器(BruteForce)和KNN(k最近邻)。
代码如下:

def matchKeyPoints(self, kp1, kp2, des1, des2, ratio, reprojThresh):    
    print('C')    
    # 初始化BF,因为使用的是SIFT ,所以使用默认参数    
    matcher = cv.DescriptorMatcher_create('BruteForce')    
    # bf = cv.BFMatcher()    
    # matches = bf.knnMatch(des1, des2, k=2)    
    matches = matcher.knnMatch(des1, des2, 2)   
    
     # 获取理想匹配    
     good = []    
     for m in matches:        
        if len(m) == 2 and m[0].distance < ratio * m[1].distance: 
            good.append((m[0].trainIdx, m[0].queryIdx))
            
    print(len(good))    
    # 最少要有四个点才能做透视变换    
    if len(good) > 4:        
        # 获取关键点的坐标        
        # src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)        
        # dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)        
        src_pts = np.float32([kp1[i] for (_, i) in good])        
        dst_pts = np.float32([kp2[i] for (i, _) in good])        

        # 通过两个图像的关键点计算变换矩阵        
        (M, mask) = cv.findHomography(src_pts, dst_pts, cv.RANSAC, reprojThresh)       
        # 返回最佳匹配点、变换矩阵和掩模        
        return good, M, mask    
        
    # 如果不满足最少四个 就返回None    
    return None
比率测试

特征匹配结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1uEgnL82-1601886061430)(en-resource://database/931:0)]

最后实现的效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-agAIovEs-1601886061431)(en-resource://database/933:0)]

最后附上程序的完整代码:

# -*- encoding: utf-8 -*-
"""
@Date : 2020/10/4 18:57
@Author : LGD
@File :test.py
@IDE :PyCharm
"""
import numpy as np
import cv2 as cv
import imutils


class Stitcher:

    def stitch(self, imgs, ratio=0.75, reprojThresh=4.0, showMatches=False):
        print('A')
        (img2, img1) = imgs
        # 获取关键点和描述符
        (kp1, des1) = self.detectAndDescribe(img1)
        (kp2, des2) = self.detectAndDescribe(img2)
        print(len(kp1), len(des1))
        print(len(kp2), len(des2))
        R = self.matchKeyPoints(kp1, kp2, des1, des2, ratio, reprojThresh)

        # 如果没有足够的最佳匹配点,M为None
        if R is None:
            return None
        (good, M, mask) = R
        print(M)
        # 对img1透视变换,M是ROI区域矩阵, 变换后的大小是(img1.w+img2.w, img1.h)
        result = cv.warpPerspective(img1, M, (img1.shape[1] + img2.shape[1], img1.shape[0]))
        # 将img2的值赋给结果图像
        result[0:img2.shape[0], 0:img2.shape[1]] = img2

        # 是否需要显示ROI区域
        if showMatches:
            vis = self.drawMatches(img1, img2, kp1, kp2, good, mask)
            return result, vis

        return result

    def detectAndDescribe(self, img):
        print('B')
        gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

        # 使用SIFT检测特征
        sift = cv.SIFT_create()
        (kps, des) = sift.detectAndCompute(img, None)

        kps = np.float32([kp.pt for kp in kps])
        # 返回关键点和描述符
        return kps, des

    def matchKeyPoints(self, kp1, kp2, des1, des2, ratio, reprojThresh):
        print('C')
        # 初始化BF,因为使用的是SIFT ,所以使用默认参数
        matcher = cv.DescriptorMatcher_create('BruteForce')
        # bf = cv.BFMatcher()
        # matches = bf.knnMatch(des1, des2, k=2)
        matches = matcher.knnMatch(des1, des2, 2)  # ***********************************

        # 获取理想匹配
        good = []
        for m in matches:
            if len(m) == 2 and m[0].distance < ratio * m[1].distance:
                good.append((m[0].trainIdx, m[0].queryIdx))

        print(len(good))
        # 最少要有四个点才能做透视变换
        if len(good) > 4:
            # 获取关键点的坐标
            # src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
            # dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
            src_pts = np.float32([kp1[i] for (_, i) in good])
            dst_pts = np.float32([kp2[i] for (i, _) in good])

            # 通过两个图像的关键点计算变换矩阵
            (M, mask) = cv.findHomography(src_pts, dst_pts, cv.RANSAC, reprojThresh)

            # 返回最佳匹配点、变换矩阵和掩模
            return good, M, mask
        # 如果不满足最少四个 就返回None
        return None

    def drawMatches(self, img1, img2, kp1, kp2, metches, mask):
        print('D')
        (hA, wA) = img1.shape[:2]
        (hB, wB) = img2.shape[:2]
        vis = np.zeros((max(hA, hB), wA + wB, 3), dtype='uint8')
        vis[0:hA, 0:wA] = img1
        vis[0:hB, wA:] = img2
        for ((trainIdx, queryIdx), s) in zip(metches, mask):
            if s == 1:
                ptA = (int(kp1[queryIdx][0]), int(kp1[queryIdx][1]))
                ptB = (int(kp2[trainIdx][0]) + wA, int(kp2[trainIdx][1]))
                cv.line(vis, ptA, ptB, (0, 255, 0), 1)

        return vis


def show():
    img1 = cv.imread('img1.jpg')
    img2 = cv.imread('img2.jpg')
    img1 = imutils.resize(img1, width=400)
    img2 = imutils.resize(img2, width=400)

    stitched = Stitcher()
    # (result, vis) = stitched.stitch([img1, img2])
    (result, vis) = stitched.stitch([img1, img2], showMatches=True)

    cv.imshow('image A', img1)
    cv.imshow('image B', img2)
    cv.imshow('keyPoint Matches', vis)
    cv.imshow('Result', result)

    cv.waitKey(0)
    cv.destroyAllWindows()


if __name__ == '__main__':
    show()

完整代码地址:https://github.com/YouthJourney/Computer-Vision-OpenCV/tree/master/Image_Panorama_Stitching

  • 3
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hong_Youth

您的鼓励将是我创作的动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值