使用OpenCV和Python拼接图像

写在前面

首先这是一篇英文博客的翻译,先放上链接:https://www.pyimagesearch.com/2018/12/17/image-stitching-with-opencv-and-python/
翻译是靠谷歌翻译和自己的理解,个别地方翻译有点问题,请对照原文,大神可以直接阅读原文。
知道Adrian Rosebrock有一段时间了,是一位高质量、高产的大神,写的博客有很多干货。
翻译的目的,一是对图像拼接感兴趣,二是可以加深理解和记忆,后续可能还会翻译一些博客,向大佬学习!
这篇博客主要是利用OpenCV内置的库实现的图像拼接,没有涉及太多理论知识,更多内容可以阅读博主之前的博客(下面有介绍),以及搜索相关资料。

Introduction

在这里插入图片描述
在本教程中,您将学习如何使用Python,OpenCV以及cv2.createStitchercv2.Stitcher_create函数执行图像拼接。使用今天的代码,您将能够将多个图像拼接在一起,创建拼接图像的全景图。

就在不到两年前,我发布了两个关于图像拼接和全景构造的指南:

这两个教程都涵盖了典型图像拼接算法的基础知识,它至少需要四个关键步骤:
1.检测关键点(DoG,Harris等)并从两个输入图像中提取局部不变描述子(SIFT,SURF等)
2.匹配图像之间的描述子
3.使用RANSAC算法使用我们匹配的特征向量估计单应矩阵
4.使用从步骤#3获得的单应矩阵应用warping变换

但是,我原来实现的最大问题是它们无法处理两个以上的输入图像。

在今天的教程中,我们将重新审视使​​用OpenCV的图像拼接,包括如何将两个以上的图像拼接成全景图像。

使用OpenCV和Python拼接图像

在今天教程的第一部分中,我们将简要回顾OpenCV的图像拼接算法,该算法通过cv2.createStitchercv2.Stitcher_create函数加入到OpenCV库中。
从那里我们将审查我们的项目结构并实现可用于图像拼接的Python脚本。
我们将查看第一个脚本的结果,注意其局限性,然后实现第二个Python脚本,该脚本可用于更美观的图像拼接结果。
最后,我们将审查第二个脚本的结果,并再次注意任何限制或缺点。

OpenCV的图像拼接算法

在这里插入图片描述
我们今天在这里使用的算法类似于Brown和Lowe在其2017年论文“Automatic Panoramic Image Stitching with Invariant Features”中提出的方法。
与以前对输入图像的排序敏感的图像拼接算法不同,Brown和Lowe方法更加鲁棒,使其对以下内容不敏感:

  • 图像的顺序
  • 图像的方向
  • 光照变化
  • 实际上不是全景图的一部分的噪声图像

此外,它们的图像拼接方法能够通过使用增益补偿和图像混合产生更美观的输出全景图像。
对该算法的完整,详细的审查超出了本文的范围,因此如果您有兴趣了解更多信息,请参阅原始出版物

项目结构

让我们看看如何使用tree命令组织此项目:

$ tree --dirsfirst
.
├── images
│   └── scottsdale
│       ├── IMG_1786-2.jpg
│       ├── IMG_1787-2.jpg
│       └── IMG_1788-2.jpg
├── image_stitching.py
├── image_stitching_simple.py
└── output.png
 
2 directories, 6 files

输入图像在images/文件夹。我选择为我的scottsdale/图像集制作一个子文件夹,以防我想在以后添加其他子文件夹。
今天我们将回顾两个Python脚本:

  • image_stitching_simple.py:我们的简单版图像拼接可以在不到50行的Python代码中完成!
  • image_stitching.py:这个脚本包括我的hack来提取拼接图像的ROI以获得美观​​的结果。

最后一个文件output.png是生成的拼接图像的名称。使用命令行参数,您可以轻松更改输出图像的文件名+路径。

cv2.createStitcher和cv2.Stitcher_create函数

在这里插入图片描述
OpenCV已经通过cv2.createStitcher(OpenCV 3.x)和cv2.Stitcher_create(OpenCV 4)函数实现了类似于Brown和Lowe的论文的方法。
假设您已正确配置和安装OpenCV,您将能够查询OpenCV 3.x的cv2.createStitcher的函数签名:

createStitcher(...)
    createStitcher([, try_use_gpu]) -> retval

