图片的全景拼接如今已不再稀奇,现在的智能摄像机和手机摄像头基本都带有图片自动全景拼接的功能,但是一般都会要求拍摄者保持设备的平稳以及单方向的移动取景以实现较好的拼接结果。这是因为拼接的图片之间必须要有相似的区域以保证拼接结果的准确性和完整性。本文主要简单描述如何用 Python 和 OpenCV 库实现多张图片的自动拼合。
要实现图像的拼接融合,首先我们先看两张图片的简单拼接,其实只需找出两张图片中相似的点 (至少四个,因为 homography 矩阵的计算需要至少四个点), 计算一张图片可以变换到另一张图片的变换矩阵 (homography 单应性矩阵),用这个矩阵把那张图片变换后放到另一张图片相应的位置 ( 就是相当于把两张图片中定好的四个相似的点给重合在一起)。如此,就可以实现简单的全景拼接。
举个例子,下面是书上原图的全景拼接结果:
全景图:
(一) RANSAC算法
回顾上一篇博客内容,SIFT算法的描述子稳健性很强,比Harris角点要来得精确,但是它的匹配正确率也并不是百分百的,会受到一些噪声点的干扰,有时就会因为不同地方有类似的图案导致匹配错误。那么RANSAC算法便是用来找到正确模型来拟合带有噪声数据的迭代方法。
RANSAC通过反复取样,也就是从整个观测数据中随机抽一些数据估算模型参数之后看和所有数据误差有多大,然后取误差最小视为最好以及分离内群与离群数据。基本的思想是,数据中包含正确的点和噪声点,合理的模型应该能够在描述正确数据点的同时摒弃噪声点。
举个简单的例子,直线的拟合便是RANSAC的一个标准化体现:
同理,RANSAC算法可以应用到其它模块中,例如用于图像变换的单应性矩阵的计算。
在拼接的过程中,通过将响速和单应矩阵H相乘,然后对齐次坐标进行归一化来实现像素间的映射。通过查看H中的平移量,我们可以决定应该将该图像填补到左边还是右边。
(二) 全景拼接
SIFT特征匹配前面写过,这里不多做介绍。这次将其应用到图像拼接上,根据特征点匹配的方式,则利用这些匹配的点来估算单应矩阵(使用上面的RANSAC算法),也就是把其中一张通过个关联性和另一张匹配的方法。通过单应矩阵H,可以将原图像中任意像素点坐标转换为新坐标点,转换后的图像即为适合拼接的结果图像。
(1)图像配准
图像配准是对图像进行变换,使变换后的图像能够在常见的坐标系中对齐。为了能够进行图像对比和更精细的图像分析,图像配准是一步非常重要的操作。
图像配准的方法有很多,这里以APAP算法为例:
1.提取两张图片的sift特征点。
2.对两张图片的特征点进行匹配。
3.匹配后,仍有很多错误点,用RANSAC的改进算法进行特征点对的筛选。筛选后的特征点基本能够异议一 一对应。
4.使用DLT算法,将剩下的特征点对进行透视变换矩阵的估计。
5.因为得到的透视变换矩阵是基于全局特征点对进行的,即一个刚性的单应性矩阵完成配准。为提高配准的精度,Apap将图像切割成无数多个小方块,对每个小方块的变换矩阵逐一估计。
(2)图割方法
1.关于最小割
如以下图1所示,这是一个有向带权图,共有4个顶点和5条边。每条边上的箭头代表了边的方向,每条边上的数字代表了边的权重。
什么是最小割?
现在要求剪短图中的某几条边,使得不存在从s到t的路径,并且保证所减的边的权重和最小。那么很明显,剪掉边”s->a”和边”b->t”。我们就能得到上图中的图2.
图中已不存在从s到t的路径,且所修剪的边的权重和为:2 + 3 = 5,为所有修剪方式中权重和最小的。我们把这样的修剪称为最小割。
2.关于最大流
什么是最大流?
如以上图1,因为s->a只能通过2,那么s->a->t这条路的流量被限制在了2,同理,s->b->t被b->t限制,最多只能通过3的流量,所以s->t的流量总和为2+3=5,为这条路的最大流。
(3)图像融合-blending
其实图像拼接完会发现在拼接的交界处有明显的衔接痕迹,存在边缘效应,因为光照色泽的原因使得图片交界处的过渡很糟糕,所以需要特定的处理解决这种不自然。那么这时候可以采用blending方法,在opencv内部已经实现了multi-band blending。
# -*- coding: utf-8
from pylab import *
from numpy import *
from PIL import Image
import numpy as np
# If you have PCV installed, these imports should work
from PCV.geometry import homography, warp
from PCV.localdescriptors import sift
"""
This is the panorama example from section 3.3.
"""
np.seterr(divide='ignore', invalid='ignore')
# set paths to data folder
featname = ['D:/Alike/python_dw/Code_a/admin_code/data/data_2/o' + str(i + 1) + '.sift' for i in range(5)]
imname = ['D:/Alike/python_dw/Code_a/admin_code/data/data_2/o' + str(i + 1) + '.jpg' for i in range(5)]
# extract features and match
l = {}
d = {}
for i in range(5):
sift.process_image(imname[i], featname[i])
l[i], d[i] = sift.read_features_from_file(featname[i])
matches = {}
for i in range(4):
matches[i] = sift.match(d[i + 1], d[i])
# visualize the matches (Figure 3-11 in the book)
# sift匹配可视化
for i in range(4):
im1 = array(Image.open(imname[i]))
im2 = array(Image.open(imname[i + 1]))
figure()
sift.plot_matches(im2, im1, l[i + 1], l[i], matches[i], show_below=True)
# function to convert the matches to hom. points
# 将匹配转换成齐次坐标点的函数
def convert_points(j):
ndx = matches[j].nonzero()[0]
fp = homography.make_homog(l[j + 1][ndx, :2].T)
ndx2 = [int(matches[j][i]) for i in ndx]
tp = homography.make_homog(l[j][ndx2, :2].T)
# switch x and y - TODO this should move elsewhere
fp = vstack([fp[1], fp[0], fp[2]])
tp = vstack([tp[1], tp[0], tp[2]])
return fp, tp
# estimate the homographies
# 估计单应性矩阵
model = homography.RansacModel()
fp, tp = convert_points(1)
H_12 = homography.H_from_ransac(fp, tp, model)[0] # im 1 to 2 # im1 到 im2 的单应性矩阵
fp, tp = convert_points(0)
H_01 = homography.H_from_ransac(fp, tp, model)[0] # im 0 to 1
tp, fp = convert_points(2) # NB: reverse order
H_32 = homography.H_from_ransac(fp, tp, model)[0] # im 3 to 2
tp, fp = convert_points(3) # NB: reverse order
H_43 = homography.H_from_ransac(fp, tp, model)[0] # im 4 to 3
# warp the images
# 扭曲图像
delta = 2000 # for padding and translation 用于填充和平移
im1 = array(Image.open(imname[1]), "uint8")
im2 = array(Image.open(imname[2]), "uint8")
im_12 = warp.panorama(H_12, im1, im2, delta, delta)
im1 = array(Image.open(imname[0]), "f")
im_02 = warp.panorama(dot(H_12, H_01), im1, im_12, delta, delta)
im1 = array(Image.open(imname[3]), "f")
im_32 = warp.panorama(H_32, im1, im_02, delta, delta)
im1 = array(Image.open(imname[4]), "f")
im_42 = warp.panorama(dot(H_32, H_43), im1, im_32, delta, 2 * delta)
imsave('ping3.jpg', array(im_42, "uint8"))
figure()
imshow(array(im_42, "uint8"))
axis('off')
show()
针对固定点拍摄多张图片,实现图像的拼接融合
室外场景
原图:
拼接结果:
室内场景
原图:
拼接结果:
分析:
室外场景
原图:
拼接结果;
分析:
上面这个场景与前一组实验中的室外场景取景相同,不同的是这组图片在拍摄时我改变了位置,即不再站在一个固定点拍照,但是可以看到得到的全景图杂乱无章。
猜测这是由于镜头拍照的位置不同,会导致图片虽然有相同的拍摄区域,但是并不能简单的将五张图片的重叠区域覆盖进行拼接。因为拍摄五张图片时,相机的世界坐标位置发生变化。试想一下,你首先在A位置看到物体一,然后换到B位置再看物体一,物体一虽然仍在你的视野里,但是,它在你眼中与周围参照物的距离会发生变化,位置不同,光线也可能不同,这都是拼接过程中存在的问题。
视差变化大的场景
原图:
拼接结果:
分析:
在该场景下,护栏以及靠在护栏上的拖把是近景目标,建筑物是远景目标,视差变化大。观察近景目标拼接,可以看到右侧护栏处出现了一些偏差,观察远景目标,看到后面的建筑物出现鬼影现象;但是观察SIFT特征匹配结果,看到匹配结果基本正确。这是因为景深变化大,这时即使都是正确的匹配点,但是不能用homography单应性矩阵表示,要解决这种问题可以使用APAP算法。
全景图出现像素失真的现象,可能与我改变了图片的像素值大小有关。
import numpy as np
np.seterr(divide='ignore', invalid='ignore')
②问题二:得到的运行结果仅有非常窄的一个长条,无法看清内容。
解决方案:修改图片像素值,原本图像像素值是200×160,修改成1400×1120,即可得到较清晰的全景图。