计算机视觉(三)--- 图像到图像的映射(全景拼接)

2023-05-16

目录

1.基本介绍 

2.RANSAC

3.单应性矩阵估计

4.全景图像


1.基本介绍 

引言

        众所周知,在我们拍摄风光摄影时,广角镜头是使用频率最高的镜头,特别是拍摄那些波澜壮阔的大场景风光。而且镜头可谓是越广越好,恨不得将眼前看到的所有美景都纳入到画面中去。这个时候,常规的超广角16mm焦段就有点不够用了,所以催生了14mm、12mm甚至更广的超广角镜头产生。但是这类超广角镜头,通常都是“灯泡头”的设计,为风光滤镜的使用增加了难度和成本负担。而且,即使是如此之广的镜头,也不完全能够满足我们的需求。因此,我们需要“曲线救国”,使用一个名为 “全景拼接” 的技术来达到我们的目的。

 

 

        图像拼接技术就是将数张有重叠部分的图像(可能是不同时间、不同视角或者不同传感器获得的)拼成一幅无缝的全景图或高分辨率图像的技术。图像拼接在医学成像、计算机视觉、卫星数据、军事目标自动识别等领域具有重要意义。图像拼接的输出是两个输入图像的并集。图像配准(image alignment)和图像融合是图像拼接的两个关键技术。图像配准是图像融合的基础,而且图像配准算法的计算量一般非常大,因此图像拼接技术的发展很大程度上取决于图像配准技术的创新。早期的图像配准技术主要采用点匹配法,这类方法速度慢、精度低,而且常常需要人工选取初始匹配点,无法适应大数据量图像的融合。图像拼接的方法很多,不同的算法步骤会有一定差异,但大致的过程是相同的。

基础流程

① 针对某个场景拍摄多张/序列图像

② 计算第二张图像与第一张图像之间的变换关 系

③ 将第二张图像叠加到第一张图像的坐标系中

④ 变换后的融合/合成

⑤ 在多图场景中,重复上述过程

2.RANSAC

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

        举个简单的例子,假设观测数据中包含局内点和局外点,其中局内点近似的被直线所通过,而局外点远离于直线。简单的最小二乘法不能找到适应于局内点的直线,原因是最小二乘法尽量去适应包括局外点在内的所有点。相反,RANSAC能得出一个仅仅用局内点计算出模型,并且概率还足够高

算法基本思想和流程

RANSAC是通过反复选择数据集去估计出模型,一直迭代到估计出认为比较好的模型。
具体的实现步骤可以分为以下几步:

  1. 选择出可以估计出模型的最小数据集;(对于直线拟合来说就是两个点,对于计算Homography矩阵就是4个点)
  2. 使用这个数据集来计算出数据模型;
  3. 将所有数据带入这个模型,计算出“内点”的数目;(累加在一定误差范围内的适合当前迭代推出模型的数据)
  4. 比较当前模型和之前推出的最好的模型的“内点“的数量,记录最大“内点”数的模型参数和“内点”数;
  5. 重复1-4步,直到迭代结束或者当前模型已经足够好了(“内点数目大于一定数量”)。

RANSAC的优点是它能鲁棒的估计模型参数。例如,它能从包含大量局外点的数据集中估计出高精度的参数。RANSAC的缺点是它计算参数的迭代次数没有上限;如果设置迭代次数的上限,得到的结果可能不是最优的结果,甚至可能得到错误的结果

运行结果

3.单应性矩阵估计

单应性(Homography)变换。可以简单的理解为它用来描述物体在世界坐标系和像素坐标系之间的位置映射关系。对应的变换矩阵称为单应性矩阵。

单应性在计算机视觉领域是一个非常重要的概念,它在图像校正、图像拼接、相机位姿估计、视觉SLAM等领域有非常重要的作用。

 

什么是齐次坐标系?为什么要用齐次坐标系? - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/373969867

 

单应性Homograph估计:从传统算法到深度学习 - 知乎 (zhihu.com)icon-default.png?t=M276https://zhuanlan.zhihu.com/p/74597564

单应矩阵描述的就是同一个平面的点在不同图像之间的映射关系

 

代码实践---更换广告牌

import cv2 as cv
import numpy as np


# 鼠标操作,鼠标选中源图像中需要替换的位置信息
def mouse_action(event, x, y, flags, replace_coordinate_array):
    cv.imshow('collect coordinate', img_dest_copy)
    if event == cv.EVENT_LBUTTONUP:
        # 画圆函数,参数分别表示原图、坐标、半径、颜色、线宽(若为-1表示填充)
        # 这个是为了圈出鼠标点击的点
        cv.circle(img_dest_copy, (x, y), 2, (0, 255, 255), -1)

        # 用鼠标单击事件来选择坐标
        # 将选中的四个点存放在集合中,在收集四个点时,四个点的点击顺序需要按照 img_src_coordinate 中的点的相对位置的前后顺序保持一致
        print(f'{x}, {y}')
        replace_coordinate_array.append([x, y])