请注意,此函数只有一个参数try_gpu,可用于改善整个图像拼接pipeline。OpenCV的GPU支持是有限的,我从来没有能够使这个参数工作,所以我建议总是把它保留为False

OpenCV 4的cv2.Stitcher_create函数具有类似的签名:

Stitcher_create(...)
    Stitcher_create([, mode]) -> retval
    .   @brief Creates a Stitcher configured in one of the stitching
    .	modes.
    .   
    .   @param mode Scenario for stitcher operation. This is usually
    .	determined by source of images to stitch and their transformation.
    .	Default parameters will be chosen for operation in given scenario.
    .   @return Stitcher class instance.

要执行实际的图像拼接,我们需要调用.stitch方法:

OpenCV 3.x:
stitch(...) method of cv2.Stitcher instance
    stitch(images[, pano]) -> retval, pano
 
OpenCV 4.x:
stitch(...) method of cv2.Stitcher instance
    stitch(images, masks[, pano]) -> retval, pano
    .   @brief These functions try to stitch the given images.
    .   
    .   @param images Input images.
    .   @param masks Masks for each input image specifying where to
    .	look for keypoints (optional).
    .   @param pano Final pano.
    .   @return Status code.

此方法接受输入图像列表,然后尝试将它们拼接成全景图,将输出全景图像返回到调用函数。
status变量指示图像拼接是否成功,并且可以是以下四个变量之一:

  • OK = 0:图像拼接成功。
  • ERR_NEED_MORE_IMGS = 1:如果您收到此状态代码,则需要更多输入图像来构建全景图。通常,如果输入图像中检测不到足够的关键点,则会发生此错误。
  • ERR_HOMOGRAPHY_EST_FAIL = 2:当RANSAC单应性估计失败时,会发生此错误。同样,您可能需要更多图像,或者您的图像没有足够的区别,独特的纹理/对象,以便准确匹配关键点。
  • ERR_CAMERA_PARAMS_ADJUST_FAIL = 3:我之前从未遇到过这个错误,所以我对它没有多少了解,但要点是它与未能从输入图像中正确估计相机内参/外参有关。如果遇到此错误,您可能需要参考OpenCV文档,甚至可以深入了解OpenCV C ++代码。

现在我们已经回顾了cv2.createStitchercv2.Stitcher_create.stitch方法,让我们继续实际使用OpenCV和Python实现图像拼接。

用Python实现图像拼接

打开image_stitching_simple.py文件并插入以下代码:

# import the necessary packages
from imutils import paths
import numpy as np
import argparse
import imutils
import cv2
 
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--images", type=str, required=True,
	help="path to input directory of images to stitch")
ap.add_argument("-o", "--output", type=str, required=True,
	help="path to the output image")
args = vars(ap.parse_args())

我们所需的包由第2-6行语句导入。值得注意的是,我们将使用OpenCV和imutils。如果您还没有,请继续安装它们:

  • 要安装OpenCV,只需按照我的OpenCV安装指南之一操作即可。
  • 可以使用pip:pip install --upgrade imutils安装/更新imutils包。请务必升级它,因为通常会添加新功能。

如果您不熟悉argparse和命令行参数的概念,请阅读此博客文章
让我们加载输入图像:

# grab the paths to the input images and initialize our images list
print("[INFO] loading images...")
imagePaths = sorted(list(paths.list_images(args["images"])))
images = []
 
# loop over the image paths, load each one, and add them to our images to stitch list
for imagePath in imagePaths:
	image = cv2.imread(imagePath)
	images.append(image)

在这里我们获取我们的imagePaths(第2行)。
然后,对于每个imagePath,我们将加载image并将其添加到images(第3-8行)。
现在,images在内存中,让我们继续使用OpenCV的内置功能将它们拼接成全景图:

# initialize OpenCV's image stitcher object and then perform the image stitching
print("[INFO] stitching images...")
stitcher = cv2.createStitcher() if imutils.is_cv3() else cv2.Stitcher_create()
(status, stitched) = stitcher.stitch(images)

在第3行创建了stitcher对象。请注意,根据您使用的是OpenCV 3还是4,将调用不同的构造函数。
随后,我们可以将图像传递给.stitch方法(第4行)。对.stitch的调用返回statusstitched图像(假设拼接成功)。
最后,我们将(1)将拼接图像写入磁盘并(2)在屏幕上显示:

