在同一位置(即图像的照相机位置相同)拍摄的两幅或者多幅图像是单应性相关的。我们经常使用该约束将很多图像缝补起来,拼成一个大的图像来创建全景图像。
RANSAC 是“RANdom SAmple Consensus”(随机一致性采样)的缩写。该方法是用来找到正确模型来拟合带有噪声数据的迭代方法。给定一个模型,例如点集之间的单应性矩阵,RANSAC 基本的思想是,数据中包含正确的点和噪声点,合理的模型应该能够在描述正确数据点的同时摒弃噪声点。
RANSAC 的标准例子:用一条直线拟合带有噪声数据的点集。简单的最小二乘在该例子中可能会失效,但是 RANSAC 能够挑选出正确的点,然后获取能够正确拟合的直线。
我们在任何模型中都可以使用 RANSAC 模块。在使用 RANSAC 模块时,我们只需要在相应 Python 类中实现 fit() 和 get_error() 方法,剩下就是正确地使用 ransac.py。我们这里使用可能的对应点集来自动找到用于全景图像的单应性矩阵。
import sift
featname = ['Univ'+str(i+1)+'.sift' for i in range(5)]
imname = ['Univ'+str(i+1)+'.jpg' for i in range(5)]
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])
显然,并不是所有图像中的对应点对都是正确的。实际上,SIFT 是具有很强稳健性的描述子,能够比其他描述子,例如图像块相关的Harris 角点,产生更少的错误的匹配。但是该方法仍然远非完美。
如图 所示为使用 SIFT 特征自动找到匹配对应。
我们使用 RANSAC 算法来求解单应性矩阵:
class RansacModel(object):
""" 用于测试单应性矩阵的类,其中单应性矩阵是由网站 http://www.scipy.org/Cookbook/RANSAC 上 的 ransac.py 计算出来的 """
def __init__(self,debug=False):
self.debug = debug
def fit(self, data):
""" 计算选取的 4 个对应的单应性矩阵 """
# 将其转置,来调用 H_from_points() 计算单应性矩阵
data = data.T
# 映射的起始点
fp = data[:3,:4]
# 映射的目标点
tp = data[3:,:4]
# 计算单应性矩阵,然后返回
return H_from_points(fp,tp)
def get_error( self, data, H):
""" 对所有的对应计算单应性矩阵,然后对每个变换后的点,返回相应的误差 """
data = data.T
# 映射的起始点
fp = data[:3]
# 映射的目标点
tp = data[3:]
# 变换 fp
fp_transformed = dot(H,fp)
# 归一化齐次坐标
for i in range(3):
fp_transformed[i] /= fp_transformed[2]
# 返回每个点的误差
return sqrt( sum((tp-fp_transformed)**2,axis=0) )
可以看到,这个类包含 fit() 方法。该方法仅仅接受由 ransac.py 选择的4个对应点对(data 中的前4个点对),然后拟合一个单应性矩阵。记住,4个点对是计算单应性矩阵所需的最少数目。由于 get_error() 方法对每个对应点对使用该单应性矩阵,然后返回相应的平方距离之和,因此 RANSAC 算法能够判定哪些点对是正确的,哪些是错误的。在实际中,我们需要在距离上使用一个阈值来决定哪些单应性矩阵是合理的。为了方便使用,将下面的函数添加到文件中:
def H_from_ransac(fp,tp,model,maxiter=1000,match_theshold=10):
""" 使用 RANSAC 稳健性估计点对应间的单应性矩阵 H(ransac.py 为从
http://www.scipy.org/Cookbook/RANSAC 下载的版本)
# 输入:齐次坐标表示的点 fp,tp(3×n 的数组)"""
import ransac
# 对应点组
data = vstack((fp,tp))
# 计算 H,并返回
H,ransac_data = ransac.ransac(data.T,model,4,maxiter,match_theshold,10,
return_all=True)
return H,ransac_data['inliers']
该函数同样允许提供阈值和最小期望的点对数目。最重要的参数是最大迭代次数:程序退出太早可能得到一个坏解;迭代次数太多会占用太多时间。函数的返回结果为单应性矩阵和对应该单应性矩阵的正确点对。
执行下面的代码,你可以将 RANSAC 算法应用于对应点对上:
# 将匹配转换成齐次坐标点的函数
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)
return fp,tp
# 估计单应性矩阵
model = homography.RansacModel()
fp,tp = convert_points(1)
H_12 = homography.H_from_ransac(fp,tp,model)[0] # im1 到 im2 的单应性矩阵
fp,tp = convert_points(0)
H_01 = homography.H_from_ransac(fp,tp,model)[0] # im0 到 im1 的单应性矩阵
tp,fp = convert_points(2) # 注意:点是反序的
H_32 = homography.H_from_ransac(fp,tp,model)[0] # im3 到 im2 的单应性矩阵
tp,fp = convert_points(3) # 注意:点是反序的
H_43 = homography.H_from_ransac(fp,tp,model)[0] # im4 到 im3 的单应性矩阵
在该例子中,图像 2 是中心图像,也是我们希望将其他图像变成的图像。图像 0 和图像 1 应该从右边扭曲,图像 3 和图像 4 从左边扭曲。在每个图像对中,由于匹配是从最右边的图像计算出来的,所以我们将对应的顺序进行了颠倒,使其从左边图像开始扭曲。因为我们不关心该扭曲例子中的正确点对,所以仅需要该函数的第一个输出(单应性矩阵)。
估计出图像间的单应性矩阵(使用 RANSAC 算法),现在我们需要将所有的图像扭曲到一个公共的图像平面上。通常,这里的公共平面为中心图像平面(否则,需要进行大量变形)。一种方法是创建一个很大的图像,比如图像中全部填充 0,使其和中心图像平行,然后将所有的图像扭曲到上面。由于我们所有的图像是由照相机水平旋转拍摄的,因此我们可以使用一个较简单的步骤:将中心图像左边或者右边的区域填充0,以便为扭曲的图像腾出空间。
def panorama(H,fromim,toim,padding=2400,delta=2400):
""" 使用单应性矩阵 H(使用 RANSAC 健壮性估计得出),协调两幅图像,创建水平全景图像。结果
为一幅和 toim 具有相同高度的图像。padding 指定填充像素的数目,delta 指定额外的平移量 """
# 检查图像是灰度图像,还是彩色图像
is_color = len(fromim.shape) == 3
# 用于 geometric_transform() 的单应性变换
def transf(p):
p2 = dot(H,[p[0],p[1],1])
return (p2[0]/p2[2],p2[1]/p2[2])
if H[1,2]<0: # fromim 在右边
print 'warp - right'
# 变换 fromim
if is_color:
# 在目标图像的右边填充 0
toim_t = hstack((toim,zeros((toim.shape[0],padding,3))))
fromim_t = zeros((toim.shape[0],toim.shape[1]+padding,toim.shape[2]))
for col in range(3):
fromim_t[:,:,col] = ndimage.geometric_transform(fromim[:,:,col],
transf,(toim.shape[0],toim.shape[1]+padding))
else:
# 在目标图像的右边填充 0
toim_t = hstack((toim,zeros((toim.shape[0],padding))))
fromim_t = ndimage.geometric_transform(fromim,transf,
(toim.shape[0],toim.shape[1]+padding))
else:
print 'warp - left'
# 为了补偿填充效果,在左边加入平移量
H_delta = array([[1,0,0],[0,1,-delta],[0,0,1]])
H = dot(H,H_delta)
# fromim 变换
if is_color:
# 在目标图像的左边填充 0
toim_t = hstack((zeros((toim.shape[0],padding,3)),toim))
fromim_t = zeros((toim.shape[0],toim.shape[1]+padding,toim.shape[2]))
for col in range(3):
fromim_t[:,:,col] = ndimage.geometric_transform(fromim[:,:,col],
transf,(toim.shape[0],toim.shape[1]+padding))
else:
# 在目标图像的左边填充 0
toim_t = hstack((zeros((toim.shape[0],padding)),toim))
fromim_t = ndimage.geometric_transform(fromim,
transf,(toim.shape[0],toim.shape[1]+padding))
# 协调后返回(将 fromim 放置在 toim 上)
if is_color:
# 所有非黑色像素
alpha = ((fromim_t[:,:,0] * fromim_t[:,:,1] * fromim_t[:,:,2] ) > 0)
for col in range(3):
toim_t[:,:,col] = fromim_t[:,:,col]*alpha + toim_t[:,:,col]*(1-alpha)
else:
alpha = (fromim_t > 0)
toim_t = fromim_t*alpha + toim_t*(1-alpha)
return toim_t
对于通用的 geometric_transform() 函数,我们需要指定能够描述像素到像素间映射
的函数。在这个例子中,transf() 函数就是该指定的函数。该函数通过将像素和 H
相乘,然后对齐次坐标进行归一化来实现像素间的映射。通过查看 H 中的平移量,
我们可以决定应该将该图像填补到左边还是右边。当该图像填补到左边时,由于目
标图像中点的坐标也变化了,所以在“左边”情况中,需要在单应性矩阵中加入平
移。简单起见,我们同样使用 0 像素的技巧来寻找 alpha 图。
现在在图像中使用该操作,函数如下所示:
# 扭曲图像
delta = 2000 # 用于填充和平移
im1 = array(Image.open(imname[1]))
im2 = array(Image.open(imname[2]))
im_12 = warp.panorama(H_12,im1,im2,delta,delta)
im1 = array(Image.open(imname[0]))
im_02 = warp.panorama(dot(H_12,H_01),im1,im_12,delta,delta)
im1 = array(Image.open(imname[3]))
im_32 = warp.panorama(H_32,im1,im_02,delta,delta)
im1 = array(Image.open(imname[j+1]))
im_42 = warp.panorama(dot(H_32,H_43),im1,im_32,delta,2*delta)
1.桌面物品:
可以看出拼接效果并不好,有两三张图片没有拼接进去,没有在拼接图中体现。原因可能是在匹配特征点的时候有一些点乱了,电脑的大部分地方太相似。
2.宿舍室内:
这次是站在原地不动,旋转身体拍摄,可以看出这次的效果不错,特征点匹配的更准确,大部分拼接挺顺畅
下图是对同一个地方进行拍摄,但是拍摄时走动过,虽然图片有很多一样的特征点也拼接失败
3.宿舍室外:
这次的五张图片,第四张图片中放大拍照,产生出来的效果图并不理想
在进行图像拼接时,图像太大会运行时间太长,图片的大小要一样,否则运行会失败。图像模糊,或者一张图里一半太亮一半太暗都会失败。