python 一份简单的车辆环视全景系统实现图像拼接缝融合
对于拼接后得到的俯视图,在拼接相邻两图拼接处存在一条拼接缝,如果不作处理,在视频播放过程中在拼接缝处会有明显的跳跃情况。由于我们相邻的两幅图之间有重叠区域,我们可以利用重叠区进行平滑过渡处理来消除拼接缝。
消除拼接缝的方法有多种,主要的两种有:1、中值滤波法消除拼接缝。2、利用加权平均融合消除拼接缝。对于拼接缝的消除有两点要求:一是拼接区域过渡平滑,二是拼接区域亮度跳跃变化不大。
在实际中,常用的方法是采用加权平均融合的方法来消除拼接缝:如图2-8所示为拼接缝融合算法示意图,假设两相邻图像中的图一像素点为、图二像素点为则有:
关于车辆的全景环视系统网上已经有很多的资料,然而几乎没有可供参考的代码,这一点对入门的新人来说非常不友好。这个项目的目的就是介绍全景系统的原理,并给出一份可以实际运行的、基本要素齐全的 Python 实现供大家参考。环视全景系统涉及的知识并不复杂,只需要读者了解相机的标定、透视变换,并懂得如何使用 OpenCV。项目代码维护在 github 上。
这个程序最初是在一辆搭载了一台 AGX Xavier 的无人小车上开发的,运行效果如下:
小车上搭载了四个 USB 环视鱼眼摄像头,相机传回的画面分辨率为 640x480,图像首先经过畸变校正,然后在射影变换下转换为对地面的鸟瞰图,最后拼接起来经过平滑处理后得到了上面的效果。全部过程在 CPU 中进行处理,整体运行流畅。
后来我把代码重构以后移植到一辆乘用车上 (处理器是同型号的 AGX),得到了差不多的效果:
这个版本使用的是四个 960x640 的 csi 摄像头,输出的全景图分辨率为 1200x1600,在不进行亮度均衡处理时全景图处理线程运行速度大约为 17 fps,加入亮度均衡处理后骤降到只有 7 fps。我认为适当缩小分辨率的话 (比如采用 480x640 的输出可以将像素个数降低到原来的 1/6) 应该也可以获得流畅的视觉效果。
注:画面中黑色的部分是相机投影后出现的盲区,这是因为前面的相机为了避开车标的部位安装在了车头左侧且角度倾斜,所以视野受到了限制。想象一个人歪着脖子还斜着眼走路的样子 …
这个项目的实现比较粗糙,仅作为演示项目展示生成环视全景图的基本要素,大家领会精神即可。我开发这个项目的目的是为了在自动泊车时实时显示车辆的轨迹,同时也用来作为我指导的实习生的实习项目。由于之前没有经验和参照,大多数算法和流程都是琢磨着写的,不见得高明,请大家多指教。代码是 Python 写成的,效率上肯定不如 C++,所以仅适合作学习和验证想法使用。
下面就来一一介绍我的实现步骤。
硬件和软件配置
我想首先强调的是,硬件配置是这个项目中最不需要费心的事情,在第一个小车项目中使用的硬件如下:
- 四个 USB 鱼眼相机,支持的分辨率为 640x480|800x600|1920x1080 三种。我这里因为是需要在 Python 下面实时运行,为了效率考虑设置的分辨率是 640x480。
- 一台 AGX Xavier。实际上在普通笔记本上跑一样溜得飞起。
我认为只要你只要有四个视野足够覆盖车周围的摄像头,再加一个普通笔记本电脑就足够进行全部的离线开发了。
其中 PyQt5 主要用来实现多线程,方便将来移植到 Qt 环境。
项目采用的若干约定
为了方便起见,在本项目中四个环视相机分别用 front
、back
、left
、right
来指代,并假定其对应的设备号是整数,例如 0, 1, 2, 3。实际开发中请针对具体情况进行修改。
准备工作:获得原始图像与相机内参
以下是视频中四个相机分别拍摄的原始画面,顺序依次为前、后、左、右,并命名为 front.png
、back.png
、left.png
、right.png
保存在项目的 images/
目录下。
四个相机的内参文件分别为 front.yaml
、back.yaml
、left.yaml
、right.yaml
,这些图像和内参文件都存放在项目的 yaml 子目录下。
设置投影范围和参数
接下来我们需要获取每个相机到地面的投影矩阵,这个投影矩阵会把相机校正后的画面转换为对地面上某个矩形区域的鸟瞰图。这四个相机的投影矩阵不是独立的,它们必须保证投影后的区域能够正好拼起来。
这一步是通过联合标定实现的。即在车的四周地面上摆放标定物,拍摄图像,手动选取对应点,然后获取投影矩阵。
首先在车身的四角摆放四个标定板,标定板的图案大小并无特殊要求,只要尺寸一致,能在图像中清晰看到即可。每个标定板应当恰好位于相邻的两个相机视野的重合区域中。
在上面拍摄的相机画面中车的四周铺了一张标定布,这个具体是标定板还是标定布不重要,只要能清楚的看到特征点就可以了。
innerShiftWidth
,innerShiftHeight
:标定板内侧边缘与车辆左右两侧的距离,标定板内侧边缘与车辆前后方的距离。shiftWidth
,shiftHeight
:这两个参数决定了在鸟瞰图中向标定板的外侧看多远。这两个值越大,鸟瞰图看的范围就越大,相应地远处的物体被投影后的形变也越严重,所以应酌情选择。totalWidth
,totalHeight
:这两个参数代表鸟瞰图的总宽高,在我们这个项目中标定布宽 6m 高 10m,于是鸟瞰图中地面的范围为(600 + 2 * shiftWidth, 1000 + 2 * shiftHeight)
。为方便计我们让每个像素对应 1 厘米,于是鸟瞰图的总宽高为totalWidth = 600 + 2 * shiftWidth totalHeight = 1000 + 2 * shiftHeight
- 1
- 2
车辆所在矩形区域的四角 (图中标注的红色圆点),这四个角点的坐标分别为
(xl, yt)
,(xr, yt)
,(xl, yb)
,(xr, yb)
(l
表示 left,r
表示 right,t
表示 top,b
表示 bottom)。这个矩形区域相机是看不到的,我们会用一张车辆的图标来覆盖此处。
注意这个车辆区域四边的延长线将整个鸟瞰图分为前左 (FL)、前中 (F)、前右 (FR)、左 (L)、右 ®、后左 (BL)、后中 (B)、后右 (BR) 八个部分,其中 FL (区域 I)、FR (区域 II)、BL (区域 III)、BR (区域 IV) 是相邻相机视野的重合区域,也是我们重点需要进行融合处理的部分。F、R、L、R 四个区域属于每个相机单独的视野,不需要进行融合处理。
以上参数存放在 param_settings.py 中。
设置好参数以后,每个相机的投影区域也就确定了,比如前方相机对应的投影区域如下:
接下来我们需要通过手动选取标志点来获取到地面的投影矩阵。
手动标定获取投影矩阵
首先运行项目中 run_get_projection_maps.py 这个脚本,这个脚本需要你输入如下的参数:
python run_get_projection_maps.py -camera front -scale 0.7 0.8 -shift -150 -100
[图片上传中…(image-cf20e7-1594974592667-10)]
然后依次点击事先确定好的四个标志点 (顺序不能错!),得到的效果如下:
[图片上传中…(image-a0db05-1594974592667-9)]
如果你不小心点歪了的话可以按 d
键删除上一个错误的点。选择好以后点回车,就会显示投影后的效果图:
觉得效果可以的话敲回车,就会将投影矩阵写入 front.yaml
中,这个矩阵的名字为 project_matrix
。失败的话就点 q
退出再来一次。
[图片上传中…(image-c62af8-1594974592667-7)]
对四个相机分别采用此操作,我们就得到了四个相机的鸟瞰图,以及对应的四个投影矩阵。下一步我们的任务是把这四个鸟瞰图拼起来。
鸟瞰图的拼接与平滑
如果你前面操作一切正常的话,运行 run_get_weight_matrices.py 后应该会显示如下的拼接图:
由于相邻相机之间有重叠的区域,所以这部分的融合是关键。如果直接采取两幅图像加权平均 (权重各自为 1/2) 的方式融合的话你会得到类似下面的结果:
你可以看到由于校正和投影的误差,相邻相机在重合区域的投影结果并不能完全吻合,导致拼接的结果出现乱码和重影。这里的关键在于权重系数应该是随像素变化而变化的,并且是随着像素连续变化。
以左上角区域为例,这个区域是
front
,left
两个相机视野的重叠区域。我们首先将投影图中的重叠部分取出来:灰度化并二值化:
注意这里面有噪点,可以用形态学操作去掉 (不必特别精细,大致去掉即可):
至此我们就得到了重叠区域的一个完整 mask。
将
front
,left
图像各自位于重叠区域外部的边界检测出来,这一步是通过先调用cv2.findContours
求出最外围的边界,再用cv2.approxPolyDP
获得逼近的多边形轮廓。我们把
front
相机减去重叠区域后的轮廓记作polyA
(左上图中蓝色边界),left
相机减去重叠区域后的轮廓记作polyB
(右上图中绿色边界)。对重叠区域中的每个像素,利用
cv2.pointPolygonTest
计算其到这两个多边形polyA
和polyB
的距离 dA,dB,则该像素对应的权值为 w=dB2/(dA2+dB2),即如果这个像素落在front
画面内,则它与polyB
的距离就更远,从而权值更大。对不在重叠区域内的像素,若其属于
front
相机的范围则其权值为 1,否则权值为 0。于是我们得到了一个连续变化的,取值范围在 0~1 之间的矩阵 G,其灰度图如下:用 G 作为权值可得融合后的图像为
front * G + (1- G) * left
。注意由于重叠区域中的像素值是来自两幅图像的加权平均,所以出现在这个区域内的物体会不可避免出现虚影的现象,所以我们需要尽量压缩重叠区域的范围,尽可能只对拼接缝周围的像素计算权值,拼接缝上方的像素尽量使用来自
front
的原像素,拼接缝下方的像素尽量使用来自back
的原像素。这一步可以通过控制 dB 的值得到。我们还漏掉了重要的一步:由于不同相机的曝光度不同,导致不同的区域会出现明暗的亮度差,影响美观。我们需要调整每个区域的亮度,使得整个拼接图像的亮度趋于一致。这一步做法不唯一,自由发挥的空间很大。我查阅了一下网上提到的方法,发现它们要么过于复杂,几乎不可能是实时的;要么过于简单,无法达到理想的效果。特别在上面第二个视频的例子中,由于前方相机的视野被车标遮挡导致感光范围不足,导致其与其它三个相机的画面亮度差异很大,调整起来很费劲。
一个基本的想法是这样的:每个相机传回的画面有
BGR
三个通道,四个相机传回的画面总共有 12 个通道。我们要计算 12 个系数,将这 12 个系数分别乘到这 12 个通道上,然后再合并起来形成调整后的画面。过亮的通道要调暗一些所以乘的系数小于 1,过暗的通道要调亮一些所以乘的系数大于 1。这些系数可以通过四个画面在四个重合区域内的亮度比值得出,你可以自由设计计算系数的方式,只要满足这个基本原理即可。我的实现见这里。感觉就像一段 shader 代码。
还有一种偷懒的办法是事先计算一个 tone mapping 函数 (比如逐段线性的,或者 AES tone mapping function),然后强制把所有像素进行转换,这个方法最省力,但是得到的画面色调会与真实场景有较大差距。似乎有的市面产品就是采用的这种方法。
最后由于有些情况下摄像头不同通道的强度不同,还需要进行一次色彩平衡,见下图。
在第二个视频的例子中,画面的颜色偏红,加入色彩平衡后画面恢复了正常。
具体实现的注意事项
多线程与线程同步。本文的两个例子中四个摄像头都不是硬件触发保证同步的,而且即便是硬件同步的,四个画面的处理线程也未必同步,所以需要有一个线程同步机制。这个项目的实现采用的是比较原始的一种,其核心代码如下:
class MultiBufferManager(object):...def sync(self, device_id):# only perform sync if enabled for specified device/streamself.mutex.lock()if device_id in self.sync_devices:# increment arrived countself.arrived += 1# we are the last to arrive: wake all waiting threadsif self.do_sync and self.arrived == len(self.sync_devices):self.wc.wakeAll()# still waiting for other streams to arrive: waitelse:self.wc.wait(self.mutex)# decrement arrived countself.arrived -= 1self.mutex.unlock()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
这里使用了一个
MultiBufferManager
对象来管理所有的线程,每个摄像头的线程在每次循环时会调用它的sync
方法,并通过将计数器加 1 的方法来通知这个对象 “报告,我已做完上次的任务,请将我加入休眠池等待下次任务”。一旦计数器达到 4 就会触发唤醒所有线程进入下一轮的任务循环。建立查找表 (lookup table) 以加快运算速度。鱼眼镜头的画面需要经过校正、投影、翻转以后才能用于拼接,这三步涉及频繁的图像内存分配和销毁,非常费时间。在我的测试中抓取线程始终稳定在 30fps 多一点左右,但是每个画面的处理线程只有 20 fps 左右。这一步最好是通过预计算一个查找表来加速。你还记得
cv2.fisheye.initUndistortRectifyMap
这个函数吗?它返回的mapx, mapy
就是两个查找表。比如当你指定它返回的矩阵类型为cv2.CV_16SC2
的时候,它返回的mapx
就是一个逐像素的查找表,mapy
是一个用于插值平滑的一维数组 (可以扔掉不要)。同理对于project_matrix
也不难获得一个查找表,两个合起来就可以得到一个直接从原始画面到投影画面的查找表 (当然损失了用于插值的信息)。 在这个项目中由于采用的是 Python 实现,而 Python 的for
循环效率不高,所以没有采用这种查找表的方式。四个权重矩阵可以作为
RGBA
四个通道压缩到一张图片中,这样存储和读取都很方便。四个重叠区域对应的 mask 矩阵也是如此:
实车运行
你可以在实车上运行 run_live_demo.py 来验证最终的效果。
附录:项目各脚本一览
run_calibrate_camera.py
:用于相机内参标定。param_settings.py
:用于设置投影区域的各参数。run_get_projection_maps.py
:用于手动标定获取到地面的投影矩阵。run_get_weight_matrices.py
:用于计算四个重叠区域对应的权重矩阵以及 mask 矩阵,并显示拼接效果。run_live_demo.py
:用于在实车上运行的最终版本。
转自:http://pywonderland.com/surround-view-system-introduction/
python 一份简单的车辆环视全景系统实现图像拼接缝融合相关推荐
- 一份简单的车辆环视全景系统实现
关于车辆的全景环视系统网上已经有很多的资料,然而几乎没有可供参考的代码,这一点对入门的新人来说非常不友好.这个项目的目的就是介绍全景系统的原理,并给出一份可以实际运行的.基本要素齐全的 Python ...
- 【初学python】用python做一个简单的超市收银台付款系统
price=float(input("请输入商品单价:")) if(price<0):print("输入不合法!") amount=int(input(& ...
- python制作统计图_刻意练习11:Python描述统计、简单统计图形
学习计划 MyPlan11 主题:Python描述统计.简单统计图形 时间:8.5-8.11周内完成 参考资料:新书<谁说菜鸟不会数据分析python篇> 各位星友们,在这个星球里每个人都 ...
- Python django实现简单的邮件系统发送邮件功能
Python django实现简单的邮件系统发送邮件功能 本文实例讲述了Python django实现简单的邮件系统发送邮件功能. django邮件系统 Django发送邮件官方中文文档 总结如下: ...
- NVIDIA专家实战演示,教你快速搭建基于Python的车辆信息识别系统
主讲人 | 何琨 英伟达 量子位编辑 | 公众号 QbitAI 随着智慧城市.自动驾驶的快速落地,车辆的检测和识别应用场景非常广泛,如车牌识别.车流统计.车辆属性识别等. 近日,在英伟达x量子位发起的 ...
- .mb是什么文件_神经网络长什么样不知道? 这有一份简单的 pytorch可视化技巧(1)
神经网络长什么样不知道?这有一份简单的 pytorch可视化技巧(1) 深度学习这几年伴随着硬件性能的进一步提升,人们开始着手于设计更深更复杂的神经网络,有时候我们在开源社区拿到网络模型的时候,做客可 ...
- 【机器学习】Python 3.0 简单实现K-邻近法
Python 3.0 简单实现K-邻近法 K-邻近法 : 手头需要样本数据, 有了样本数据才能建立分类函数 第一步:导入样本数据K-近邻算法具体思想(1)计算已知类别数据集中的点于点当前之间的距离(2 ...
- 案例-使用python实现基于opencv的车辆识别
写在开头,接触opencv也有很长一段时间了,中间还接触了halcon.但都是基于C++实现,发现如果有什么idea,还是使用python可以快速实现.基于C++版本的后期会有更新. 首先,这个案例是 ...
- python是不是最简单的语言_全世界都公认运行Python的最简单方法
Python是一种跨平台的编程语言,这意味着它可以在Windows,macOS,Linux等多种平台上运行,甚至可以移植到Java和.NET虚拟机.它是免费和开源的. 即使当今的大多数Linux和Ma ...
最新文章
- Convert .Net Program To Mono
- 基于OHCI的USB主机 —— OHCI(设计思路)
- 一些关于流量和带宽的知识
- 换主板会影响oracle数据库吗,nas瞎折腾 篇一:J3455更换h310+8700t es折腾作业
- Spring 3.1 –从数据库加载XML配置的属性
- epoll nio区别_【总结】两种 NIO 实现:Selector 与 Epoll
- .net mvc actionresult 返回字符串_Spring 框架基础(06):Mvc架构模式简介,执行流程详解
- android studio 新建工程慢,关于AndroidStudio新建与编译项目速度慢解决办法
- 《读编程珠玑有感》——细节处见技术
- 国产卫星高分四号(GF4)预处理(辐射定标)
- eoffice key8.0.php_Office2019增强版永久密钥key
- R语言之K-mean聚类分析
- 留学生的英文期刊论文怎么写?
- 创维广电服务器无线,创维酷开电视连接有线和无线上网教程
- 泛泛而谈webservice
- 7-4 身份证号码最后一位 (100分)
- iphone开发每日一练【2011-10-11】
- 极客日报:三星嘲讽iPhone13:120Hz高刷我们早用上了;华为撤回对OPPO欧洲专利的异议;淘宝搜索崩了登上热搜
- Java项目:在线博客问答系统(java+Springboot+jsp+maven+mysql)
- 华南理工大学计算机专业课程表,华南理工大学2017.92018.7学年度第二学期课程表.DOC...