OpenCV完美实现两张图片的全景拼接(详细教程)

news2024/11/18 16:44:27

目录

1,主要步骤

1.1  导入需要的包和模块,并读取两张待拼接的图片,这里我们假设它们为 left.jpg 和 right.jpg。

1.2  创建SIFT检测器

1.3 创建一个基于 FLANN 的匹配器

1.4  筛选过程删除掉一些不合适的匹配点,只保留最好的匹配点

1.5透视变换

1.6  消除重叠的效果,对两张图片进行加权处理

2,代码展示

3,效果展示


应用场景主要有两个方面:

  1. 风景或建筑物的拍摄

对于一些风景或建筑物的拍摄,有时候需要的画面宽度超出了单张图片所能提供的视野范围。这时可以通过拍摄多张图片并将它们拼接成一张更加宽阔的全景图来达到所需的效果。

  1. 科学研究

在一些科学研究中,需要对一定的区域进行高精度测量,例如地形测量、海洋测量等。这时候就需要一些宽视野相机来实现拍摄。但是,由于一张图片所能覆盖的区域有限,因此通常还需要将多张图片拼接成一张更大的全景图像,方便科学家们进行研究和分析。

1,主要步骤

  1. 读入待拼接的图片并调整大小;
  2. 使用 SIFT 或 SURF 算法提取图片的关键点和描述符;
  3. 使用基于 FLANN 的匹配器进行关键点匹配,并筛选出较好的匹配点;
  4. 计算视角变换矩阵,并使用透视变换对右边的图片进行变换;
  5. 消除重叠的效果,对两张图片进行加权处理;
  6. 输出拼接后的结果。

1.1  导入需要的包和模块,并读取两张待拼接的图片,这里我们假设它们为 left.jpgright.jpg

左视图:

右视图:

1.2  创建SIFT检测器

cv2.xfeatures2d.SIFT_create() 创建一个 SIFT 检测器。

也可以选择使用 cv2.SIFT_create()

不过前者是更新的版本,可能会更好一些

然后,在两张图片上分别使用这个检测器进行关键点检测和特征提取,获得关键点集合和描述符集合。

surf=cv2.xfeatures2d.SIFT_create()#可以改为SIFT
#sift = cv2.SIFT_create()
sift = cv2.xfeatures2d.SIFT_create()

kp1,descrip1 = sift.detectAndCompute(imageA,None)
kp2,descrip2 = sift.detectAndCompute(imageB,None)

1.3 创建一个基于 FLANN 的匹配器

调用 cv2.FlannBasedMatcher() 创建一个基于 FLANN 的匹配器,并使用 knnMatch() 处理两张图片的特征描述符,得到最佳匹配。

indexParams = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
searchParams = dict(checks=50)
flann=cv2.FlannBasedMatcher(indexParams,searchParams)
match=flann.knnMatch(descrip1,descrip2,k=2)
good=[]

1.4  筛选过程删除掉一些不合适的匹配点,只保留最好的匹配点

for i,(m,n) in enumerate(match):
    if(m.distance<0.75*n.distance):
        good.append(m)

1.5透视变换

判断满足条件的匹配点数量是否大于阈值 MIN,如果大于,则进行视角变换矩阵的计算,将右边的图片 imageB 对其进行透视变换,得到 warpImg

if len(good) > MIN:
    src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1,1,2)
    ano_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1,1,2)
    M,mask = cv2.findHomography(src_pts,ano_pts,cv2.RANSAC,5.0)
    warpImg = cv2.warpPerspective(imageB, np.linalg.inv(M), (imageA.shape[1]+imageB.shape[1], imageB.shape[0]))
    direct=warpImg.copy()
    direct[0:imageA.shape[0], 0:imageB.shape[1]] =imageA
    simple=time.time()

show('res',warpImg)

同时,将左边的图片覆盖在变换后的图片上,得到 direct。最后,显示结果。

