ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

上帝视角俯视图生成实战:从相机标定到多路拼接全解析

上帝视角俯视图生成实战:从相机标定到多路拼接全解析 “gods-eye-view”这个词我第一次听到的时候还觉得挺中二后来在计算机视觉里混久了才发现它其实就是大家常说的“上帝视角”——说白了就是从正上方往下看的一幅全局俯视图。这几年做安防巡检、自动驾驶辅助甚至无人机地面站都绕不开这个需求。我这次把完整流程从头到尾捋了一遍包括坐标推导、单应变换、多路视频拼接和实时渲染踩的坑整理成这份文档适合正在做俯视拼接、全景环视或者想搞懂透视变换底层原理的朋友参考。很多初学者一上来就拿着cv2.warpPerspective乱试觉得只要传个矩阵就完事了。但真正做“上帝视角”的时候最难的不是OpenCV怎么调而是你怎么把“相机的画面”和“真实世界的地面坐标”对应起来。这里面牵扯到相机高度、俯仰角、内参外参、畸变校正、拼接权重一系列问题。我这次就从一个真实项目切入把从标定到最终实时出图的每个环节讲透。1. 项目整体定位与方案选型1.1 “上帝视角”到底解决什么问题在传统监控或者车载视野里相机都是斜着往下看的画面里近处物体巨大、远处物体非常小行人走到画面底部的时候可能只露出一个头。这种透视畸变让视觉算法非常难受检测框不稳定、测距误差大、多目标跟踪容易出现ID Switch。而“上帝视角”的本质是把图像投影到地面平面上让世界坐标系中的距离关系在画面里近似等比呈现。说得直白点就是把斜着看到的画面“压平”变成一张俯视图。这样一来行人走一步在图像里对应的像素距离基本是固定的车辆转向、行人间距这些信息都能直观地在二维平面上进行几何计算。我做的这个项目采用的是一个车顶鱼眼相机加上车载IMU的组合。鱼眼镜头负责把周围大范围场景拍进来IMU负责提供当前相机相对地面的俯仰角和横滚角。这两个信息一结合就能把每一帧视频都实时变换成一张俯视拼接图输出到车载中控屏上也能直接喂给后续的目标检测模块。1.2 三条路线对比为什么我没选纯深度学习做“上帝视角”现在有三条常见路线。第一条是纯几何路线也就是用相机内参、外参和地面平面假设直接做透视变换加多路拼接。优点是可解释性强、不需要训练数据、边缘设备也能跑得动缺点是产生的俯视图是基于平面假设的遇到地面起伏比较大的路面会出现拉伸。第二条是纯深度估计路线用神经网络推断出每个像素的深度把图像点云化之后再投影到俯视平面。优点是适应复杂地形缺点是算力需求高、推理帧率上不去而且深度估计在远处表现不稳定。第三条是几何和深度学习混合比如用神经网络分割车道线或可行驶区域再用逆透视映射把分割结果映射到鸟瞰图。这一般在自动驾驶感知模块里比较常见能很好的结合语义信息和几何约束。我这次选的是第一条纯几何路线。原因不复杂项目部署在嵌入式设备上官方给的算力摆在那跑深度网络就会抢占其他检测任务的资源。而且我们是固定安装在车顶相机相对车身的位置角度标定好以后基本不会变这种情况下纯几何已经够用推起来还稳定。1.3 输出形态与应用场景“上帝视角”的最终输出是一张或者几路视频拼接完成的大俯视图。我们项目里分成两个场景低速泊车场景需要实时显示车体周围360度全景画面中心是车模四周是拼接后的俯视视野。路程巡检场景只取车身正下方区域做直线行驶矫正车偏没偏、和路沿距离多少直接在俯视图上量就行。这两种场景需要的画幅和拼接方式完全不一样。前者是四路鱼眼拼接后者是单路校正。我最初就是把单路校正做通了才开始做四路拼接建议大家也别一上来就上全套先把单相机到俯视映射的链路吃透后面扩展就是重复劳动。2. 前置工作相机标定与坐标关系推导2.1 为什么标定信息对出图质量影响这么大网上搜“gods-eye-view”能搜到不少炫酷的效果图但很多人照着教程做出来却是歪七扭八的。大部分问题都出在标定和坐标关系上。相机标定要解决的是两个问题内参和畸变。内参数是焦距、主点坐标这些畸变是镜头边缘的桶形或枕形失真。鱼眼镜头尤其明显如果不对畸变做处理直接把原图拿去算透视变换画面边角会被拉出很夸张的弧形拼接处完全对不上。我用的是一块9x6的黑白棋盘格标定板边长30毫米。采集了大概80张不同角度、不同距离的图片用cv2.calibrateCamera和cv2.fisheye.calibrate分别试了一下。普通针孔模型对鱼眼畸变拟合不好最后用的还是鱼眼模型import cv2 import numpy as np objp np.zeros((6 * 9, 3), np.float32) objp[:, :2] np.mgrid[0:9, 0:6].T.reshape(-1, 2) * 0.03 objpoints [] imgpoints [] # 对采集到的每张图像提取角点 for fname in img_files: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, (9, 6), None) if ret: objpoints.append(objp) imgpoints.append(corners) K, D, rvecs, tvecs cv2.fisheye.calibrate( objpoints, imgpoints, gray.shape[::-1], None, None )注意这里K是3x3内参矩阵D是4个以上的畸变系数。鱼眼模型和普通针孔模型的畸变模型不一样别混着用。你拿针孔模型算出的畸变系数去做鱼眼矫正确实会出问题边角会有一圈“融化”的感觉。2.2 外参与地面平面的关系相机内参搞定之后接下来就是外参。外参描述的是相机坐标系相对世界坐标系的旋转和平移。在“上帝视角”这个场景里世界坐标系一般定义在地上Z轴垂直于地面向上。相机装在车顶有安装高度、有俯仰角、有偏航角。因为你希望最后的俯视图是地面平面上的正射影像所以需要把相机的光轴旋转到垂直向下同时把坐标系原点放在地面平面内。这个旋转矩阵加上高度的平移量合在一起就是一个3x4的外参矩阵。我推荐用“地面平面标定”替代繁琐的外部参数测量在相机视野内铺一张大标定布把标定布的四个角点坐标手动量出来然后通过cv2.solvePnP求解相机相对标定布的外参。这样得到的旋转和平移直接就和地面平面绑定了比用IMU估计还准。# 世界坐标系的四个角点单位米假设标定布为 2m x 2m放在地面 world_pts np.array([ [0, 0, 0], [2, 0, 0], [2, 2, 0], [0, 2, 0] ], dtypenp.float32) # 图像上对应的四个角点像素坐标 image_pts np.array([ [260, 340], [680, 420], [720, 780], [310, 820] ], dtypenp.float32) # 求解外参 retval, rvec, tvec cv2.solvePnP(world_pts, image_pts, K, D)有了解出的旋转向量和平移向量任意地面上的一个点都能投影到图像平面反过来图像上的任意一个像素也能通过地面平面假设反投影到世界坐标。这样你造出来的俯视图各个位置的比例尺就是统一的。2.3 逆透视映射从图像平面到俯视平面所谓逆透视映射IPMInverse Perspective Mapping本质上就是把相机图像里面的路面区域重投影成从上方垂直往下看的效果。它的公式推导可以简单理解为先把图像像素坐标通过内参矩阵转成相机坐标系射线再把这个射线和外参矩阵确定的地面平面求交点得到地面世界坐标最后用你想要的俯视图分辨率去采样。实际编码时不需要你手推公式OpenCV 里cv2.getPerspectiveTransform或cv2.warpPerspective就能做。但前提是你已经知道自己想要的正视图对应那几个点以及原图上那几个点。四组点对应一个单应矩阵用矩阵映射过去就完成了。# 原图上的四个点一般取路面区域 src_pts np.float32([[200, 300], [700, 300], [900, 700], [100, 700]]) # 俯视图上对应的四个点等比例铺开 dst_pts np.float32([[200, 200], [600, 200], [600, 600], [200, 600]]) H, _ cv2.findHomography(src_pts, dst_pts) bird_view cv2.warpPerspective(img, H, (800, 800))这个过程中最简单也最容易搞错的就是源点和目标点的顺序。源点在原图上目标点在俯视图上一一对应不能随便换。写代码的时候最好用一个字典把两个点集放在一起维护方便调参。3. 单目鱼眼相机的俯视图生成实操3.1 畸变校正与区域裁剪的前后顺序拿到鱼眼相机原始画面之后第一件事是去畸变。畸变校正可以有两种做法一种是把整幅图先矫正成针孔图像再做透视变换另一种是直接把去畸变映射和透视变换合在一起用重映射优化成一步。我在项目里用的是后者。因为鱼眼图像的有效区域只有中心一大块圆四周全是无效黑色区域如果先单独做一次畸变校正画面会变成一个不规则的矩形透视变换时还得处理边界。把两步合成一步相当于直接在原始像素坐标上做查找表输出一次就得到目标俯视图既省时间又减少一次插值带来的模糊。map_x, map_y cv2.fisheye.initUndistortRectifyMap( K, D, np.eye(3), K, (800, 800), cv2.CV_32FC1 ) undistorted_img cv2.remap(img, map_x, map_y, cv2.INTER_LINEAR)当然如果你要做的不是实时视频处理而是离线批量生成俯视图分开两步也没问题代码更好理解。但要是上了实时流建议还是合成一步每一帧能省两三毫秒别小看这几毫秒。3.2 俯仰角变化怎么自动补偿车辆在坡道或者加减速的时候车身姿态会变相机相对地面的俯仰角不再是标定时的值。如果固定用一个单应矩阵画面会在上下方向漂移远处的物体被拉得很近或者被推得很远。这个问题我用IMU来解决。IMU输出的是三轴加速度和三轴角速度经过姿态解算可以得到横滚角Roll和俯仰角Pitch。有了当前实时的俯仰角之后可以对标定时得到的旋转矩阵做一个增量修正再重新生成或者插值单应矩阵。# 读取IMU当前俯仰角 pitch imu.get_pitch() # 单位弧度 roll imu.get_roll() # 补偿矩阵先绕X轴补偿roll再绕Y轴补偿pitch Rx np.array([ [1, 0, 0], [0, np.cos(roll), -np.sin(roll)], [0, np.sin(roll), np.cos(roll)] ]) Ry np.array([ [np.cos(pitch), 0, np.sin(pitch)], [0, 1, 0], [-np.sin(pitch), 0, np.cos(pitch)] ]) R_corrected R_initial Rx Ry这里关键的一点是你的IMU坐标系要和相机坐标系对齐。别拿一颗歪着装在自己DIY支架上的IMU直接算标定出来的角度全是偏的。我一般的做法是把IMU和相机刚性固定在一起然后通过几次旋转实验来确定两个坐标系之间的旋转关系。如果没有IMU也有一个土办法在画面里固定远处的一条水平线比如地平线或路沿线的延长线动态估计它的位置变化进而反推俯仰角的偏移量。这个方案鲁棒性稍差但胜在不需要额外硬件。3.3 输出分辨率和感兴趣区域的平衡做实时俯视图的时候“要看多大范围”和“要清楚到多少像素”永远是矛盾的。视野拉大远处细节丢失视野收窄旁边盲区照顾不到。我最后调出来的经验是车身正下方的区域像素密度要最高越往外可以越模糊。因为低速泊车场景驾驶员最关心的是车身四个角附近有没有障碍物而远一点的东西雷达和超声波探头能补上。所以我的俯视图输出分辨率设为800x800投影的地面范围大约为前8米、后6米、左右各4米。这样刚好能把相邻两个车位的车收进画面同时每个车牌的局部特征也还能辨认。为了平衡视野和清晰度还可以做成“中心高分辨率、边缘低分辨率”的非均匀映射。实现方式是在计算IPM映射表的时候对不同的距离段使用不同的尺度因子。这个细节不是必须的但做完以后体验会好很多。3.4 单路到多路拼接的扩展思路单路俯视图通了多路拼接其实就是把几个相机各自生成的俯视图放到同一个世界坐标系里做一次融合。每一路相机都有自己独立的外参在标定的时候都把世界原点放在车体中心地面处。这样每个相机生成出来的俯视图理论上已经对齐在同一张世界地图上。接下来需要处理的只是四个画面之间的重叠区域怎么融合。重叠区域使用的是距离权重融合。距离中心越近的像素它的相机输出权重越高到了视野交界处平滑过渡到另一个相机的画面。# 简化示意两个俯视图的alpha融合 alpha overlap_mask # 0到1之间的权重图 result alpha * bird_view_left (1 - alpha) * bird_view_right这个overlap_mask可以按像素到本相机视野中心的距离预先算好运行时直接查表就行。如果不做权重融合四个画面拼出来会有一条明显的“接缝”而且在车辆移动时接缝处的物体还会跳动非常出戏。4. 实时系统的工程化细节4.1 坐标系约定是团队协作的命门做视觉项目坐标系一致性太重要了。我们项目里参与的不只有算法工程师还有做硬件的同事。硬件那边给你一组IMU数据如果你坐标系定义不统一轻则姿态角方向反了重则俯视图直接翻转。我建议在项目一开始就写一个坐标系说明文档明确以下几点相机的X轴指向右Y轴朝下Z轴朝前也就是标准OpenCV坐标系。世界坐标系的原点在车体后轴中心地面X轴指向车头右侧Y轴指向前方Z轴垂直向上。IMU的坐标系和相机坐标系之间的旋转矩阵必须有校准流程。遇到过最坑的一次是硬件同事给的IMU数据单位是度我以为是弧度直接拿去做矩阵运算结果整个画面像喝醉酒一样乱晃。后来我把所有角度统一转成弧度并在接口层做了校验。这种事看似小出问题排查起来非常磨人。4.2 性能优化从50毫秒压到12毫秒嵌入式设备上的实时俯视图合成CPU占用必须控制在合理范围。刚开始我把整条流水线跑下来单帧处理时间高达50毫秒完全达不到实时。后来做了三处优化第一把所有能预计算的都预计算。透视变换的单应矩阵、畸变校正的映射表、重叠区域的融合权重这些在标定完成后基本都是固定的一次性生成好存内存运行时只查表。第二输出分辨率不要盲目拉高。我们在800x800的基础上又提供了一个640x640的低功耗档位。需要高精度时切高分辨率电池供电时切低分辨率。第三重映射用cv2.remap的时候选择合适插值方式。双线性插值已经够了不需要三次插值后者虽然边缘更平滑但耗时翻倍。去畸变映射表的数据类型用CV_32FC1别用CV_64FC1内存和速度差别都不小。优化完以后整条链路的耗时分配大概是图像采集4毫秒去畸变加重映射5毫秒透视变换1.5毫秒融合和后处理1.5毫秒。整体稳稳压在12毫秒帧率满足要求。4.3 光照和阴影对画面的干扰俯视图把地面上的所有东西都拍进来了包括影子。车辆自身的影子在画面中是一大块黑色区域如果后续要做车道线检测或者障碍物检测阴影区域容易误报。这个问题不能完全靠图像处理消除但可以通过光照补偿缓解。我用的是局部直方图均衡对每个拼接区域单独做自适应亮度调整然后在大区域之间做一次线性亮度过渡。效果是影子边缘的对比度降下来了不再是那种生硬的黑色块。另一个容易被忽略的点是不同的相机自动曝光参数不一致四路画面拼接出来的同一片地面左边亮右边暗。工程师在实地调试的时候不觉得等车主晚上开出去就发现问题了。所以一定要记得把四个相机的曝光时间、增益、白平衡设置成固定值不能让它们各自自动调节。4.4 相机与IMU的时间同步问题如果IMU数据和视频帧时间戳没对齐姿态补偿就是白做。实测下来如果IMU数据延迟20毫秒俯视图边缘会出现明显的“摆动感”。解决这个问题我记得有两种做法一种是在嵌入式端用硬件同步信号触发比如让相机曝光和IMU采样同一时刻。另一种是在软件层做时间戳插值拿到当前帧的时间戳之后从IMU数据队列里取相邻两个采样点的角度做线性插值。我当时的实现是在嵌入式板子上把IMU数据打上单调时钟的时间戳视频帧在读取时也记录同一时钟的时间。然后在算法侧维护一个IMU数据缓冲区用当前帧时间戳去查查前后的角度线性插值得到这一帧对应的精确姿态。实测下来俯视图边缘的摆动明显被抑制住了。5. 常见问题与排查技巧实录5.1 问题一远处画面被严重拉伸现象俯视图里近处正常越靠近画面顶部拉伸越夸张感觉整个地面被拉起来了。原因路程远的区域地面平面假设下对应的像素区域会被极度放大再加上透视变换后插值采样不均看起来就会拉伸。处理方法一是对感兴趣区域设置最大距离超出范围的部分直接裁剪掉不映射进俯视图二是在生成映射表时对远处区域做插值平滑三是配合IMU做俯仰补偿避免远处因为姿态误差被进一步放大。说实话完全消除远处拉伸是不现实的能做到的只是把拉伸范围控制在画面边缘不显眼的位置。5.2 问题二拼接区域物体出现重影现象两个相机同时拍到同一辆车在重叠区域里这辆车出现了两个半透明的轮廓。原因两个相机对同一个3D点的投影位置存在偏差要么是标定参数不准要么是地面平面假设在车这种立体物体上失效车辆被“压平”到了地面。处理方法标定参数重新检查重点看外参是否准确对重叠区域采用距离权重融合至少让重影不那么刺眼。如果重影来自车辆的立体高度可以再加一层由深度学习目标检测结果引导的前景区域掩膜前景不参与全景融合直接沿用主相机的画面。5.3 问题三车辆静止时画面却在微微漂移现象汽车完全停住了但是俯视图里的地面纹理还在慢慢移动。原因大概率是IMU数据噪声或者相机曝光时间变化引起的抖动。停车时IMU的角速度噪声会通过姿态解算产生微小的角度变化再放大到地面像素上就是一两个像素的漂移。处理方法对IMU姿态数据加低通滤波停车状态下给姿态角增加一个零速修正直接锁定为静止。还有一种思路是使用图像特征点匹配来做帧间配准对IMU姿态进行校正。5.4 问题四夜间画面噪点明显现象晚上开到光线不好的地库俯视图全是雪花点拼接质量差。处理方法先看相机ISP设置把自动增益上限调低降低噪点然后在拼接融合时对图像做一次轻度的去噪处理比如快速非局部均值或者双边滤波。千万不要在这个环节用太强的去噪否则把图像细节磨没了后续目标检测一样会受影响。5.5 我整理的一份调试速查表现象优先检查项失败时尝试方向俯视图整体偏移外参平移量是否正确重新做solvePnP标定俯视图旋转错位旋转矩阵/IMU坐标系对齐检查IMU安装角度拼接裂缝重叠区域权重图计算增加权重平滑半径路面纹理模糊插值方式与分辨率换双线性以上插值画面有蓝色或红色色偏各相机白平衡不一致锁定相机参数车辆移动时地面扭曲相机曝光时间过长缩短曝光并提高增益这套表其实就是我每次去现场调车之前会打印出来带上的。看起来简单真到现场被一堆问题包围的时候有张表至少能让你按顺序排查而不是瞎试一通。按我个人的经验这类“上帝视角”项目最值得花时间的地方永远是标定和坐标系对齐。很多看起来炫酷的问题根子上就那么几个偏差没对齐。你如果正准备做类似的事情我的建议是别急着堆功能先花一个下午把标定流程做扎实后面推进会顺很多。
返回列表