使用Python和OpenCV进行图像拼接和全景图构建

使用Python和OpenCV进行图像拼接和全景图构建


这篇博客将介绍如何使用OpenCV执行图像拼接和全景构建。即给定两个图像,将它们“缝合”在一起以创建一个简单的全景图。
并提供了用于OpenCV 2.4和OpenCV 3图像拼接的源代码。

1. 效果图

某一次团建去凤凰山玩的时候所拍的图,两个输入图像原始图A与原始图B,如下:
在这里插入图片描述两个图像之间匹配的关键点可视化,如图
在这里插入图片描述叠合后全景图:
在这里插入图片描述
上述效果不是很好,**可以看到在俩图缝合图像的中心有一条明显的“缝”。这是因为在打开自动对焦的情况下使用相机拍摄了许多照片,因此每次拍摄的焦点略有不同。**接缝是由于拍摄照片时传感器属性的变化而引起的。

叠合全景图效果图2:
在这里插入图片描述
如上图所示,当两个输入图像之间有大量重叠时,全景图的主要添加部分是在拼接图像的右侧,因此会有右边大量的黑色空白区域。

2. 原理及步骤

构建图像全景图,将利用计算机视觉和图像处理技术进行图像拼接。

算法包括四个步骤:

  1. 检测关键点并提取局部不变描述符;
  2. 图像之间进行特征匹配;
  3. 使用RANSAC算法通过匹配的特征向量来估计单应矩阵。
  4. 使用单应性矩阵应用翘曲变换。
  • 调用 detectAndDescribe,该方法仅检测关键点并从两个图像中提取局部不变描述符(即SIFT);
  • 给定俩个图像的关键点,调用 matchKeypoints 以匹配两个图像中的特征;
  • matcher.knnMatch(featuresA, featuresB, 2),使用k = 2在两个特征向量集之间执行KNN匹配(指示返回每个特征向量的前两个匹配项)。希望获得前两场匹配而不是仅仅获得第一场匹配的原因是,我们需要将David Lowe的比率测试应用于假阳性匹配修剪。
  • 调用 warpPerspective 应用翘曲变换以叠合图像;

当对每张照片使用相同的焦点时,图像拼接和全景构造效果最佳。

使用互相匹配的关键点,可以应用透视变换获得最终的全景图。

后续拓展将介绍如何构建全景图以及如何对多于两张图像进行图像拼接。

3. 源码

# USAGE
# python stitch.py --first images/left.png --second images/right.png

# 导入必要的类
from panorama import Stitcher
import argparse

import cv2
import imutils

# 构建命令行参数及解析
# --first 全景图的第一张图像
# --second 全景图的第二张图像
# 请记住,这些图像路径需要按从左到右的顺序进行补充!
ap = argparse.ArgumentParser()
ap.add_argument("-f", "--first", required=False, default="images/left1.jpg",
                help="path to the first image")
ap.add_argument("-s", "--second", required=False, default="images/right1.jpg",
                help="path to the second image")
args = vars(ap.parse_args())

# 加载图像,并保持宽高比的缩放以加快处理速度
imageA = cv2.imread(args["first"])
imageB = cv2.imread(args["second"])
imageA = imutils.resize(imageA, width=400)
imageB = imutils.resize(imageB, width=400)

# 把图像叠合在一起以构建全景图
stitcher = Stitcher()
(result, vis) = stitcher.stitch([imageA, imageB], showMatches=True)

# 展示图像
cv2.imshow("Image A", imageA)
cv2.imshow("Image B", imageB)
cv2.imshow("Keypoint Matches", vis)
cv2.imshow("Result", result)
cv2.waitKey(0)

panorama.py

# 导入必要的包
import numpy as np
import imutils
import cv2


