基于OpenCV全景拼接(Python)

翻译自https://www.pyimagesearch.com 基于OpenCV(Python)的图片拼接和全景图构建。“缝合”两张有重叠区域的图来创建一张全景图。构建全景图利用到了计算机视觉和图像处理技术有:关键点检测、局部不变特征、关键点匹配、RANSAC(Random Sample Consensus,随机采样一致性)和透视变形。因为在处理关键点检测和局部不变性在OpenCV 2.4.X和OpenCV 3.X中有很大的不同,比如SIFT和SURF。这里将给出兼容两个版本的代码。在之后的博客会解决多张图片的拼接,而不仅仅只是针对两张图片。

OpenCV全景拼接

全景拼接算法有四部分组成

  • Step1:从输入的两张图片里检测关键点、提取局部不变特征。
  • Step2:匹配的两幅图像之间的特征
  • Step3:使用RANSAC算法利用匹配特征向量估计单应矩阵(homography matrix)。
  • Step4:利用Step3得到的单应矩阵应用扭曲变换。

将所有的步骤都封装在panorama.py,定义一个Stitcher类来构建全图。Stitcher类将会依赖Python的包imutils,安装方法:

pip install imutils

关于panorama.py

# import the necessary packages
import numpy as np
import imutils
import cv2

class Stitcher:
    def __init__(self):
        # determine if we are using OpenCV v3.X
        self.isv3 = imutils.is_cv3()

    def stitch(self, images, ratio=0.75, reprojThresh=4.0,
        showMatches=False):
        # unpack the images, then detect keypoints and extract
        # local invariant descriptors from them
        (imageB, imageA) = images
        (kpsA, featuresA) = self.detectAndDescribe(imageA)
        (kpsB, featuresB) = self.detectAndDescribe(imageB)

        # match features between the two images
        M = self.matchKeypoints(kpsA, kpsB,
            featuresA, featuresB, ratio, reprojThresh)

        # if the match is None, then there aren't enough matched
        # keypoints to create a panorama
        if M is None:
            return None

        # otherwise, apply a perspective warp to stitch the images
        # together
        (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

        # check to see if the keypoint matches should be visualized
        if showMatches:
            vis = self.drawMatches(imageA, imageB, kpsA, kpsB, matches,
                status)

            # return a tuple of the stitched image and the
            # visualization
            return (result, vis)

        # return the stitched image
        return result

    def detectAndDescribe(self, image):
        # convert the image to grayscale
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

        # check to see if we are using OpenCV 3.X
        if self.isv3:
            # detect and extract features from the image
            descriptor = cv2.xfeatures2d.SIFT_create()
            (kps, features) = descriptor.detectAndCompute(image, None)

        # otherwise, we are using OpenCV 2.4.X
        else:
            # detect keypoints in the image
            detector = cv2.FeatureDetector_create("SIFT")
            kps = detector.detect(gray)

            # extract features from the image
            extractor = cv2.DescriptorExtractor_create("SIFT")
            (kps, features) = extractor.compute(gray, kps)

        # convert the keypoints from KeyPoint objects to NumPy
        # arrays
        kps = np.float32([kp.pt for kp in kps])

        # return a tuple of keypoints and features
        return (kps, features)

    def matchKeypoints(self, kpsA, kpsB, featuresA, featuresB,
        ratio, reprojThresh):
        # compute the raw matches and initialize the list of actual
        # matches
        matcher = cv2.DescriptorMatcher_create("BruteForce")
        rawMatches = matcher.knnMatch(featuresA, featuresB, 2)
        matches = []

        # loop over the raw matches
        for m in rawMatches:
            # ensure the distance is within a certain ratio of each
            # other (i.e. Lowe's ratio test)
            if len(m) == 2 and m[0].distance < m[1].distance * ratio:
                matches.append((m[0].trainIdx, m[0].queryIdx))

        # computing a homography requires at least 4 matches
        if len(matches) > 4:
            # construct the two sets of points
            ptsA = np.float32([kpsA[i] for (_, i) in matches])
            ptsB = np.float32([kpsB[i] for (i, _) in matches])

            # compute the homography between the two sets of points
            (H, status) = cv2.findHomography(ptsA, ptsB, cv2.RANSAC,
                reprojThresh)

            # return the matches along with the homograpy matrix
            # and status of each matched point
            return (matches, H, status)

        # otherwise, no homograpy could be computed
        return None

    def drawMatches(self, imageA, imageB, kpsA, kpsB, matches, status):
        # initialize the output visualization image
        (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

        # loop over the matches
        for ((trainIdx, queryIdx), s) in zip(matches, status):
            # only process the match if the keypoint was successfully
            # matched
            if s == 1:
                # draw the match
                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 the visualization
        return vis

2-4行是导入需要的包。用NumPy来进行矩阵操作。imutils是一套OpenCV的工具包。最后把cv2导入OpenCV。 在第6行定义了Stitcher类,可以检测我们是否使用了OpenCV3。由于在opencv 2.4和OpenCV 3处理关键点检测和局部不变特征的有明显的差异,OpenCV的版本对我们的使用是很重要的。 接下来是定义方法stitch,stitch方法只需要一个单一的参数:images。这是传入图片的列表,后面是要缝合在一起形成全景图。 还可以提供ratio ,用于特征匹配时David Lowe比率测试,reprojthresh 是RANSAC算法中最大像素“回旋的余地”,最后的showMatches,是一个布尔类型的值,用于表明是否应可以可视化关键点匹配。 第15行是所有图片的列表,这一次我们只包含两张图片的情况。照片列表的顺序很重要,我们希望能够提供的图像是从左到右的顺序。如果提供的不是这样的顺序,程序仍然可以跑,但是输出全景是不正确的。 我们拆包图片列表后,调用detectAndDescribe方法(16-17行)这个方法可以检测到两张图片里关键点、提取局部不变特征。 有了关键点和特征,我们可以用matchKeypoints方法(20-21行)来匹配两张图片里的特征。这个方法后面会做解释。 如果返回匹配的M为None,就是因为现有的关键点不足以匹配生成全景图。返回函数为25-26行。

接下来就是准备应用透视变换: 假设M不返回None,我们在第30行拆包这个元组,是一个包含关键点匹配、从RANSAC算法中得到的单应矩阵H以及最后的status,用来表明那些已经成功匹配的关键点。 有了单应矩阵H后,就可将两张图片“缝合起来”。首选调用cv2.warpPerspective,需要三个参数:想要“缝合”上来的照片(本程序里的右边的图片);还有3*3的转换矩阵H;最后就是塑造出要输出的照片。我们得到输出图像的宽是两图片之和,高即为第二张图像的高度。 第36行检查看是否应该将关键点匹配,如果是的话就调用drawMatches函数,然后返回一个包含全图和可视化的图的元组。(37-42行) 这样,就简单的返回一个拼接的图片。(第45行) stitch的方法已经被定义,接下来介绍下那些辅助方法。首先从detectAndDescribe方法开始。顾名思义,detectAndDescribe方法用来接收照片,检测关键点和提取局部不变特征。在我们的实现中用到了高斯差分(Difference of Gaussian (DoG))关键点检测,和SIFT特征提取。在第52行我们检测是否用了OpenCV 3.X,如果是,就用cv2.xfeatures2d.SIFT_create方法来实现DoG关键点检测和SIFT特征提取。detectAndCompute方法用来处理提取关键点和特征。(第54和55行) 有一点必须要注意的是编译OpenCV3.X的opencv_contrib是可用的。如果没有,必然会出错:AttributeError: ‘module’ object has no attribute ‘xfeatures2d’。如果出现这种问题,请正确安装OpenCV和opencv_contrib,这样就能支持后面一系列的操作。 第58-65行是我们用OpenCV2.4的情况。cv2.FeatureDetector_create方法来实现关键点的检测(DoG)。detect方法返回一系列的关键点。 到这里,我们需要用SIFT关键字来初始化cv2.DescriptorExtractor_create,设置SIFT特征提取。调用extractor的compute方法返回一组关键点周围量化检测的特征向量。 最后,关键点从KeyPoint对象转换为NumPy数列后返回给调用函数。(第69行) 下一步,我们看看matchKeypoints方法。 matchKeypoints方法需要四个参数,第一张图片的关键点和特征向量,第二张图片的关键点特征向量。David Lowe’s ratio测试变量和RANSAC重投影门限也应该被提供。 匹配的特征实际上是一个相当简单的过程。我们循环每张图片的描述子,计算距离,最后找到每对描述子的最小距离。因为这是计算机视觉中的一个非常普遍的做法,OpenCV已经内置了一个cv2.DescriptorMatcher_create方法,用来匹配特征。BruteForce的值表示我们能够更详尽计算两张图片直接的欧式距离,以此来寻找每对描述子的最短距离。79行的knnMatch方法是在K=2的两个特征向量的k-NN匹配(k-nearest neighbors algorithm,K近邻算法),表明每个匹配的前两名作为特征向量返回。 之所以我们要的是匹配的前两个而不是只有第一个,是因为我们需要用David Lowe’s ratio来测试假匹配然后做修剪。 之后,用第79行的rawMatches来计算每对描述子,但是这些描述子可能是错误的,也就是这是图片不是真正的匹配。去修剪这些有误的匹配,我们可以运用 Lowe’s ratio测试特别的来循环rawMatches,这是用来确定高质量的特征匹配。正常的Lowe’s ratio 值在[0.7,0.8]. 我们用Lowe’s ratio 测试得到matche的值后,我们就可以计算这两串关键点之间的单应性。 计算两串关键点的单应性需要至少四个匹配。为了获得更为可信的单应性,我们至少需要超过四个匹配点。 最后,Stitcher里的最后一个方法drawMatches–用来将两张图片关键点的联系可视化。 这种方法需要我们通过两张原始图像来对每个图像的关键点进行设置,应用Lowe’s ratio 试验后的初始匹配,和最后由单应计算提供的状态列表。 运用这些变量,我们可以通过将两张图片“里面”的关键点N和关键点M画直线来可视化。

驱动脚本stitch.py

到此为止已经定义好了Stitcher类。接着么建立一个stitch.py的驱动脚本。

# import the necessary packages
from pyimagesearch.panorama import Stitcher
import argparse
import imutils
import cv2
 
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-f", "--first", required=True,
    help="path to the first image")
ap.add_argument("-s", "--second", required=True,
    help="path to the second image")
args = vars(ap.parse_args())
# load the two images and resize them to have a width of 400 pixels
# (for faster processing)
imageA = cv2.imread(args["first"])
imageB = cv2.imread(args["second"])
imageA = imutils.resize(imageA, width=400)
imageB = imutils.resize(imageB, width=400)
 
# stitch the images together to create a panorama
stitcher = Stitcher()
(result, vis) = stitcher.stitch([imageA, imageB], showMatches=True)
 
# show the images
cv2.imshow("Image A", imageA)
cv2.imshow("Image B", imageB)
cv2.imshow("Keypoint Matches", vis)
cv2.imshow("Result", result)
cv2.waitKey(0)

第2-5行我们导入需要的包。注意我们已经将panorama.py和Stitcher类整合到pyimagesearch模块里,这样只是为了保持我们代码的整洁。

第8-14行是解析参数的命令行:–first是我们全景图的第一张图片(左边的图),–second是全景图的第二张图片(右边的图)。 注意:这些图像的路径一定是从左到右的顺序! stitch.py剩下的程序是驱动脚本,会简单处理载入图片,调整大小(适合屏幕大小),构建全图。

图片载入调整大小后,我们需要初始化类Stitcher(第23行)。之后调用stitch方法,通过两张图片的可视化可以很清晰的观察两张图片之间的关键点匹配。

最后,第27-31行展示我们输出的照片。

全景拼接测试

python stitch.py --first images/bryce_left_01.png --second images/bryce_right_01.png
python stitch.py --first images/IMG_4674.JPG --second images/IMG_4678.JPG

注意:这两张图片为我用iPhone拍摄,所以是自动对焦。虽然自动对焦在两张图片之间有轻微的不同,但是两张图片“缝合”的地方还是有裂缝,图片拼接和全景图构建最好用同一焦距去拍摄。

附录

SIFT算法详解 随机抽样一致性算法(RANSAC) RANSAC算法详解 (updating)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏DHUtoBUAA

编程求取直线一般式表达式,两直线交点

背景介绍   最近在水面无人艇(USV)模拟仿真中,用到了一些点和线的关系求解,本文主要讲述一下两点确认直线,点到直线距离,两条直线的交点等问题的解决方法,并给...

52170
来自专栏MixLab科技+设计实验室

用谷歌新开源的deeplearnJS预测互补颜色

本文翻译自deeplearnJS的示例教程,并结合了我在学习过程中的理解。 deeplearnJS简介: deeplearn.js是用于机器学习的开源WebGL...

33480
来自专栏数据科学学习手札

(数据科学学习手札25)sklearn中的特征选择相关功能

一、简介   在现实的机器学习任务中,自变量往往数量众多,且类型可能由连续型(continuou)和离散型(discrete)混杂组成,因此出于节约计算成本、精...

51790
来自专栏大数据文摘

机器学习中的线性代数:关于常用操作的新手指南

20920
来自专栏和蔼的张星的图像处理专栏

图像旋转即c++实现

主要还是考虑面试的时候会不会用到,刚才好好看了下旋转的这个思路,其实和图像缩放的思路差不多的,主要的问题是要找到坐标的映射方式。 因为还是包含了一部分的公式,...

33540
来自专栏华章科技

与数据挖掘有关或有帮助的R包和函数的集合

rpart,party,randomForest,rpartOrdinal,tree,marginTree,

9330
来自专栏Python数据科学

Seaborn从零开始学习教程(四)

数据集中的数据类型有很多种,除了连续的特征变量之外,最常见的就是类目型的数据类型了,常见的比如人的性别,学历,爱好等。这些数据类型都不能用连续的变量来表示,而是...

20520
来自专栏数值分析与有限元编程

三角形面积坐标

(一)三角形面积坐标的定义 三角形中任一点P与其三个角点相连形成三个子三角形,如图1所示 ? 需要注意的是,这里引用的面积坐标,只限于用在一个三角形单元之内,在...

43650
来自专栏IT派

一文带你入门Tensorflow

导语:此文编译自FCC(FreeCodeCamp),作者为Déborah Mesquita,该作者利用神经网络和TensorFlow进行了机器文本分类,并提出了...

45390
来自专栏数据结构与算法

圆的反演变换

挺神奇的东西,网上没有多少资料,我也不是太懂,代码什么的都没写过,那就抄一下百度百科吧

15920

扫码关注云+社区

领取腾讯云代金券