当前位置: 首页 > news >正文

从开源项目聊鱼眼相机的“360全景拼接”

概述

        写这篇文章的原因完全源于开源项目(GitHub参阅参考文献1)。该项目涵盖了环视系统的较为全貌的制作过程,包含完整的标定、投影、拼接和实时运行流程。该篇文章主要是梳理全景拼接技术中的一些实现细节,并在些地方记录了自己的思考。鉴于该开源项目,后续将计划:(1)基于自动驾驶车辆环视相机标定参数(内外参), 完成360全景拼接demo;(2)这篇文章要跳过的参数标定环节,后面要单独拿出篇幅详析。

从360全景的背景讲起

        百度百科:“360度全景倒车影像,是一套通过车载显示屏幕观看汽车四周360度全景融合,超宽视角,无缝拼接的适时图像信息(鸟瞰图像),了解车辆周边视线盲区,帮助汽车驾驶员更为直观、更为安全地停泊车辆的泊车辅助系统,又叫全景泊车影像系统或全景停车影像系统(有别于市面上把汽车四周画面在显示屏幕上进行分割显示的“全景”系统)。”

源自网络,如侵联删

        说的通俗些,360全景影像给人视觉上呈现的就是鸟瞰图,即从天空望车顶或者地面的俯视图,但俯视效果的实现是通过多个视角的照片拼接而成的。如下图所示,我们观察自己的车就能发现在车的前后左右四个方向上都有一个鱼眼相机,一般左右方向的鱼眼相机因为视野的问题布置在后视镜下方的比较多。

源自网络,如侵联删

        把四个鱼眼相机通过一定的方法拼接到一张图上,再把车辆照片贴到图片中央,就能近似形成完成的全景图像。下图所示的就是开源项目中所使用的的四路鱼眼相机的前、后、左、右方向的照片。

跨过参数标定聊透视变化

        写这篇文章的重点是要梳理如何从鱼眼图片透视变换为全景图片,相机的内外参标定跳过。要完成从单目鱼眼图到鸟瞰图的变化,最重要的一步骤就是完成如下从左图到右图的投影变换,也叫做透视变换。

        从左侧到右侧的变化是通过如下表达式实现的。我这里插一句,有同学将透视矩阵叫做外参,我认为这种说法是牵强的。为了说明这个问题,我们就先在世界坐标系下讨论,其他坐标系一样的道理。

源自网络,如侵联删
  1. 请参上式,基于将像素坐标反投回世界坐标系,需要内外参。所以即便透视后就是世界坐标,透视矩阵能只叫外参吗?
  2. 其次透视转换后得到的坐标定义为世界坐标是否合适?因为每个视角的透视变换是分离的,并不在同一坐标下。
  3. 还有像素坐标到世界坐标得到的是归一化平面下的世界坐标,非严格意义的世界坐标,那透视转换后得到的坐标定义为世界坐标是否准确?

        以上三个问题抛砖引玉,愿与大家探讨。我也计划分别针对“相机内参”和“实车全景拼接”再写两篇文章专门从数据上来验证这个事情。

引用自: Perspective Transformation | TheAILearner
引用自: Perspective Transformation | TheAILearner

        上面的公式解释了透视变换矩阵及其元素物理意义,感兴趣可直接到原文地址拜读。下面对于透视变换的图例描述的也挺形象的

引用自: Perspective Transformation | TheAILearner

        作者针对四路鱼眼图都进行了同样的透视变换。但在实施投影之前还有个重要的工作,就是图像去畸变,并在去畸变的过程中做一些视野范围的校正。

源自网络,如侵联删

        关于CV库去畸变方法中的矫正矩阵:参考上式,如果你推导过内参矩阵对应元素位置数值的话,应该很清楚在对应非零元素位置乘系数的作用就是:完成像素坐标在各轴线的放缩及平移。而在图像大小已定义的情况下,这个变化的作用无异于“裁剪”的效果。这样一来,就完成了去畸变的工作。

def update_undistort_maps(self):new_matrix = self.camera_matrix.copy()# 校正内参new_matrix[0, 0] *= self.scale_xy[0]new_matrix[1, 1] *= self.scale_xy[1]new_matrix[0, 2] += self.shift_xy[0]new_matrix[1, 2] += self.shift_xy[1]width, height = self.resolutionself.undistort_maps = cv2.fisheye.initUndistortRectifyMap(self.camera_matrix,self.dist_coeffs,np.eye(3),new_matrix,(width, height),cv2.CV_16SC2)return selfdef undistort(self, image):print("undistort_maps: ", self.undistort_maps)result = cv2.remap(image, *self.undistort_maps, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT)return result

        进一步的我们开始处理透视变换:透视变换需要提供输入图片及其变换矩阵。变换矩阵的变换可参考源码,基本的道理就是假设真实世界中的标定布的尺寸缩放出几个点的对应坐标数值,将其作为dst、原鱼眼图片对应位置的坐标作为src,通过getPerspectiveTransform方法获取,此处不赘述,后续会写相关文章。 