if __name__ == '__main__':
    # 首先,加载待替换的源图像,并获得该图像的长度等信息,cv.IMREAD_COLOR 表示加载原图
    img_src = cv.imread('../images/JMU.jpg', cv.IMREAD_COLOR)
    h, w, c = img_src.shape
    # 获得图像的四个边缘点的坐标
    img_src_coordinate = np.array([[x, y] for x in (0, w - 1) for y in (0, h - 1)])
    print(img_src_coordinate)
    # cv.imshow('replace', replace)

    print("===========================")

    # 加载目标图像
    img_dest = cv.imread('../images/img_dest.webp', cv.IMREAD_COLOR)
    # 将源数据复制一份,避免后来对该数据的操作会对结果有影响
    img_dest_copy = np.tile(img_dest, 1)

    # 源图像中的数据
    # 定义一个数组,用来存放要源图像中要替换的坐标点,该坐标点由鼠标采集得到
    replace_coordinate = []
    cv.namedWindow('collect coordinate')
    cv.setMouseCallback('collect coordinate', mouse_action, replace_coordinate)
    while True:
        # 当采集到四个点后,可以按esc退出鼠标采集行为
        if cv.waitKey(20) == 27:
            break

    print(replace_coordinate)

    replace_coordinate = np.array(replace_coordinate)
    # 根据选中的四个点坐标和代替换的图像信息完成单应矩阵
    matrix, mask = cv.findHomography(img_src_coordinate, replace_coordinate, 0)
    print(f'matrix: {matrix}')
    perspective_img = cv.warpPerspective(img_src, matrix, (img_dest.shape[1], img_dest.shape[0]))
    cv.imshow('img', perspective_img)

    # cv.imshow('threshold', threshold_img)
    # 降噪,去掉最大或最小的像素点
    retval, threshold_img = cv.threshold(perspective_img, 0, 255, cv.THRESH_BINARY)
    # 将降噪后的图像与之前的图像进行拼接
    cv.copyTo(src=threshold_img, mask=np.tile(threshold_img, 1), dst=img_dest)
    cv.copyTo(src=perspective_img, mask=np.tile(perspective_img, 1), dst=img_dest)
    cv.imshow('result', img_dest)
    cv.waitKey()
    cv.destroyAllWindows()

 

运行结果

书本上样例代码

import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

featname = ['../images/chp3/sift/JMU' + str(i + 1) + '.sift' for i in range(5)]
imname = ['../images/chp3/JMU' + str(i + 1) + '.jpg' for i in range(5)]

# 显示5张校园照片
plt.figure(figsize=(15, 8))
for i in range(5):
    im = np.array(Image.open(imname[i]))
    plt.subplot(2, 3, i + 1)
    plt.imshow(im)
    plt.title('Univ' + str(i + 1) + '.jpg')
    plt.axis('off')
plt.show()

class RansacModel(object):
    """ Class for testing homography fit with ransac.py from
        http://www.scipy.org/Cookbook/RANSAC"""
    
    def __init__(self,debug=False):
        self.debug = debug
        
    def fit(self, data):
        """ Fit homography to four selected correspondences. """
        
        # transpose to fit H_from_points()
        data = data.T
        
        # from points
        fp = data[:3,:4]
        # target points
        tp = data[3:,:4]
        
        # fit homography and return
        return H_from_points(fp,tp)
    
    def get_error( self, data, H):
        """ Apply homography to all correspondences, 
            return error for each transformed point. """
        
        data = data.T
        
        # from points
        fp = data[:3]
        # target points
        tp = data[3:]
        
        # transform fp
        fp_transformed = dot(H,fp)
        
        # normalize hom. coordinates
        fp_transformed = normalize(fp_transformed)
                
        # return error per point
        return sqrt( sum((tp-fp_transformed)**2,axis=0) )

def H_from_ransac(fp,tp,model,maxiter=1000,match_theshold=10):
    """ Robust estimation of homography H from point 
        correspondences using RANSAC (ransac.py from
        http://www.scipy.org/Cookbook/RANSAC).
        
        input: fp,tp (3*n arrays) points in hom. coordinates. """
    
    import ransac
    
    # group corresponding points
    data = vstack((fp,tp))
    
    # compute H and return
    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)
    return fp,tp
