目录
概述
写这篇文章的原因完全源于开源项目(GitHub参阅参考文献1)。该项目涵盖了环视系统的较为全貌的制作过程,包含完整的标定、投影、拼接和实时运行流程。该篇文章主要是梳理全景拼接技术中的一些实现细节,并在些地方记录了自己的思考。鉴于该开源项目,后续将计划:(1)基于自动驾驶车辆环视相机标定参数(内外参), 完成360全景拼接demo;(2)这篇文章要跳过的参数标定环节,后面要单独拿出篇幅详析。
从360全景的背景讲起
百度百科:“360度全景倒车影像,是一套通过车载显示屏幕观看汽车四周360度全景融合,超宽视角,无缝拼接的适时图像信息(鸟瞰图像),了解车辆周边视线盲区,帮助汽车驾驶员更为直观、更为安全地停泊车辆的泊车辅助系统,又叫全景泊车影像系统或全景停车影像系统(有别于市面上把汽车四周画面在显示屏幕上进行分割显示的“全景”系统)。”
说的通俗些,360全景影像给人视觉上呈现的就是鸟瞰图,即从天空望车顶或者地面的俯视图,但俯视效果的实现是通过多个视角的照片拼接而成的。如下图所示,我们观察自己的车就能发现在车的前后左右四个方向上都有一个鱼眼相机,一般左右方向的鱼眼相机因为视野的问题布置在后视镜下方的比较多。
把四个鱼眼相机通过一定的方法拼接到一张图上,再把车辆照片贴到图片中央,就能近似形成完成的全景图像。下图所示的就是开源项目中所使用的的四路鱼眼相机的前、后、左、右方向的照片。
跨过参数标定聊透视变化
写这篇文章的重点是要梳理如何从鱼眼图片透视变换为全景图片,相机的内外参标定跳过。要完成从单目鱼眼图到鸟瞰图的变化,最重要的一步骤就是完成如下从左图到右图的投影变换,也叫做透视变换。
从左侧到右侧的变化是通过如下表达式实现的。我这里插一句,有同学将透视矩阵叫做外参,我认为这种说法是牵强的。为了说明这个问题,我们就先在世界坐标系下讨论,其他坐标系一样的道理。
- 请参上式,基于将像素坐标反投回世界坐标系,需要内外参。所以即便透视后就是世界坐标,透视矩阵能只叫外参吗?
- 其次透视转换后得到的坐标定义为世界坐标是否合适?因为每个视角的透视变换是分离的,并不在同一坐标下。
- 还有像素坐标到世界坐标得到的是归一化平面下的世界坐标,非严格意义的世界坐标,那透视转换后得到的坐标定义为世界坐标是否准确?
以上三个问题抛砖引玉,愿与大家探讨。我也计划分别针对“相机内参”和“实车全景拼接”再写两篇文章专门从数据上来验证这个事情。
上面的公式解释了透视变换矩阵及其元素物理意义,感兴趣可直接到原文地址拜读。下面对于透视变换的图例描述的也挺形象的。
作者针对四路鱼眼图都进行了同样的透视变换。但在实施投影之前还有个重要的工作,就是图像去畸变,并在去畸变的过程中做一些视野范围的校正。
关于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.resolution
self.undistort_maps = cv2.fisheye.initUndistortRectifyMap(
self.camera_matrix,
self.dist_coeffs,
np.eye(3),
new_matrix,
(width, height),
cv2.CV_16SC2
)
return self
def 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