图像配准(apap)是将两张场景相关的图像进行映射,寻找其中的关系,多用在医学图像配准、图像拼接、不同摄像机的几何标定等方面,其研究也较为成熟。OpenCv中的stitching类就是使用了2007年的一篇论文(Automatic panoramic image stitching using invariant features)实现的。虽然图像配准已较为成熟,但其实其精度、鲁棒性等在某些场合仍不足够,如光线差异很大的两张图片、拍摄角度差异很大的图片等。2013年,Julio Zaragoza等人发表了一种新的图像配准算法Apap(As-Projective-As-Possible Image Stitching with Moving DLT),该算法的效果还是不错的,比opencv自带的auto-stitch效果要好。而2015年也有一篇cvpr是介绍图像配准(Non-rigid Registration of Images with Geometric and Photometric Deformation by Using Local Affine Fourier-Moment Matching),其效果貌似很牛,但没有源码,难以检验。
如图1所示,是一个有向带权图,共有4个顶点和5条边。每条边上的箭头代表了边的方向,每条边上的数字代表了边的权重。
G = < V, E >是图论中对图的表示方式,其中V表示顶点(vertex)所构成的集合,E表示边(edge)所构成的集合。顶点V的集合和边的集合E构成了图G(graph)。
那什么是最小割呢?
以图1为例,图1中顶点s表示源点(source),顶点t表示终点(terminal),从源点s到终点t共有3条路径:
s -> a -> t
s -> b -> t
s -> a -> b-> t
现在要求剪短图中的某几条边,使得不存在从s到t的路径,并且保证所减的边的权重和最小。相信大家能很快想到解答:剪掉边”s -> a”和边”b -> t”。
剪完以后的图如图2所示。此时,图中已不存在从s到t的路径,且所修剪的边的权重和为:2 + 3 = 5,为所有修剪方式中权重和最小的。
我们把这样的修剪称为最小割。
什么是最大流呢?
继续以图1为例,假如顶点s源源不断有水流出,边的权重代表该边允许通过的最大水流量,请问顶点t流入的水流量最大是多少?
从顶点s到顶点t的3条路径着手分析,从源点s到终点t共有3条路径:
s -> a -> t:流量被边”s -> a”限制,最大流量为2
s -> b -> t:流量被边”b -> t”限制,最大流量为3
s -> a -> b-> t:边”s -> a”的流量已经被其他路径占满,没有流量
所以,顶点t能够流入的最大水流量为:2 + 3 = 5。
这就是最大流问题。所以,图1的最大流为:2 + 3 = 5。
细心的你可能已经发现:图1的最小割和最大流都为5。是的,经过数学证明可以知道,图的最小割问题可以转换为最大流问题。所以,算法上在处理最小割问题时,往往先转换为最大流问题。
那如何凭直觉解释最小割和最大流存在的这种关系呢?借用Jecvy博客的一句话:1.最大流不可能大于最小割,因为最大流所有的水流都一定经过最小割那些割边,流过的水流怎么可能比水管容量还大呢? 2.最大流不可能小于最小割,如果小,那么说明水管容量没有物尽其用,可以继续加大水流。
1、提取两张图片的sift特征点
2、对两张图片的特征点进行匹配,匹配的过程引用了论文(Distinctive Image Features from Scale-Invariant Keypoints)
3、匹配后,仍有很多错误点,此时使用论文(Accelerated Hypothesis Generation for Multi-Structure Robust Fitting)提到的RANSAC的改进算法进行特征点对的筛选。筛选后的特征点基本能够一一对应。
4、使用DLT算法(Multiple View Geometry p92提到),将剩下的特征点对进行透视变换矩阵的估计。
5、因为得到的透视变换矩阵是基于全局特征点对进行的,即一个刚性的单应性矩阵完成配准。为提高配准的精度,Apap将图像切割成无数多个小方块,对每个小方块的变换矩阵逐一估计。
# -*- coding: utf-8 -*-
from pylab import *
from numpy import *
from PIL import Image
# 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.
"""
# set paths to data folder
featname = ['C:\Python\Pictrue/apap/pic' + str(i + 1) + '.sift' for i in range(5)] # 图片路径记得修改
imname = ['C:\Python\Pictrue/apap/pic' + 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)
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
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)
figure()
imshow(array(im_42, "uint8"))
axis('off')
savefig("example1.png", dpi=300)
show()
固定拍摄位置,移动镜头拍摄多张图片,以中间图片为中心,实现图像的拼接融合(1)测试图片如下:
(2) sift特征匹配:
(3)全景图像拼接:
实验小结:该组实验测试图片拍摄位置选定,在原地转换角度从左到右依次拍摄,因此图片基本处于同一水平。运行结果总体拼接效果不错,拼接痕迹不明显,边缘部分存在部分扭曲。在这个结果上,我认为除了跟拍摄角度有关联之外,还可以在图片纠正上做一些改进处理,可以使得到的结果效果更好。
针对同一场景(视差变化大的场景),更换拍摄位置,实现图像的拼接融合
(1)测试图片如下:
(2)sift特征匹配:
(3)全景图片拼接:
实验小结:
在这组实验中,虽然大体拼接上了,但是在边缘拼接的地方出现了少量黑边。我认为,除了与算法本身有关的情况下,与拍摄角度也是有关系的。同时,拼接效果图上也出现了较为明显的拼接缝,由于两张图片角度相差过大,因此会更明显。
(1)在实验过程中因为图片尺寸过大导致运行超时
解决方法:统一修改图片尺寸,并且一组图片尺寸应相同
(2)拼接图片有大部分黑色
解决方法:修改代码中delta的值,改小以后黑边明显减少,delta值合适时黑边可以消除
(3)运行结果扭曲拼接效果不佳
解决方法:测试图片要按照拍摄顺序从右到左进行命名排列