model = homography.RansacModel()
 
fp,tp = convert_points(1)
H_12 = homography.H_from_ransac(fp,tp,model)[0] 
 
fp,tp = convert_points(0)
H_01 = homography.H_from_ransac(fp,tp,model)[0] 
 
tp,fp = convert_points(2) 
H_32 = homography.H_from_ransac(fp,tp,model)[0] 
 
tp,fp = convert_points(3) 
H_43 = homography.H_from_ransac(fp,tp,model,match_threshold=3000)[0] 

4.全景图像

        估计出图像间的单应性矩阵(使用 RANSAC 算法),现在我们需要将所有的图像扭 曲到一个公共的图像平面上。通常,这里的公共平面为中心图像平面(否则,需要 进行大量变形)。一种方法是创建一个很大的图像,比如图像中全部填充 0,使其和 中心图像平行,然后将所有的图像扭曲到上面。由于我们所有的图像是由照相机水平 旋转拍摄的,因此我们可以使用一个较简单的步骤:将中心图像左边或者右边的区域填充0,以便为扭曲的图像腾出空间。 

def panorama(H,fromim,toim,padding=2400,delta=2400):
    """ Create horizontal panorama by blending two images 
        using a homography H (preferably estimated using RANSAC).
        The result is an image with the same height as toim. 'padding' 
        specifies number of fill pixels and 'delta' additional translation. """ 
    
    # check if images are grayscale or color
    is_color = len(fromim.shape) == 3
    
    # homography transformation for 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 is to the right
        print('warp - right')
        # transform fromim
        if is_color:
            # pad the destination image with zeros to the right
            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:
            # pad the destination image with zeros to the right
            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')
        # add translation to compensate for padding to the left
        H_delta = array([[1,0,0],[0,1,-delta],[0,0,1]])
        H = dot(H,H_delta)
        # transform fromim
        if is_color:
            # pad the destination image with zeros to the left
            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:
            # pad the destination image with zeros to the left
            toim_t = hstack((zeros((toim.shape[0],padding)),toim))
            fromim_t = ndimage.geometric_transform(fromim,
                                    transf,(toim.shape[0],toim.shape[1]+padding))
    
    # blend and return (put fromim above toim)
    if is_color:
        # all non black pixels
        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

im1 = np.array(Image.open(imname[1]))
delta = im1.shape[1] # 領域追加と水平移動量
 
im2 = np.array(Image.open(imname[2]))
im_12 = warp.panorama(H_12,im1,im2,delta,delta)
 
im1 = np.array(Image.open(imname[0]))
im_02 = warp.panorama(np.dot(H_12,H_01),im1,im_12,delta,delta)
 
im1 = np.array(Image.open(imname[3]))
im_32 = warp.panorama(H_32,im1,im_02,delta,delta)
 
im1 = np.array(Image.open(imname[4]))
im_42 = warp.panorama(np.dot(H_32,H_43),im1,im_32,delta,2*delta)
 

不懂啥意思 但是最后结果还是出来了 

运行结果

 

 

我们还是可以看到明显的拼接缝,这是由于拍摄图片的曝光程度有所不同而造成的 ,同时,由于 这组照片,角度的方位变化有些大,所以效果还不是很好。

后续多拍几组照片进行测试,暂时先这样吧!

RANSAC算法详解(附Python拟合直线模型代码) - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/62238520

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

