创建全景图

  • 前言
  • 一、全景拼接原理
    • 1. 提取图像的特征和匹配
    • 2. 将匹配转化成齐次坐标
    • 3. 估计单应性矩阵(RANSAC算法)
    • 4. 拼接图像
  • 二、实验结果及分析
    • 1. 全代码
    • 2. 实现
      • 2.1 第一组
      • 2.2 第二组
      • 2.3 第三组

前言

在同一位置拍摄的两幅或者多幅图像是单应性相关的。
我们经常使用该约束将很多图像缝补起来,拼成一个大的图像来创建图像。接下来,我们将探讨如何创建全景图像。


一、全景拼接原理

全景拼接的基础流程如下:

(1)针对同一场景拍摄系列图像

(2)提取图像的特征和匹配

(3)将匹配转化成齐次坐标点

(4)估计单应性矩阵

(5)拼接图像

1. 提取图像的特征和匹配

SIFT算法实现特征匹配主要有三个流程
1、提取关键点;
2、对关键点附加详细的信息(局部特征),即描述符;
3、通过特征点(附带上特征向量的关键点)的两两比较找出相互匹配的若干对特征点,建立景物间的对应关系。

2. 将匹配转化成齐次坐标

3. 估计单应性矩阵(RANSAC算法)

RANSAC 是“RANdom SAmple Consensus”(随机一致性采样)的缩写。该方法是用来找到正确模型来拟合带有噪声数据的迭代方法。给定一个模型,例如点集之间的单应性矩阵,RANSAC 基本的思想是,数据中包含正确的点和噪声点,合理的模型应该能够在描述正确数据点的同时摒弃噪声点。 即从一组数据集(包含噪声点的数据集)中,能够从中挑选出正确的点,获取能够正确拟合的参数模型。

RANSAC的求解过程如下:

(1)随机选择4对匹配特征对应点对
(2)根据DLT计算单应性矩阵H(唯一解)
(3)对所有匹配点,计算映射误差(对每个对应点使用该单应性矩阵,然后返回平方距离之和)
(4)根据误差阈值,确定正常值inliers(小于阈值的点看做正确点,否则认为是错误的点)
重复步骤(1)-(4)
(5)针对最大inliers集合,重新计算单应性矩阵H

RANSC其实是DLT(Direct Linear Transformation,直接线性变换)的优化。DLT算法是将给定的4个或更多个对应点方程的系数堆叠到一个矩阵中,使用SVD(奇异值分解)算法求得H的最小二乘解。但是,值得注意的是并非所有的对应点对都是正确的

我们使用 RANSAC 算法来求解单应性矩阵,首先需要将下面模型类添加到homography.py文件中:

class RansacModel(object):""" 用于测试单应性矩阵的类,其中单应性矩阵是由网站http://www.scipy.org/Cookbook/RANSAC 上的ransac.py 计算出来的"""def __init__(self, debug=False):self.debug = debugdef 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:]# 变换fpfp_transformed = dot(H, fp)# 归一化齐次坐标# for i in range(3):#     fp_transformed[i] /= fp_transformed[2]fp_transformed = normalize(fp_transformed)# 返回每个点的误差return sqrt(sum((tp - fp_transformed) ** 2, axis=0))