# if the status is '0', then OpenCV successfully performed image stitching
if status == 0:
	# write the output stitched image to disk
	cv2.imwrite(args["output"], stitched)
 
	# display the output stitched image to our screen
	cv2.imshow("Stitched", stitched)
	cv2.waitKey(0)
 
# otherwise the stitching failed, likely due to not enough keypoints) being detected
else:
	print("[INFO] image stitching failed ({})".format(status))

假设我们的状态标志指示成功(第2行),我们将stitched图像写入磁盘(第4行)并显示它直到按下一个键(第7和8行)。
否则,我们只会打印一条失败消息(第11和12行)。

基本图像拼接结果

要尝试使用图像拼接脚本,请确保使用教程的“Downloads”部分下载源代码和示例图像。
images/scottsdale/目录中,你会发现我在亚利桑那州斯科茨代尔参观Frank Lloyd Wright著名的Taliesin West房子时拍摄的三张照片:
在这里插入图片描述
我们的目标是将这三幅图像拼接成一幅全景图像。要执行拼接,请打开终端,导航到下载代码和图像的位置,然后执行以下命令:

$ python image_stitching_simple.py --images images/scottsdale --output output.png
[INFO] loading images...
[INFO] stitching images...

在这里插入图片描述
请注意我们已经成功执行图像拼接!
但那些全景周围的黑色区域呢?那些是什么?
这些区域来自执行构建全景图所需的视角warps。
有一种方法可以摆脱它们……但我们需要在下一节中实现一些额外的逻辑。

使用OpenCV和Python的更好的图像拼接器

在这里插入图片描述
我们的第一个图像拼接脚本是一个良好的开端,但围绕全景本身的那些黑色区域不是我们称之为“美学上令人愉悦”的东西。
更重要的是,你不会在iOS,Android等内置流行的图像拼接应用程序中看到这样的输出图像。
因此,我们将稍微hack我们的脚本并包含一些额外的逻辑来创建更美观的全景图。
我将再次重申,这种方法是一种黑客(hack)行为。
我们将审查基本的图像处理操作,包括阈值,轮廓提取,形态学操作等,以获得我们想要的结果。
据我所知,OpenCV的Python绑定并没有为我们提供手动提取全景图的最大内部矩形区域所需的信息。如果OpenCV可以做到,请在评论中告诉我,我很想知道。
让我们继续并开始 - 打开image_stitching.py脚本并插入以下代码:

# import the necessary packages
from imutils import paths
import numpy as np
import argparse
import imutils
import cv2

# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--images", type=str, required=True,
	help="path to input directory of images to stitch")
ap.add_argument("-o", "--output", type=str, required=True,
	help="path to the output image")
ap.add_argument("-c", "--crop", type=int, default=0,
	help="whether to crop out largest rectangular region")
args = vars(ap.parse_args())

# grab the paths to the input images and initialize our images list
print("[INFO] loading images...")
imagePaths = sorted(list(paths.list_images(args["images"])))
images = []

# loop over the image paths, load each one, and add them to our images to stich list
for imagePath in imagePaths:
	image = cv2.imread(imagePath)
	images.append(image)

# initialize OpenCV's image sticher object and then perform the image stitching
print("[INFO] stitching images...")
stitcher = cv2.createStitcher() if imutils.is_cv3() else cv2.Stitcher_create()
(status, stitched) = stitcher.stitch(images)

所有这些代码都与我们之前的脚本相同,只有一个例外。
添加了--crop命令行参数。当在终端中为此参数提供1时,我们将继续执行我们的裁剪hack。
下一步我们开始实现其他功能:

# if the status is '0', then OpenCV successfully performed image stitching
if status == 0:
	# check to see if we supposed to crop out the largest rectangular region from the stitched image
	if args["crop"] > 0:
		# create a 10 pixel border surrounding the stitched image
		print("[INFO] cropping...")
		stitched = cv2.copyMakeBorder(stitched, 10, 10, 10, 10,
			cv2.BORDER_CONSTANT, (0, 0, 0))

		# convert the stitched image to grayscale and threshold it
		# such that all pixels greater than zero are set to 255
		# (foreground) while all others remain 0 (background)
		gray = cv2.cvtColor(stitched, cv2.COLOR_BGR2GRAY)
		thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)[1]

