鱼眼相机环视系统
此段笔记记录在对鱼眼广角相机标定、拼接的过程中遇到的问题和解决思路,并辅以相关的基础知识备忘。
源代码可以下载如下两个压缩包,1用锁实现了同步、2只是简单拼接。两者方法思路基本都是一致的,都可以作为了解原理的起点:
点击下载代码 1
点击下载代码 2
标定板的制作参考如下:
Camera Calibration Pattern Generator – calib.io
https://calib.io/pages/camera-calibration-pattern-generator
可以使用这个小软件来制作标定板
概念整理
先理清两个概念:
1、畸变系数
畸变系数,畸变主要分为径向畸变和切向畸变。
①径向畸变,沿着透镜半径方向分布的畸变;
产生原因:是由透镜质量引起的,光线在远离透镜中心的地方比靠近中心的地方更加弯曲。径向畸变主要包括桶形畸变和枕形畸变两种。
就好比把一个直线拉弯了,哈哈镜都玩过,一个原理
产生的原因是透镜形状和质量引起的,一般来说市场上的鱼眼相机是桶形畸变
②切向畸变,
切向畸变是由于透镜本身与相机传感器平面(像平面)或图像平面不平行而产生的。 切向畸变主要发生在相机传感器和镜头不平行的情况下;因为有夹角,所以光透过镜头传到图像传感器上时,成像位置发生了变化。
[k1, k2, p1, p2, k3]
如果是使用的opencv的接口做的标定的话,那么畸变向量的顺序是这个样子的
其中k指的是径向畸变radial distortion
p指的是切向畸变TangentialDistortion
2、内参矩阵
内参矩阵是相机的固有属性,制造出来之后就是那样了。
如果不使用代码里的标定方法做内参标定,也可以使用Matlab工具箱里的相机标定工具,但是要注意它标定的内参需要转置一下才和OpenCV的结果一致,这点注意看代码就明白了。
代码分析
所需求库:
Pillow
Pyqt5
Imutils
Pyyaml
np.flip已针对版本 v.1.12.0 及更高版本进行了介绍。对于较旧的版本,您可以考虑使用 np.fliplr 和 np.flipud。
对于一个直线检测的方法
createLineSegmentDetector()
来自 https://docs.opencv.org/3.4.14/dd/d1a/group__imgproc__feature.html#ga6b2ad2353c337c42551b521a73eeae7d
由于源代码版权问题,受到保护,所以将其删除了
代码的问题记录和解决方法备案:
环境准备:首先,由于一些函数的问题,opencv的版本不宜过高,我在之前的评论中看到关于版本的问题。
opencv选择4.0.0.21
第一个脚本:1. run_calibrate_camera.py
:用于相机内参标定。
它的参数设置,需要指明
1 | # input video stream |
这样设置:-input 0 -grid “9x6” -fisheye –no_gst
如果使用的是usb请将最后一个参数设置为true
第二个脚本2. run_get_projection_maps.py
:用于手动标定获取到地面的投影矩阵。
Overload resolution failed重载解析失败的问题
它会报错在cv2.fisheye.initUndistortRectifyMap这个函数的位置,提示我们Can’t parse ‘size’. Sequence item with index 0 has a wrong type。
这个问题的解决很简单。我更换了使用的环境,python3.6 、opencv4.0.0.21 、pillow问题都得到了解决,主要是使用的函数接口版本较为旧,在往后的opencv中被更改了。
获得射影变换的四个点时,不能随意选择,一般来说是选择地面上一个矩形区域的四个顶点,实在不行一个四边形的四个顶点也行,我将在后面的博客中去实现一个自动化检测四个投影点的脚本。
最重要的是:假设你选择了地面上的四个点 {P1, P2, P3, P4},并且它们在投影后的鸟瞰图中的像素坐标是 {x_i, y_i, i=1,2,3,4} (这些是你自己定义的),
那么你需要将这里的 dstL, dstF 手动替换为它们的坐标,并且在点击时按照一样的顺序来点击。你可能需要了解一些关于射影变换和四点对应的知识来更好的理解这一部分。
如果要求出投影矩阵,必须的在投影前的图像上选取4个点,同时选择与投影前4个点相对应的透视变换的四个点;变换后的四个点在get_projection_maps.py脚本给出,即dstF,dstL。
需要人工选择的其实就是投影前的四个点,这个其实就是get_projection_maps.py脚本里的
colors = [(0, 0, 255), (0, 255, 0), (255, 0, 0), (0, 255, 255)] ,而且我也注意到了这几颜色点的顺序必须与鼠标点击的顺序一致;
以上步骤其实都是ok,而且是可以拼接成功的,只是随着每次标注的位置不同,拼接的结果会有很大不同,而且要完美呈现实验里的效果着实有些困难,不过最后我还是搞出了比较完美的结果,
我反复看了示例给出的两幅手动标定图,数着那四个点在方格上的准确位置,4辐图严格按照相同的格子位置进行的标注,最后才ok的。
为了使得效果更好,同一行两个点的y坐标最好是一样的。我实验了很多次,如果不严格按照示例给出的点的位置进行标注,拼出来的图很不好看。
+-camera
:指定是哪个相机。
+-scale
:校正后画面的横向和纵向放缩比。
+-shift
:校正后画面中心的横向和纵向平移距离。
这三个参数都很好理解,这个scale是放缩比例,shift是左右上下移动的像素距离,具体实现的方法是什么呢,在cv2.fisheye.initUndistortRectifyMap这个方法里第四个参数,
允许我们传入一个新的摄像机内参,这个矩阵可以由人为的改变其中的参数进行放缩,具体的代码是:
1 | def update_undistort_maps(self): |
这个放缩的方法也非常简单,先copy一个原始的相机内参矩阵(3x3的),由计算机几何的知识知道,【0,0】和【1,1】这两个元素是两个方向上的放缩比例,【0,2】和【1,2】是负责左右上下移动的。这里是为了在外参标定的时候,便于人的观看和选定。当然,这个参数也要传入到最终的投影变换中去。
又或者,我们可以采用一种更简洁的办法:
cv很人性化地提供了一个接口:
1 | new_matrix=cv2.getOptimalNewCameraMatrix |
这个可以用来获得一个新的相机内参矩阵,第一个参数是原来的内参矩阵,第二个是畸变系数,第三个是图像尺寸,第四个是参数是放缩系数。放缩系数的范围是(0,1)。
我们使用这个cv2.getOptimalNewCameraMatrix获得一个新的内参矩阵,然后传入到cv2.fisheye.initUndistortRectifyMap的第四个参数。
在第三个脚本:3. param_settings.py
:用于设置投影区域的各参数。
这个脚本控制的是附图中所用参数的所有设置方式。包含三个方面:第一个是图像的总宽高,在最初做这个项目的时候,约定了每个像素代表真实世界的一厘米,即约定了所有的单位为厘米。totalW和totalH代表的是:图像的总宽高。
还有标定布的宽度cheesboardsize原作者认为整个标定布的宽度(这里的宽度指的是布本身的宽度而不是铺在地上那个宽度)是一致的,就是左右两侧和前后的宽都是这个尺寸。
shift_w和shift_H决定是标定布外面能看多远还是清晰的,或者可以理解为投影变换的最边缘。
inn_shift_w
inn_shift_h 这两个参数是车身距离标定布的距离
当然CarWidth这个参数就能通过以上的参数算出来,中间那个黑色的区域就是放个汽车图片,最后做一个resize的操作以匹配整幅图像的尺寸。
第四个脚本:4. run_get_weight_matrices.py
:用于计算四个重叠区域对应的权重矩阵以及 mask 矩阵,并显示拼接效果。
这个脚本很关键,它是拼接实现环视影像的脚本。要求是必须要有重叠区域
实现的方法也很简单,
- 由于相邻相机之间有重叠的区域,所以这部分的融合是关键。如果直接采取两幅图像加权平均 (权重各自为 1/2) 的方式融合的话你会得到类似下面的结果:
这个产生的原因是:图像的重叠区域因为加权,仍然会损失很多信息,因为是“强拼”的性质,没有用特征匹配。你可以看到由于校正和投影的误差,相邻相机在重合区域的投影结果并不能完全吻合,导致拼接的结果出现乱码和重影。这里的关键在于权重系数应该是随像素变化而变化的,并且是随着像素连续变化。
具体还要怎么做呢?
以左上角区域为例,这个区域是 front
, left
两个相机视野的重叠区域。我们首先将投影图中的重叠部分取出来:
好,我们看到这个区域,是怎么得到的呢?可以在utils.py里找到这个流程所使用的各种方法。
具体可以结合代码来看。这一大坨流程文档描述得非常清楚
脚本6. run_live_demo.py
:用于在实车上运行的最终版本。
这个脚本也会报一些奇奇怪怪的错误,
这个问题的解决办法是:resize那个mask的尺寸