可以看到,这个类包含 fit() 方法。该方法仅仅接受由 ransac.py 选择的4个对应点对(data 中的前 4 个点对),然后拟合一个单应性矩阵。记住,4 个点对是计算单应性矩阵所需的最少数目。由于 get_error() 方法对每个对应点对使用该单应性矩阵,然后返回相应的平方距离之和,因此 RANSAC 算法能够判定哪些点对是正确的,哪些是错误的。在实际中,我们需要在距离上使用一个阈值来决定哪些单应性矩阵是合理的。为了方便使用,将下面的函数添加到 homography.py 文件中:

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 的数组)"""from PCV.tools 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']

该函数允许提供阈值和最小期望的点对数目。最重要的参数是最大迭代次数:程序退出太早可能得到一个坏解;迭代次数太多会占有太多时间。函数的返回结果为单应性矩阵和对应该单应性矩阵的正确点对。

# 将匹配转换成齐次坐标点的函数
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 elsewherefp = vstack([fp[1], fp[0], fp[2]])tp = vstack([tp[1], tp[0], tp[2]])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 的单应性矩阵

在每个图像对中,由于匹配是从最右边的图像计算出来的,所以我们将对应的顺序进行了颠倒,使其从左边图像开始扭曲。图像编号顺序应逆序编号。

4. 拼接图像

估计出单应性矩阵H后,我们需要将所有图像扭曲到一个公共的图像平面上。通常,这里的公共平面为中心平面(否则,需要进行大量变形)。一种方法是创建一个很大的图像,比如将平面中全部填充为0,使其和中心图像平行,然后将所有的图像扭曲到上面。其基本步骤如下:
(1)实现像素间的映射(计算像素和和单应性矩阵H相乘,然后对齐次坐标进行归一化)
(2)判断图像填补位置(查看H中的平移量,左边为正,右边为负)
(3)在单应性矩阵中加入平移量,进行alpha图填充

由于我们所有的图像是由照相机水平旋转拍摄的,因此我们可以使用一个较简单的步骤:将中心图像左边或者右边的区域填充 0,以便为扭曲的图像腾出空间。将下面的代码添加到 warp.py 文件中:

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')# 变换fromimif is_color:# 在目标图像的右边填充0toim_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:# 在目标图像的右边填充0toim_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:# 在目标图像的左边填充0toim_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:# 在目标图像的左边填充0toim_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]), "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)

二、实验结果及分析

1. 全代码

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 siftnp.seterr(invalid='ignore')"""
This is the panorama example from section 3.3.
"""# 设置数据文件夹的路径
featname = ['D:/JMU/大三下/计算机视觉/实验/全景/pic/pic2/' + str(i + 1) + '.sift' for i in range(5)]
imname = ['D:/JMU/大三下/计算机视觉/实验/全景/pic/pic2/' + str(i + 1) + '.jpg' for i in range(5)]# 提取特征并匹配使用sift算法
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])# 可视化匹配
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)# 将匹配转换成齐次坐标点的函数
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 elsewherefp = vstack([fp[1], fp[0], fp[2]])tp = vstack([tp[1], tp[0], tp[2]])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 的单应性矩阵# 扭曲图像
delta = 2000  # 用于填充和平移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')
show()

2. 实现

以下图片按从右到左拍摄顺序编号

2.1 第一组

测试图:

阴天室外

图4-1 zhongshan1.jpg 图4-2 zhongshan2.jpg 图4-3 zhongshan3.jpg 图4-4 zhongshan4.jpg 图4-5 zhongshan5.jpg

运行结果:

图4-6:其中一张特征点匹配图
从图中看出,检测点主要集中在建筑上,对草地、树木、天空的匹配效果差

图4-7:全景图1
如图所见,图像曝光不同,在单个图像的边界上存在边缘效应。中间建筑拼接缝处有一点重影错位

2.2 第二组

测试图:

图4-8 1.jpg 图4-9 2.jpg 图4-10 3.jpg 图4-11 4.jpg 图4-12 5.jpg

运行结果:

图4-13:全景图2
这里拼接效果还是较好的,右边部分由于相机转向云中光线折射效果不同有较为明显的拼接缝,中间拼接色差过渡可见。

2.3 第三组

测试图:
室内在两个窗口拍窗外

图4-14 1.jpg 图4-15 2.jpg 图4-16 3.jpg

图4-15和4-16在一个窗口拍摄,图4-14在另一个窗口拍摄
运行结果:

图4-17:全景图3

很明显的看出这张图的拼接缝处,屋顶建筑出现了错位重叠

计算机视觉学习blog(四)——创建全景图相关推荐

  1. 如何用mysql创建orders表_MySQL学习十四创建和操纵表

    摘要: 本篇博客仅作为笔记,如有侵权,请联系,立即删除(网上找博客学习,然后手记笔记,因纸质笔记不便保存,所以保存到网络笔记). 本博讲述表的创建.更改和删除的基本知识. 一.创建表 MySQL不仅用 ...

  2. 计算机视觉的深度学习实战四:图像特征提取

    更多精彩内容请关注微信公众号:听潮庭. 计算机视觉的深度学习实战四:图像特征提取 综述: 颜色特征 量化颜色直方图.聚类颜色直方图 几何特征 Edge,Corner,Blob 基于关键点的特征描述子 ...

  3. python计算机视觉学习第三章——图像到图像的映射

    目录 引言 一. 单应性变换 1.1 直接线性变换算法 1.2 仿射变换 二. 图像扭曲 2.1 图像中的图像 2.2 分段仿射扭曲 2.2 图像配准 三.创建全景图 3.1 RANSAC(随机一致性 ...

  4. Java IO流学习总结四:缓冲流-BufferedReader、BufferedWriter

    Java IO流学习总结四:缓冲流-BufferedReader.BufferedWriter 转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/det ...

  5. IOS学习笔记(四)之UITextField和UITextView控件学习

    IOS学习笔记(四)之UITextField和UITextView控件学习(博客地址:http://blog.csdn.net/developer_jiangqq) Author:hmjiangqq ...

  6. python应用学习(四)——wordcloud生成词云

    python应用学习(四)--wordcloud生成词云 前言 一.准备 二.导入库 三.基本功能实现 四.爬取书评并制作词云 最后 前言 朋友最近在公众号发一些好书好剧推荐,然后我想着帮帮忙,做一个 ...

  7. 『03网络』 实验一:多功能浏览器的使用和个人Blog的创建和使用

    实验一:多功能浏览器的使用和个人Blog的创建和使用<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office: ...

  8. Maven学习总结(四)——Maven核心概念

    2019独角兽企业重金招聘Python工程师标准>>> Maven学习总结(四)--Maven核心概念 一.Maven坐标 1.1.什么是坐标? 在平面几何中坐标(x,y)可以标识平 ...

  9. 【转】MyBatis学习总结(四)——解决字段名与实体类属性名不相同的冲突

    [转]MyBatis学习总结(四)--解决字段名与实体类属性名不相同的冲突 在平时的开发中,我们表中的字段名和表对应实体类的属性名称不一定都是完全相同的,下面来演示一下这种情况下的如何解决字段名与实体 ...

  10. 四、Android学习第四天——JAVA基础回顾(转)

    (转自:http://wenku.baidu.com/view/af39b3164431b90d6c85c72f.html) 四.Android学习第四天--JAVA基础回顾 这才学习Android的 ...

最新文章

  1. 网络推广产品浅析网站SEO文章更新要注意哪些因素?
  2. php fpm安装curl后,nginx出现connect() to unix:/var/run/php5-fpm.sock failed (13: Permission denied)的错误...
  3. mysql yn 字段类型_mysql常用数据类型
  4. snort create_mysql_入侵检测系统Snort+Base安装
  5. windows update更新失败 安全模式进不去
  6. apt-get常见错误
  7. 第八章、面向对象设计
  8. 通过一个模拟程序让你明白WCF大致的执行流程
  9. flask web开发是前端还是后端_后端开发该不该学前端开发?
  10. 微波心得2——阻抗匹配
  11. 基于产生式系统的野人渡河问题求解
  12. html在线围棋对战,闲情奕趣(基于html5的围棋应用)
  13. 自适应鲁棒控制(ARC)实例推导(手写超详细)
  14. 对TMS320F28335存储空间的理解
  15. CSS3的clac()函数无效,警告提示“invalid property value”
  16. 超详细的PS抠图方法
  17. Windwos磁盘管理工具diskpart
  18. 计算机网络安全中的审计,什么是网络安全审计
  19. 一个人怎样才算见过世面?
  20. html中保留空格及换行

热门文章

  1. PMI-ACP(103:17-56)
  2. 最受程序员欢迎的公司榜单发布
  3. html中文本框中的空,互联网常识:如何判断html中文本框内容是否为空
  4. iTween基础之Punch(摇晃)
  5. SWUST软件技术基础实验(1)栈的实现和应用(修订版)
  6. 服务器死机后重启进不了系统,电脑死机重启了下,服务器就出问题了,麻烦高手给看看。...
  7. FL Studio Producer Edition 21.2.0. Build 3842破解激活版下载,Fl Studio 2024中文破解版补丁
  8. jenkins-使用继承实现基于 kubernetes Pod 的多容器的多构建环境的 Jenkins Slave
  9. 基于eclipse的android项目实战—博学谷(十七)播放记录界面
  10. 函数公寓高泽龙出席长租公寓开发与运营研讨峰会