注意我是如何在第4行设置--crop标志时创建一个新块的。让我们开始讨论这个块:

  • 首先,我们将在拼接图像的所有边上添加10像素边框(第7和8行),确保我们能够在本节后面找到完整全景轮廓的轮廓。
  • 然后我们将创建一个灰度版本的拼接图像(第13行)。
  • 从那里我们对灰度图像进行阈值处理(第14行)。

以下是这三个步骤的结果(阈值):
在这里插入图片描述
我们现在有一个全景图的二进制图像,其中白色像素(255)是前景,黑色像素(0)是背景。
给定我们的阈值图像,我们可以应用轮廓提取,计算最大轮廓的边界框(即全景轮廓本身的轮廓),并绘制边界框:

		# find all external contours in the threshold image then find
		# the *largest* contour which will be the contour/outline of
		# the stitched image
		cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
			cv2.CHAIN_APPROX_SIMPLE)
		cnts = imutils.grab_contours(cnts)
		c = max(cnts, key=cv2.contourArea)

		# allocate memory for the mask which will contain the
		# rectangular bounding box of the stitched image region
		mask = np.zeros(thresh.shape, dtype="uint8")
		(x, y, w, h) = cv2.boundingRect(c)
		cv2.rectangle(mask, (x, y), (x + w, y + h), 255, -1)

在第4-6行上提取和解析轮廓。然后,第7行抓取具有最大区域的轮廓(即拼接图像本身的轮廓)。
注意:imutils.grab_contours函数是imutils == 0.5.2中的新功能,以适应OpenCV 2.4,OpenCV 3和OpenCV 4以及它们对cv2.findContours的不同返回签名。
第11行为我们的新矩形mask分配内存。然后,第12行计算出最大轮廓的边界框。使用边界矩形信息,在第13行,我们在mask上绘制一个纯白色矩形。
上面代码块的输出如下所示:
在这里插入图片描述
此边界框是整个全景图可以容纳的最小矩形区域。
现在,这里出现了我为博客文章撰写的最大的hack之一:

		# create two copies of the mask: one to serve as our actual
		# minimum rectangular region and another to serve as a counter
		# for how many pixels need to be removed to form the minimum
		# rectangular region
		minRect = mask.copy()
		sub = mask.copy()

		# keep looping until there are no non-zero pixels left in the
		# subtracted image
		while cv2.countNonZero(sub) > 0:
			# erode the minimum rectangular mask and then subtract
			# the thresholded image from the minimum rectangular mask
			# so we can count if there are any non-zero pixels left
			minRect = cv2.erode(minRect, None)
			sub = cv2.subtract(minRect, thresh)

在第5和6行,我们创建了两个掩模图像副本:

  • minMask,第一个mask,将逐渐缩小,直到它可以放入全景内部。
  • sub,第二个mask,将用于确定是否需要继续减小minMask的大小。

第10行开始一个while循环,它将继续循环,直到sub中没有更多的前景像素。
第14行执行侵蚀形态学操作以减小minRect的大小。
第15行然后从minRect中减去thresh ,一旦minRect中没有更多的前景像素,我们就可以从循环中断开。
我在下面做了一个动画:
在这里插入图片描述
上面是sub图像,下面是minRect图像。
注意minRect的大小是如何逐渐减小的,直到sub中没有剩下前景像素为止,此时我们知道我们已经找到了可以适合全景最大矩形区域的最小矩形掩模。
给定最小内部矩形,我们可以再次找到轮廓并计算边界框,但这次我们只需从拼接图像中提取ROI:

		# find contours in the minimum rectangular mask and then
		# extract the bounding box (x, y)-coordinates
		cnts = cv2.findContours(minRect.copy(), cv2.RETR_EXTERNAL,
			cv2.CHAIN_APPROX_SIMPLE)
		cnts = imutils.grab_contours(cnts)
		c = max(cnts, key=cv2.contourArea)
		(x, y, w, h) = cv2.boundingRect(c)

		# use the bounding box coordinates to extract the our final
		# stitched image
		stitched = stitched[y:y + h, x:x + w]

这里:

  • minRect中找到轮廓(第3和4行)。
  • 为多个OpenCV版本处理解析轮廓(第5行)。您需要imutils>= 0.5.2才能使用此功能。
  • 抓住最大的轮廓(第6行)。
  • 计算最大轮廓的边界框(第7行)。
  • 使用边界框信息从我们的拼接图像中提取ROI(第11行)。

