目录:
本文参考上面这个链接,实现多张图像的拼接,构建一张全景图。
根据多个图像创建全景图的步骤为:
原理比较复杂,本文先不讲解,OpenCV中已经实现了全景图拼接的算法,它们是 cv2.createStitcher
(OpenCV 3.x) 和 cv2.Stitcher_create
(OpenCV 4) 。
该算法对以下条件具有较好的鲁棒性:
OpenCV 3.x 的 cv2.createStitcher 函数原型为:
createStitcher(...)
createStitcher([, try_use_gpu]) -> retval
这个函数有一个参数 try_use_gpu
,它可以用来提升图像拼接整个过程的速度。
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=0
表示图像拼接是否成功。
先把三张图片读取出来存放到列表里:
img_dir = 'pictures/stitching'
names = os.listdir(img_dir)
images = []
for name in names:
img_path = os.path.join(img_dir, name)
image = cv2.imread(img_path)
images.append(image)
图片顺序没有影响,我试了一下,不同的图片顺序,输出全景图都相同。
然后构造图像拼接对象stitcher
, 要注意的是,OpenCV 3 和 4 的构造器是不同的。
import imutils
stitcher = cv2.createStitcher() if imutils.is_cv3() else cv2.Stitcher_create()
再把图像列表传入.stitch
函数,该函数会返回状态和拼接好的全景图(如果没有错误):
status, stitched = stitcher.stitch(images)
完整代码如下:
import os
import cv2
import imutils
img_dir = 'pictures/stitching'
names = os.listdir(img_dir)
images = []
for name in names:
img_path = os.path.join(img_dir, name)
image = cv2.imread(img_path)
images.append(image)
stitcher = cv2.createStitcher() if imutils.is_cv3() else cv2.Stitcher_create()
status, stitched = stitcher.stitch(images)
if status==0:
cv2.imwrite('pictures/stitch.jpg', stitched)
OpenCV真的很强大,这短短几行,就实现了拼接全景图。
全景图如下:
呃,全景图是实现了,但是周围出现了一些黑色区域。
这是因为构建全景时会做透视变换,透视变换时会产生这些黑色区域。
所以需要做进一步处理,裁剪出全景图的最大内部矩形区域,也就是只保留下图中红色虚线边框内的全景区域。
获取图像列表并得到初步全景图,这两步还是相同的:
img_dir = 'pictures/stitching'
names = os.listdir(img_dir)
images = []
for name in names:
img_path = os.path.join(img_dir, name)
image = cv2.imread(img_path)
images.append(image)
stitcher = cv2.createStitcher() if imutils.is_cv3() else cv2.Stitcher_create()
status, stitched = stitcher.stitch(images)
在全景图四周各添加10像素宽的黑色边框,以确保能够找到全景图的完整轮廓:
stitched = cv2.copyMakeBorder(stitched, 10, 10, 10, 10,
cv2.BORDER_CONSTANT, (0, 0, 0))
再将全景图转换为灰度图,并将不为0的像素全置为255,作为前景,其他像素灰度值为0,作为背景。
gray = cv2.cvtColor(stitched, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
现在有了全景图的二值图,再应用轮廓检测,找到最大轮廓的边界框,
注:和轮廓相关的详细讲解可以查看 这篇文章。
cnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = max(cnts, key=cv2.contourArea) # 获取最大轮廓
mask = np.zeros(thresh.shape, dtype="uint8")
x, y, w, h = cv2.boundingRect(cnt)
# 绘制最大外接矩形框(内部填充)
cv2.rectangle(mask, (x, y), (x + w, y + h), 255, -1)
这个白色矩形框是整个全景图可以容纳下的最小矩形区域。
接下来就是最难,也是最巧妙的部分了,先创建mask的两个副本:
minRect
,这个mask的白色区域会慢慢缩小,直到它刚好可以完全放入全景图内部。sub
,这个mask用于确定minRect
是否需要继续减小,以得到满足要求的矩形区域。minRect = mask.copy()
sub = mask.copy()
# 开始while循环,直到sub中不再有前景像素
while cv2.countNonZero(sub) > 0:
minRect = cv2.erode(minRect, None)
sub = cv2.subtract(minRect, thresh)
不断地对minRect
进行腐蚀操作,然后用minRect
减去之前得到的阈值图像,得到sub
,
再判断sub
中是否存在非零像素,如果不存在,则此时的minRect
就是我们最终想要的全景图内部最大矩形区域。
sub
和minRect
在while循环中的变化情况如下动图所示:
因为OpenCV中灰度图像素值范围0-255,如果两个数相减得到负数的话,会直接将其置为0;如果两个数相加,结果超过了255的话,则直接置为255。
比如下面这个图,左图中白色矩形可以完全包含在全景图中,但不是全景图的最大内接矩形,用它减去右边的阈值图,
因为黑色像素减白色像素,会得到黑色像素,所以其结果图为全黑的图。
所以上面那个while循环最终得到的minRect
就是减去阈值图得到全黑图的面积最大的矩形区域。
好了,我们已经得到全景图的内置最大矩形框了,接下来就是找到这个矩形框的轮廓,并获取其坐标:
cnts, hierarchy = cv2.findContours(minRect.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = max(cnts, key=cv2.contourArea)
# 计算最大轮廓的边界框
(x, y, w, h) = cv2.boundingRect(cnt)
# 使用边界框坐标提取最终的全景图
stitched = stitched[y:y + h, x:x + w]
得到最终结果图如下:
完整代码如下:
import os
import cv2
import imutils
import numpy as np
img_dir = '/images'
names = os.listdir(img_dir)
images = []
for name in names:
img_path = os.path.join(img_dir, name)
image = cv2.imread(img_path)
images.append(image)
stitcher = cv2.createStitcher() if imutils.is_cv3() else cv2.Stitcher_create()
status, stitched = stitcher.stitch(images)
# 四周填充黑色像素,再得到阈值图
stitched = cv2.copyMakeBorder(stitched, 10, 10, 10, 10, cv2.BORDER_CONSTANT, (0, 0, 0))
gray = cv2.cvtColor(stitched, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
cnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = max(cnts, key=cv2.contourArea)
mask = np.zeros(thresh.shape, dtype="uint8")
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(mask, (x, y), (x + w, y + h), 255, -1)
minRect = mask.copy()
sub = mask.copy()
# 开始while循环,直到sub中不再有前景像素
while cv2.countNonZero(sub) > 0:
minRect = cv2.erode(minRect, None)
sub = cv2.subtract(minRect, thresh)
cnts, hierarchy = cv2.findContours(minRect.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = max(cnts, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(cnt)
# 使用边界框坐标提取最终的全景图
stitched = stitched[y:y + h, x:x + w]
cv2.imwrite('final.jpg', stitched)
如果觉得有用,就点个赞吧(ง •̀_•́)ง。