def project(self, image):result = cv2.warpPerspective(image, self.project_matrix, self.project_shape)return result

        下一步要将各个视角的透视图拼起来:这里需要梳理清楚的主要是各个图的相对位置和方向的关系。由于各个视角的透视变换图都是单独处理的,相当于是摄像头的朝向,所以对于后方摄像头需要做中心变换贴在鸟瞰图的上方,左侧摄像头视图需要左旋放在左侧,右侧如法。

def flip(self, image):if self.camera_name == "front":return image.copy()elif self.camera_name == "back":return image.copy()[::-1, ::-1, :]elif self.camera_name == "left":return cv2.transpose(image)[::-1]else:return np.flip(cv2.transpose(image), 1)

拼接图片后处理

        最后阶段的鸟瞰图的拼接与平滑,不作为文章的重点赘述,但主要涉及到了几种策略:

birdview = BirdView()
Gmat, Mmat = birdview.get_weights_and_masks(projected)
birdview.update_frames(projected)
birdview.make_luminance_balance().stitch_all_parts()
birdview.make_white_balance()
birdview.copy_car_image()
  • 重叠区域中的像素值的加权平均处理
  • 为拼接图像的亮度一致性调整各区域的亮度
  • 通过色彩平衡改善摄像头不同通道的强度不同的问题

参考文献

[1]  https://github.com/neozhaoliang/surround-view-system-introduction

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 说说你对React Router的理解?常用的Router组件有哪些?
  • 【poi导出excel模板——通过建造者模式+策略模式+函数式接口实现】
  • rust实现quic服务端和客户端
  • C# , .netWebApi, WPF 用特性实现类似Java 的Ioc 自动装配@Autowired
  • 汽车之家车型_车系_配置参数数据抓取
  • 前端设计模式之【工厂模式】
  • 2311rust无畏并发.
  • C#基于inpoutx64读写ECRAM硬件信息
  • 机器学习---多分类SVM、支持向量机分类
  • FFMPEG库实现mp4/flv文件(H264+AAC)的封装与分离
  • 中文编程软件视频推荐,自学编程电脑推荐,中文编程开发语言工具下载
  • SDN和NFV笔记
  • k8s docker cgroup驱动问题 —— 筑梦之路
  • HTTPS安全相关-通信安全的四个特性-ssl/tls
  • (论文阅读22/100)Learning a Deep Compact Image Representation for Visual Tracking
  • [ JavaScript ] 数据结构与算法 —— 链表
  • 《微软的软件测试之道》成书始末、出版宣告、补充致谢名单及相关信息
  • 5、React组件事件详解
  • Brief introduction of how to 'Call, Apply and Bind'
  • C学习-枚举(九)
  • gops —— Go 程序诊断分析工具
  • mysql innodb 索引使用指南
  • Storybook 5.0正式发布:有史以来变化最大的版本\n
  • 力扣(LeetCode)357
  • 扑朔迷离的属性和特性【彻底弄清】
  • 删除表内多余的重复数据
  • 使用 Xcode 的 Target 区分开发和生产环境
  • 源码安装memcached和php memcache扩展
  • ​iOS实时查看App运行日志
  • ​学习一下,什么是预包装食品?​
  • #我与Java虚拟机的故事#连载17:我的Java技术水平有了一个本质的提升
  • #在 README.md 中生成项目目录结构
  • ( 10 )MySQL中的外键
  • (4)事件处理——(7)简单事件(Simple events)
  • (android 地图实战开发)3 在地图上显示当前位置和自定义银行位置
  • (Mirage系列之二)VMware Horizon Mirage的经典用户用例及真实案例分析
  • (Redis使用系列) Springboot 使用redis实现接口Api限流 十
  • (定时器/计数器)中断系统(详解与使用)
  • (二)什么是Vite——Vite 和 Webpack 区别(冷启动)
  • (附源码)springboot“微印象”在线打印预约系统 毕业设计 061642
  • (牛客腾讯思维编程题)编码编码分组打印下标题目分析
  • (十三)Java springcloud B2B2C o2o多用户商城 springcloud架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)...
  • (译) 函数式 JS #1:简介
  • (转)全文检索技术学习(三)——Lucene支持中文分词
  • (转载)跟我一起学习VIM - The Life Changing Editor
  • .equal()和==的区别 怎样判断字符串为空问题: Illegal invoke-super to void nio.file.AccessDeniedException
  • .Net core 6.0 升8.0
  • .NET Framework 服务实现监控可观测性最佳实践
  • .NET6 开发一个检查某些状态持续多长时间的类
  • .NET6使用MiniExcel根据数据源横向导出头部标题及数据
  • .net之微信企业号开发(一) 所使用的环境与工具以及准备工作
  • ??Nginx实现会话保持_Nginx会话保持与Redis的结合_Nginx实现四层负载均衡
  • @Autowired @Resource @Qualifier的区别
  • @JSONField或@JsonProperty注解使用
  • [ C++ ] STL_list 使用及其模拟实现