print(rows)
print(cols)
for col in range(0,cols):
    # 开始重叠的最左端
    if imageA[:, col].any() and warpImg[:, col].any():
        left = col
        print(left)
        break

for col in range(cols-1, 0, -1):
    #重叠的最右一列
    if imageA[:, col].any() and warpImg[:, col].any():
        right = col
        print(right)
        break

1.6  消除重叠的效果,对两张图片进行加权处理

根据图片相对位置的不同,左边的图片和右边的图片有可能会在某些列出现重叠部分,为了消除这种不自然的效果,需要实现像素级的混合。首先找到左右图片开始重叠的位置和结束的位置,然后对两张图片进行加权处理,最后将加权后的图片输出。

#加权处理
res = np.zeros([rows, cols, 3], np.uint8)
for row in range(0, rows):
    for col in range(0, cols):
        if not imageA[row, col].any():  # 如果没有原图,用旋转的填充
            res[row, col] = warpImg[row, col]
        elif not warpImg[row, col].any():
            res[row, col] = imageA[row, col]
        else:
            srcImgLen = float(abs(col - left))
            testImgLen = float(abs(col - right))
            alpha = srcImgLen / (srcImgLen + testImgLen)
            res[row, col] = np.clip(imageA[row, col] * (1 - alpha) + warpImg[row, col] * alpha, 0, 255)

warpImg[0:imageA.shape[0], 0:imageA.shape[1]]=res
show('res',warpImg)
final=time.time()
print(final-starttime)

2,代码展示

import cv2
import numpy as np
from matplotlib import pyplot as plt
import time
def show(name,img):
    cv2.imshow(name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
MIN = 10
FLANN_INDEX_KDTREE = 0
starttime = time.time()
img1 = cv2.imread('left.jpg') #query
img2 = cv2.imread('right.jpg') #train
imageA = cv2.resize(img1,(0,0),fx=0.2,fy=0.2)
imageB = cv2.resize(img2,(0,0),fx=0.2,fy=0.2)
surf=cv2.xfeatures2d.SIFT_create()#可以改为SIFT
#sift = cv2.SIFT_create()
sift = cv2.xfeatures2d.SIFT_create()

kp1,descrip1 = sift.detectAndCompute(imageA,None)
kp2,descrip2 = sift.detectAndCompute(imageB,None)
#创建字典
indexParams = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
searchParams = dict(checks=50)
flann=cv2.FlannBasedMatcher(indexParams,searchParams)
match=flann.knnMatch(descrip1,descrip2,k=2)
good=[]
#过滤特征点
for i,(m,n) in enumerate(match):
    if(m.distance<0.75*n.distance):
        good.append(m)

# 当筛选后的匹配对大于10时,计算视角变换矩阵
if len(good) > MIN:
    src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1,1,2)
    ano_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1,1,2)
    M,mask = cv2.findHomography(src_pts,ano_pts,cv2.RANSAC,5.0)
    warpImg = cv2.warpPerspective(imageB, np.linalg.inv(M), (imageA.shape[1]+imageB.shape[1], imageB.shape[0]))
    direct=warpImg.copy()
    direct[0:imageA.shape[0], 0:imageB.shape[1]] =imageA
    simple=time.time()

show('res',warpImg)
rows,cols=imageA.shape[:2]
print(rows)
print(cols)
for col in range(0,cols):
    # 开始重叠的最左端
    if imageA[:, col].any() and warpImg[:, col].any():
        left = col
        print(left)
        break

for col in range(cols-1, 0, -1):
    #重叠的最右一列
    if imageA[:, col].any() and warpImg[:, col].any():
        right = col
        print(right)
        break