计算机视觉(三)--- 图像到图像的映射(全景拼接) 的相关文章

  • 【亲测有效】树莓派4B安装realsense(Intel深度摄像头)

    第一步尝试通过pip下载 xff0c 发现不能下载 pip span class token function install span pyrealsense2 pip中的pyrealsense2只能下载给X86结构的计算机 xff0c
  • 驼峰规则

    驼峰规则包含两种 xff1a 大驼峰和小驼峰 大驼峰 指我们在命名的时候往往采用第一个字母大写 xff0c 比如Animal 这种命名形式常用于类名或函数名 小驼峰 指我们在命名是往往采用中间字母大写 xff0c 比如setName 这种命
  • 八皇后问题(回溯法)

    目录 什么是八皇后 八皇后问题怎么解决 xff1f 什么是回溯法 回溯法的模板 八皇后问题的核心代码 判断皇后位置是否可行 总体实现代码 每日一句 xff1a 种一棵树的最好时间是十年前 xff0c 其次是现在 什么是八皇后 八皇后问题 x
  • 淘宝搜索页面爬取数据

    淘宝搜索页面爬取数据 1 首先导入库 span class token keyword import span requests span class token keyword import span json 2 主函数 span cl
  • Android进程保活 --- 守护进程(code)

    1 守护进程 xff1a 一个在后台运行并且不受任何终端控制的进程 可以用来给其他应用拉起 xff0c 保活 import android app Service import android content ComponentName i
  • 基于树莓派的追光系统(python)

    目录 前言 一 材料 二 硬件 控制逻辑 1 主设备的准备 1 启用树莓派的i2c设备 2 安装python smbus 2 从设备的准备 1 BH1750 2 L298N驱动芯片 3 云台的准备 1 增加电机固定模块 2 增加bh1750
  • pytorch优化器(optimizer)中params参数详细介绍

    这里先给出使用的一个小型网络 xff08 自己瞎定义的一个网络 xff09 xff0c 后面使用的model就是这里定义的一个小型的网络 xff1a 定义网络 class Test nn Module def init self super
  • [下面的框架可能不正确和/或缺失,没有为 ntdll.dll 加载符号]

    96 96 96 cpp 在这里插入代码片 之前老师出现这些问题 之后我改了realse模式 依旧不行 我经过一夜的思考 xff0c 发现这个和我的代码 没有关系 我修改了程序内部的一些char 然后重新启动realse 就没有这个了 在这
  • 操作系统-处理机调度、进程调度的时机、切换与过程、方式、调度算法的评价指标、调度算法

    文章目录 基本概念三个层次高级调度 作业调度 中级调度 内存调度 低级调度 进程调度 三层调度的联系 对比补充知识时机什么时候需要进程调度什么时候不能进行进程调度临界区与内核程序临界区 切换与过程 34 狭义的调度 34 与 34 切换 3
  • QGC地面站配置PX4Flow光流传感器

    打开地面站 xff0c 进入到参数设置里面 xff0c 查询 EKF2 AID MASK xff0c 在px4中使能px4flow xff0c 设置为2即关闭GPS打开光流 2 查询 SENS EN MB12XX 在px4中使能px4flo
  • Git如何创建一条分支,并且进行分支的切换

    核心指令 xff1a git checkout xx 下面讲解怎么创建 可以看到 xff0c 我们当前的处于master分支 输入 git branch dev xff08 创建一个dev分支 xff09 这样已经是创建成功了 可以输入gi
  • Bosch SMI810 IMU传感器芯片驱动

    Bosch SMI810 IMU传感器芯片驱动 文章目录 Bosch SMI810 IMU传感器芯片驱动一 总体特点二 SPI通信三 数据处理四 寄存器设置和代码编写 一 总体特点 1 smi8xx家族的传感器分为 xff0c 陀螺仪 43
  • 村田 IMU SCC2000系列芯片驱动

    村田 IMU SCC2000系列芯片驱动 文章目录 村田 IMU SCC2000系列芯片驱动一 总体特点二 启动时序和逻辑三 SPI通信和数据读取四 数据处理 一 总体特点 1 本次具体的型号是村田SCC2130系 xff0c IMU有1轴
  • Vue模板的使用,vue中使用js表达式

    1 v 属性 使用方法和需要注意的点 span class token tag span class token tag span class token punctuation lt span template span span cla
  • ROS多机通信主机接收不到从机的消息

    关一下防火墙试试 xff1a sudo ufw disable 另 xff1a 检查防火墙是否关闭 xff1a sudo ufw status 另 xff1a 其实ROS多机通信只要设置好ROS MASTER URL和ROS HOSTNAM
  • 单个象棋棋子图片!png

    之前做完项目直接全删了 xff0c 结果帅竟然忘了上传 这回重新扣个帅效果差了好多 xff0c 大家凑合用吧
  • numpy中的cov以及参数rowvar

    numpy中计算协方差利用cov方法 xff0c 如何计算协方差 xff1f 利用这个公式 xff0c 可以求得两个矩阵的协方差 xff0c 举个例子 xff1a 这里 X Y X Y X Y 分别对应着矩阵
  • size.width>0 && size.height>0 in function ‘cv::imshow‘

    遇到报错 xff1a cv2 error OpenCV 3 4 2 C Miniconda3 conda bld opencv suite 1534379934306 work modules highgui src window cpp
  • windows安装ubuntu双系统

    因为要学习机器人 xff0c 老师要求安装ubuntu和ros系统 xff0c 安装第一次踩了雷不太成功 xff0c 第二次安装成功了ubuntu21 04但没有对应的ros系统 xff0c 因此在此向大家安利 安装ubuntu18 04比
  • pytorch

    pytorch基础 1 Tensor数据类型 创建tensor时 xff0c 默认类型为torch FloatTensor xff08 32位浮点 xff09 a span class token operator 61 span torc

随机推荐