最终拼接的图像可以显示在我们的屏幕上,然后保存到磁盘:

	# write the output stitched image to disk
	cv2.imwrite(args["output"], stitched)

	# display the output stitched image to our screen
	cv2.imshow("Stitched", stitched)
	cv2.waitKey(0)

# otherwise the stitching failed, likely due to not enough keypoints)
# being detected
else:
	print("[INFO] image stitching failed ({})".format(status))

改进的图像拼接效果

打开一个终端并执行以下命令:

$ python image_stitching.py --images images/scottsdale --output output.png \
	--crop 1
[INFO] loading images...
[INFO] stitching images...
[INFO] cropping...

在这里插入图片描述
注意这次我们如何通过应用上面详述的hack从输出拼接图像中去除黑色区域(由warping变换引起)。

限制和缺点

在之前的教程中,我演示了如何构建实时全景图像拼接算法 ,这个教程取决于我们手动执行关键点检测,特征提取和关键点匹配这一事实,使我们能够访问使用的单应矩阵将我们的两个输入图像变成全景图。
虽然OpenCV的内置cv2.createStitchercv2.Stitcher_create函数当然能够构建准确,美观的全景图,但该方法的主要缺点之一是它抽象出对单应矩阵的任何访问。
实时全景构造的假设之一是场景本身在内容方面没有太大变化。
一旦我们计算出初始单应性估计,我们只需要偶尔重新计算矩阵。
无需执行完整的关键点匹配和RANSAC估计,在构建全景图时可以大大提高速度,因此无需访问原始单应矩阵,采用OpenCV的内置图像拼接算法并将其转换为实时具有挑战性。

使用OpenCV执行图像拼接时遇到错误?

尝试使用cv2.createStitcher函数或cv2.Stitcher_create函数时,可能会遇到错误。
我看到人们遇到的两个“易于解决”的错误就是忘记了他们正在使用的OpenCV版本。
例如,如果您使用OpenCV 4但尝试调用cv2.createSticher,您将遇到以下错误消息:

cv2.createStitcher
Traceback (most recent call last):
File “”, line 1, in
AttributeError: module ‘cv2’ has no attribute ‘createStitcher’

您应该使用cv2.Stitcher_create函数。
同样,如果您使用OpenCV 3并尝试调用cv2.Sticher_create,您将收到此错误:

cv2.Stitcher_create
Traceback (most recent call last):
File “”, line 1, in
AttributeError: module ‘cv2’ has no attribute ‘Stitcher_create’

而是使用cv2.createSticher函数。
如果您不确定您使用的是哪个OpenCV版本,可以使用cv2 .__ version__进行检查:

>>> cv2.__version__
'4.0.0'

在这里你可以看到我正在使用OpenCV 4.0.0。
您可以对系统执行相同的检查。
您可能遇到的最终错误,可以说是最常见的错误,与OpenCV(1)没有贡献支持和(2)在未启用OPENCV_ENABLE_NONFREE = ON选项的情况下编译有关。
要解决此错误,必须安装opencv_contrib模块并将OPENCV_ENABLE_NONFREE选项设置为ON
如果您遇到与OpenCV的none-freecontrib模块相关的错误,请确保参考我的OpenCV安装指南以确保您完全安装了OpenCV。
注意:如果您未遵循我的某个安装指南,我将无法调试您自己安装的OpenCV,因此请确保在配置系统时使用我的OpenCV安装指南。

总结

在今天的教程中,您学习了如何使用OpenCV和Python执行多个图像拼接。
使用OpenCV和Python,我们能够将多个图像拼接在一起并创建全景图像。
我们的输出全景图像不仅精确的拼接位置,而且美观也令人愉悦。
然而,使用OpenCV的内置图像拼接类的一个最大缺点是它抽象了大部分内部计算,包括产生的单应矩阵本身。
如果您尝试执行实时图像拼接,就像我们在上一篇文章中所做的那样,您可能会发现缓存单应矩阵并且偶尔执行关键点检测,特征提取和特征匹配是有益的。
跳过这些步骤并使用缓存矩阵执行透视变形可以减少管道的计算负担并最终加速实时图像拼接算法,但不幸的是,OpenCV的cv2.createStitcher Python绑定不能让我们访问原始矩阵。
如果您有兴趣了解有关实时全景构建的更多信息,请参阅我之前的帖子
我希望你喜欢今天关于图像拼接的教程!

猜你喜欢

转载自blog.csdn.net/learning_tortosie/article/details/85083825
今日推荐