#加权处理
res = np.zeros([rows, cols, 3], np.uint8)
for row in range(0, rows):
    for col in range(0, cols):
        if not imageA[row, col].any():  # 如果没有原图,用旋转的填充
            res[row, col] = warpImg[row, col]
        elif not warpImg[row, col].any():
            res[row, col] = imageA[row, col]
        else:
            srcImgLen = float(abs(col - left))
            testImgLen = float(abs(col - right))
            alpha = srcImgLen / (srcImgLen + testImgLen)
            res[row, col] = np.clip(imageA[row, col] * (1 - alpha) + warpImg[row, col] * alpha, 0, 255)

warpImg[0:imageA.shape[0], 0:imageA.shape[1]]=res
show('res',warpImg)
final=time.time()
print(final-starttime)

3,效果展示

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1095489.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[Spring] SpringMVC 简介(三)

目录 九、SpringMVC 中的 AJAX 请求 1、简单示例 2、RequestBody&#xff08;重点关注“赋值形式”&#xff09; 3、ResponseBody&#xff08;经常用&#xff09; 4、为什么不用手动接收 JSON 字符串、转换 JSON 字符串 5、RestController 十、文件上传与下载 1、Respo…

03_51单片机点亮LED灯

51单片机是一种非常常见的单片机型号&#xff0c;广泛应用于各种嵌入式系统和电子设备中。LED灯是一种常见的输出设备&#xff0c;用于显示信息或指示状态。下面是关于51单片机控制LED灯的介绍&#xff1a; 1. 连接LED灯&#xff1a;将LED的正极连接到51单片机的一个I/O引脚&a…

英语——歌曲篇——All Out Of Love

All Out Of Love [Air Supply失落的爱] 歌词 I’m lying alone with my head on the phone Thinking of you till it hurts I know you hurt too but what else can we do Tormented and torn apart I wish I could carry your smile in my heart For times when my life se…

MacOS ventura跳过配置锁

Macbook pro 2021跳配置锁 1.什么是配置锁&#xff1f; 配置锁顾名思义就是美国一些企业和公司向苹果工公司定制采购的机器&#xff0c;这些机器一般供应内部员工使用&#xff0c;这种机器和正常机没有什么区别&#xff0c;也是无锁三网机器&#xff0c;功能和正常机器一摸一…

去雨去雪去雾算法本地实验速度对比

在进行去雨去雪去雾算法的实验过程中&#xff0c;博主使用自己的电脑进行实验&#xff0c;尽管能够成功运行&#xff0c;但速度却属实不尽人意&#xff0c;博主的笔记本显卡是GTX960&#xff0c;显存为4G&#xff0c;在进行实验的过程中&#xff0c;batch-size只能设置为3&…

找不到mfc140u.dll,无法继续执行代码 cdr

在计算机系统中&#xff0c;DLL文件&#xff08;动态链接库&#xff09;是一种非常重要的资源。它们包含了许多可以在程序运行时被调用的代码和数据。然而&#xff0c;当某个特定的DLL文件丢失或损坏时&#xff0c;可能会导致程序无法正常运行。本文将详细介绍五个解决mfc140u.…

flink教程

文章目录 来自于尚硅谷教程1. Flink概述1.1 特点1.2 与SparkStreaming对比 2. Flink部署2.1 集群角色2.2 部署模式2.3 Standalone运行模式2.3.1 本地会话模式部署2.3.2 应用模式 2.4 YARN运行模式2.4.1 会话模式部署2.4.2 应用模式部署 2.5 历史服务 3. 系统架构3.1 并行度3.2 …

线性回归原理

1、 线性回归的原理 1.1 线性回归应用场景 房价预测 销售额度预测 金融:贷款额度预测、利用线性回归以及系数分析因子1.2 什么是线性回归 1.2.1定义与公式 线性回归(Linear regression)是利用回归方程(函数)对一个或多个自变量(特征值)和因变量(目标值)之间关系进行建模的…

在Unity中挂载C#脚本的三种方法