class Stitcher:
    def __init__(self):
        # 定义是否在使用opncv3
        self.isv3 = imutils.is_cv3(or_better=True)

    # 缝合器方法
    # images 要缝合在一起以形成全景图的(两张)图像的列表
    # ratio  用于匹配功能时进行David Lowe的比率测试
    # reprojThresh RANSAC算法允许的最大像素“摆动空间”
    # showMatches 一个布尔值,用于指示关键点匹配是否应该可视化。
    def stitch(self, images, ratio=0.75, reprojThresh=4.0, showMatches=False):
        # 解压缩图像
        # 然后在俩张图像上,检测关键点并提取局部不变描述符(extract local invariant descriptors)
        (imageB, imageA) = images
        (kpsA, featuresA) = self.detectAndDescribe(imageA)
        (kpsB, featuresB) = self.detectAndDescribe(imageB)

        # 在俩张图像上匹配特征
        M = self.matchKeypoints(kpsA, kpsB,
                                featuresA, featuresB, ratio, reprojThresh)

        # 如果匹配为“无”,则没有充分匹配
        # 无法创建全景的关键点
        if M is None:
            return None

        # 应用透视变换(perspective transform)叠合俩张图像
        # 打开了元组的包装,从而提供了关键点匹配列表,单应矩阵H源自RANSAC算法,最后是状态
        (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)

            # 返回tuple,包括叠合后的全景图及可视化结果
            return (result, vis)

        # 返回叠合后的图像
        return result

    # 方法接受图像,然后检测关键点并提取局部不变描述符。
    # 在实现中,使用高斯差分(DoG Difference of Gaussian)关键点检测器和SIFT特征提取器。
    def detectAndDescribe(self, image):
        # 转换图像为灰度图
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

        # 兼容openCV版本
        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)

        # 将关键点对象转换为Numpy数组
        kps = np.float32([kp.pt for kp in kps])

        # 返回一个元组,包括关键点数组和特征
        return (kps, features)

    # 将特征匹配在一起实际上是一个相当简单的过程。我们只需从两个图像中循环遍历描述符,计算距离,然后找到每对描述符的最小距离。
    # 由于这是计算机视觉中非常普遍的做法,因此OpenCV具有称为cv2的内置函数。需要四个参数
    # kpsA,featuresA 与第一个图像关联的关键点和特征向量
    # kpsB,featuresB 其后是与第二个图像关联的关键点和特征向量
    # ratio 大卫·劳的比率
    # reprojThresh 提供了测试变量和RANSAC重投影阈值
    def matchKeypoints(self, kpsA, kpsB, featuresA, featuresB,
                       ratio, reprojThresh):
        # cv2.DescriptorMatcher_create 表示将穷举计算两个图像中所有特征向量之间的欧几里得距离,并找到距离最小的一对描述符。
        # 调用knnMatch使用k = 2在两个特征向量集之间执行k-NN匹配(指示返回每个特征向量的前两个匹配项)。
        # 希望获得前两场匹配而不是仅仅获得第一场匹配的原因是,我们需要将David Lowe的比率测试应用于假阳性匹配修剪。
        matcher = cv2.DescriptorMatcher_create("BruteForce")
        rawMatches = matcher.knnMatch(featuresA, featuresB, 2)
        matches = []

        # 遍历匹配以修剪假阳性匹配(false-positive match pruning)
        # 应用Lowe比率测试,该测试用于确定高质量的功能匹配。Lowe比率的典型值通常在[0.7,0.8]范围内。
        for m in rawMatches:
            # 确保距离在一个特定的比率下
            if len(m) == 2 and m[0].distance < m[1].distance * ratio:
                matches.append((m[0].trainIdx, m[0].queryIdx))

        # 获得匹配后,使用Lowe的比率测试,可以计算两组关键点之间的单应性
        # 计算单应性(homography)需要至少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

    # 可视化两个图像之间的关键点匹配关系
    # 此方法要求我们传入两个原始图像,与每个图像关联的关键点集,
    # 应用Lowe比率测试后的初始匹配项以及最终状态单应性计算提供的列表。
    # 使用这些变量,可以通过绘制从第一张图像中的关键点N到第二张图像中的关键点M的直线来可视化“内部”关键点。
    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

参考

猜你喜欢

转载自blog.csdn.net/qq_40985985/article/details/112918413