第一种 ①在Project&#xff08;工程&#xff09;窗口的某个文件夹中&#xff08;也可以选择新建在Assets&#xff08;资源根目录&#xff09;中&#xff09;&#xff0c;然后单击鼠标右键&#xff0c;选择Create->C# Script 注意&#xff1a;扩展名在Unity编辑器中是隐藏…

测试中Android与IOS分别关注的点

目录 1、自身不同点 2、测试注重点 3、其他测试点 主要从本身系统的不同点、系统造成的不同点、和注意的测试点做总结 1、自身不同点 研发商&#xff1a;Adroid是google公司做的手机系统&#xff0c;IOS是苹果公司做的手机系统   开源程度&#xff1a;Android是开源的&a…

微信发红包(各种红包类型)-测试用例设计

微信发红包&#xff08;各种红包类型&#xff09;

基于Java的宠物商城管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

Docker逃逸---procfs文件挂载

一、产生原因 将宿主机/proc目录挂载进了容器&#xff0c;而该目录内的/proc/sys/kernel/core_pattern文件是负责进程奔溃时内存数据转储的&#xff0c;当第一个字符是| 管道符时&#xff0c;后面的部分会以命令行的方式进行解析并运行&#xff0c;攻击者可以将恶意文件写入该…

Redis常用数据类型、Redis常用命令

Redis常用数据类型、Redis常用命令&#xff1a; Redis常用数据类型&#xff1a;1. 字符串String 类型2. 哈希hash 类型3. 列表list 类型4. 集合set 类型5. 有序集合sorted set / zset 类型 Redis常用命令&#xff1a;1. 字符串操作命令2. 哈希操作命令3. 列表操作命令4. 集合操…

数据结构--》掌握数据结构中的排序算法

当我们面对海量数据时&#xff0c;如何高效地将其排序是数据结构领域中一个重要的问题。排序算法作为其中的关键部分&#xff0c;扮演着至关重要的角色。 无论你是初学者还是进阶者&#xff0c;本文将为你提供简单易懂、实用可行的知识点&#xff0c;帮助你更好地掌握排序算法在…

如何根据Mapper的Class反向生成SQL文件创建数据库表

微信公众号&#xff1a;大数据高性能计算 问题&#xff1a;已经根据数据库表生成了一堆的Mapper Class&#xff0c;但是当另一个新同学复制项目的时候没有SQL建表文件&#xff0c;导致无法创建数据库&#xff0c;那么这时候我们就需要通过反射以及注解相关的解析自动去生成数据…

Springboot-MyBatisPlue入门

一 创建项目&#xff0c;选择spring boot 初始化&#xff0c;配置相关信息 第五步创建实体类 二 快速开发实体类的jar包--lombok <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12<…

openGauss学习笔记-100 openGauss 数据库管理-管理数据库安全-客户端接入之用SSL进行安全的TCP/IP连接

文章目录 openGauss学习笔记-100 openGauss 数据库管理-管理数据库安全-客户端接入之用SSL进行安全的TCP/IP连接100.1 背景信息100.2 前提条件100.3 注意事项100.4 操作步骤100.5 相关参考 openGauss学习笔记-100 openGauss 数据库管理-管理数据库安全-客户端接入之用SSL进行安…

Vue-3.4Vuex

Vuex概述 是什么&#xff1a; vuex是一个vue的状态管理工具&#xff0c;状态就是数据。 Vuex是一个插件&#xff0c;可以帮我们管理vue通用的数据&#xff08;多组件共享的数据&#xff09; 例如&#xff1a;购物车数据、个人信息数据 场景&#xff1a; 1&#xff09;某个…

暴力递归转动态规划(十)

题目 给定一个二维数组matrix[][]&#xff0c;一个人必须从左上角出发&#xff0c;最终到达右下角&#xff0c;沿途只可以向下或者向右走&#xff0c;沿途的数字都累加就是距离累加和。返回最小距离累加和。 这道题中会采用压缩数组的算法来进行优化 暴力递归 暴力递归方法的整…