ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从IPM到BEV:用OpenCV实现上帝视角俯视图的完整指南

从IPM到BEV:用OpenCV实现上帝视角俯视图的完整指南 1. 从倒车影像说起gods-eye-view的三个技术流派去年帮朋友改一台老车的倒车影像原车屏幕上的辅助线是固定画上去的不会随方向盘转动倒车时看着那条线心里直发毛。后来我给他换了个带动态轨迹的摄像头轨迹线会跟着方向盘的转角实时变化。这玩意儿看着很神奇其实拆开来看就是一次最朴素的**gods-eye-view上帝视角**工程把车尾地面的物理坐标投影到图像坐标系上再把方向盘转角换算成圆弧轨迹最后叠加显示出来。这次经历让我把上帝视角这个话题完整地摸了一遍。它在不同场景里其实指三件不太一样的事市面上不少教程混着讲容易把人绕晕单目图像的逆透视变换IPM把斜着拍到的路面重投影成俯视图就像你从天上往下看一样。典型场景是倒车影像的辅助线、车道线俯视拼接。多相机环视拼接AVM车上装4-6个广角/鱼眼相机各自生成局部俯视画面再拼成一个以车为中心的360°全景环视。这是目前中高端车的标配功能。数据驱动的鸟瞰感知BEV用神经网络直接从多路摄像头图像中推理出以车为中心的鸟瞰特征图用于自动驾驶的障碍物检测、路径规划。近几年BEV感知几乎成了自动驾驶的标配方案。流派输入核心手段输出典型项目IPM单目图像几何单应变换局部俯视图倒车辅助线、车道拼接AVM多路鱼眼图像联合标定拼接融合360°环视俯视图自动泊车、窄路辅助BEV多路图像神经网络特征投影鸟瞰特征图自动驾驶障碍物检测这篇文章不会一上来就铺开讲BEV这种重方案而是沿着先跑通、再拓展的路线把IPM讲明白先讲几何原理再给一套能直接跑起来的OpenCV代码然后复盘我实测中踩过的坑最后再聊怎么从单图俯视走向多相机环视和BEV。这样从零开始做gods-eye-view相关项目的朋友可以少走不少弯路。2. 逆透视变换的几何原理摄像头是怎么把斜视变成俯视的2.1 小孔成像模型与消失点想理解逆透视变换先得把相机的成像过程想清楚。小孔成像模型里世界坐标中的一个三维点经过镜头光心投影到传感器平面上成为像素点。这个过程把三维信息压缩成了二维信息代价是深度信息丢失了。深度丢失最直观的表现就是消失点。站在一条笔直的马路上路的左右边缘在远处汇聚到一点这在CV里叫vanishing point。铁轨也是同一个道理两根轨枕之间的间距明明是相等的但视觉上越远越密最后压成一个点。这个透视收缩效应正是IPM要消除的东西。IPM的本质就是把这个收缩过程反向做一遍既然透射投影把原本平行的线变成了汇聚线那我用一个逆变换把汇聚的线重新撑开成平行线斜视图像就变成了俯视图。2.2 地面平面假设为什么能从一个摄像头恢复尺度这里有一个关键问题一个像素点的深度信息明明丢失了凭什么能恢复出地面的真实几何答案是地面平面假设我假定场景中所有关心的点都落在一个共同的平面上也就是地面它的方程为z0。有了这个约束深度不再是自由变量可以通过几何关系唯一解出来。通俗地说你看一张照片时能大概猜出远处那辆车离你多远是因为你默认路是平的。如果路是悬崖峭壁这个估计就失效了。IPM也一样它建立在所有目标都在同一个平面上这个强假设之上。一旦场景里出现斜坡、台阶、减速带这个假设被打破俯视图就会出现明显的拉伸变形。2.3 从世界坐标到像素坐标单应矩阵H透视成像的完整公式是s · [u, v, 1]ᵀ K · [R | t] · [X, Y, Z, 1]ᵀ其中K是相机内参矩阵包含焦距和光心坐标R和t是相机相对地面的外参旋转和平移s是深度因子X、Y、Z是世界坐标。因为限定了地面平面Z0第三列可以合并消掉。此时从地面点到图像点的变换退化成一个3×3的单应矩阵Hp_img H · p_groundH K · [r₁, r₂, t]其中r₁和r₂是旋转矩阵的前两列。反过来如果已知图像上的点p_img想求它对应的地面坐标p_ground最直接的想法是求H的逆但实际工程中不会直接对整个图像求逆矩阵因为原图上每个像素的采样密度不均匀直接逆变换会引入大量无效区域。更常见的做法是地面网格重投影我在世界坐标系的地面上画一个矩形网格每个网格节点用H投影到图像上找到它对应的像素颜色再填回俯视图。这正是OpenCV里cv2.remap做的事也是下一章代码的实现思路。3. 用OpenCV落地一个最小俯视视角系统代码与参数详解3.1 方案A手工选点 getPerspectiveTransform最快速的原型验证方法是在斜视图上手工选取四个点对应地面上的一个矩形区域然后用cv2.getPerspectiveTransform求单应矩阵。这个方法不需要标定相机内参非常适合先用一张图验证整体流程。import cv2 import numpy as np img cv2.imread(road.jpg) h, w img.shape[:2] # 原始图像中地面上的梯形区域按 左上、右上、右下、左下 的顺序选取 # 实际使用时可以用鼠标事件在图上点选 src np.float32([ [300, 180], # 左上 [620, 180], # 右上 [780, 420], # 右下 [140, 420] # 左下 ]) # 输出俯视图尺寸单位是像素。 # 如果梯形区域对应地面上的 4m x 6m 区域希望分辨率做到 0.02m/pixel # 那么输出宽 4 / 0.02 200高 6 / 0.02 300 out_w, out_h 200, 300 dst np.float32([ [0, 0], [out_w, 0], [out_w, out_h], [0, out_h] ]) H cv2.getPerspectiveTransform(src, dst) bird_view cv2.warpPerspective(img, H, (out_w, out_h)) cv2.imwrite(bird_view.jpg, bird_view)这段代码里有个很容易忽略的点src四个点对应的地面区域必须是一个矩形但在斜视图看来是梯形。因为平行线在透视图中汇聚所以地面上的矩形拍到画面里就变成了梯形。你选择的梯形越贴合真实地面的矩形生成的俯视图变形越小。3.2 方案B基于相机内外参的网格重投影手工选点只能处理理想的平地场景一旦相机安装角度固定不动但想频繁调整俯视图的覆盖范围就需要用相机内外参来做精确重投影。这个方案需要你事先标定相机至少知道内参K以及相机相对地面的高度和俯仰角。import cv2 import numpy as np # 相机内参用棋盘格标定获得。这里给一个示例值实际以你的相机为准 K np.array([ [800.0, 0.0, 640.0], [0.0, 800.0, 360.0], [0.0, 0.0, 1.0] ]) # 相机在世界坐标系中的位置和朝向 # 假设世界坐标系x向右y向前z向上地面为z0 cam_pos np.array([0.0, -1.5, 1.2]) # 相机在车尾上方 look_at np.array([0.0, 5.0, 0.0]) # 光轴看向前方地面 up np.array([0.0, 0.0, 1.0]) # 相机上方向 def look_at_rotation(eye, center, up): f center - eye f f / np.linalg.norm(f) s np.cross(f, up) s s / np.linalg.norm(s) u np.cross(s, f) R np.stack([s, u, -f], axis0) return R R_c2w look_at_rotation(cam_pos, look_at, up) R_w2c R_c2w.T t_w2c -R_w2c cam_pos # 地面网格以相机位置为原点前方1~9米左右各2米 x_range np.linspace(-2.0, 2.0, 400) # 左右 4m400 列 y_range np.linspace(1.0, 9.0, 800) # 前向 8m800 行 xx, yy np.meshgrid(x_range, y_range) zz np.zeros_like(xx) # 世界坐标 - 相机坐标 - 像素坐标 points_world np.stack([xx.ravel(), yy.ravel(), zz.ravel()], axis1) points_cam (R_w2c points_world.T).T t_w2c valid points_cam[:, 2] 0.1 # 深度必须为正剔除相机背后的点 points_cam points_cam[valid] points_img (K points_cam.T).T u points_img[:, 0] / points_img[:, 2] v points_img[:, 1] / points_img[:, 2] # 生成remap所需的采样坐标网格 map_x np.full(xx.shape, -1, dtypenp.float32) map_y np.full(xx.shape, -1, dtypenp.float32) map_x.ravel()[valid] u.astype(np.float32) map_y.ravel()[valid] v.astype(np.float32) bird_view cv2.remap( img, map_x, map_y, interpolationcv2.INTER_LINEAR, borderModecv2.BORDER_CONSTANT, borderValue(0, 0, 0) ) cv2.imwrite(bird_view_ipm.jpg, bird_view)这个方案里最关键的细节有三个深度掩码points_cam[:, 2] 0.1这一步必须做。遍历地面网格时相机后方和紧贴镜头的点会产生负深度或接近0的深度除出来像素坐标是乱飞的不剔除的话俯视图上会出现一片乱码。网格密度决定输出分辨率代码里400×800个网格点对应4m×8m区域分辨率为0.01m/pixel。你可以按需求调整网格越密输出图越精细但remap的计算量也越大。外参标定精度决定一切这个方案对相机高度和俯仰角非常敏感后面第四章会详细展开说。3.3 结果验证与标定小技巧代码跑通之后先别急着接业务务必用一张棋盘格验证一下比例尺。把棋盘格放在地面上让它在俯视图里占据整数个像素数一下边长对应的像素数算出来的实际分辨率应该和你的期望一致。我习惯的做法是在地面贴一条已知长度为1米的胶带同时在画面远处也放一个参照物分别检查近处和远处的比例尺是否一致。近处准而远处不准说明俯仰角估计有偏差远近都不准但等比例缩放说明相机高度给错了。这个排查思路比对着屏幕瞎调参数高效得多。4. 为什么你生成的俯视图总是不对劲六个常见畸变源与排查顺序4.1 场景不是平面斜坡、减速带与井盖IPM的地面平面假设在真实场景中处处碰壁。最典型的就是斜坡如果俯视图覆盖范围内有一段上坡坡面上的点在投影时会被强行压到水平地面上看起来就像坡上的物体被拉长了。减速带、井盖这类凸起物也一样高度越大的物体畸变越明显。这几乎是所有IPM方案的天花板。解决思路通常有三种一是缩小俯视图覆盖范围让假设地面近似成立二是对场景做语义分割把非地面区域单独处理三是用多传感器融合引入激光雷达或深度相机来修正高程。对小项目来说第一种最务实。4.2 外参不准导致的比例尺漂移相机内参焦距、光心标定一次管很久但外参每次安装都会变。你如果用手拧了一下相机支架或者车后悬挂负载变重导致车身姿态变化相机相对地面的高度和俯仰角就全变了。外参稍有偏差远处区域的比例尺会以二次方的速度漂移。实测下来俯仰角误差1°在10米处的横向偏移能差出30厘米以上。这就是为什么市面上量产车的环视系统都要求严格紧固相机支架并且每次出厂都要做外参标定部分车型还带在线外参自标定功能就是靠车道线、地面纹理来实时修正外参。4.3 镜头畸变与鱼眼镜头普通镜头可以近似用针孔模型但广角镜头和鱼眼镜头的畸变非常严重不校正直接用单应变换画面边缘的物体全是弯曲的。OpenCV里处理方式很简单先用棋盘格标定得到畸变系数再用cv2.undistort或者cv2.fisheye.undistortImage做去畸变然后再做逆透视变换。这里有一个顺序问题一定是先去畸变再做IPM。反过来做的话你先拉伸出一张俯视图里面的畸变全部被放大再去校正效果很差。4.4 越远的区域拉伸越严重逆透视变换的输出图像其分辨率在近处和远处不是均匀的。地面网格中近处的节点映射到原图时占据大量像素远处的节点映射到原图时挤在一起。这意味着俯视图的远处区域采样密度不足看起来是糊的而且横向拉伸特别严重。如果业务上需要看清远处目标不要指望单纯提高输出分辨率能解决。更合理的做法是换用多个不同焦距的相机或者缩短单路相机的覆盖距离用多相机拼接来扩大视野。畸变源现象排查方法场景不平坡上物体拉长缩小覆盖范围或分割非地面区域外参不准远处比例尺漂移用1米胶带分近、远两次验证镜头畸变画面边缘弯曲先去畸变再做IPM距离过远远处模糊、拉伸缩短单路覆盖多相机拼接光照变化亮度不均、阴影干扰局部直方图均衡或等间距多帧平均标定板不平透视矩阵基准错误用刚性平面棋盘格贴在平整地面4.5 固定排查顺序踩过几次坑后我给自己定了一个固定排查序列先确认标定板摆放平整然后看内参标定结果的重投影误差再检查外参最后才怀疑算法本身。很多新手一上来就优化变换逻辑结果发现bug出在标定时棋盘格都没贴平。先硬件后软件先标定后算法这个顺序能省下大量调试时间。5. 从单图俯视到全景鸟瞰多相机拼接与BEV感知的选型思考5.1 多相机环视AVM系统要做的事单路IPM做得再完美也只能覆盖车头或车尾一个扇形区域。想做真正的360°环视就得把4个鱼眼相机前后左右甚至6个相机组合起来。AVM系统的完整链路是多相机联合标定、鱼眼去畸变、各路图像分别IPM投影到统一地平面、最后做拼接融合。拼接融合是最麻烦的环节。相邻相机的重叠区域里同一个物体在两路图中亮度、色温、透视角度都不同直接硬拼会出现明显的接缝。工程上常用做法是拉普拉斯金字塔融合做多频段混合或者找一个拼接缝对缝隙两侧做加权渐变。但不管怎么融合接缝位置的物体只要稍微有高度比如一个行人融合后就会出现重影。这也是AVM系统对障碍物高度特别敏感的原因。做多相机项目时我最大的建议是先把单相机的IPM调稳投影到同一个地平面坐标系后再谈融合。很多团队上来就拼四路图结果每路都有自己的外参误差拼出来的全景图歪歪扭扭最后根本没法定位。5.2 深度学习BEV摆脱平面假设的新路线IPM和多相机拼接都受限于几何模型遇到不平整地面、动态障碍物、遮挡场景效果很难再提升。这几年业界大规模转向数据驱动的BEV感知核心思路是让神经网络学习从多路图像到鸟瞰特征的映射。这条路线的代表作是LSSLift, Splash, Shoot和BEVFormer。LSS的思路很直观先对每个像素预测一个深度分布把二维图像特征提升到三维空间再溅射到鸟瞰网格上形成BEV特征。BEVFormer则用Transformer的注意力机制让BEV网格上的每个位置主动去图像特征里查询自己该关注什么。两者殊途同归都是让网络在数据驱动下学会把多视角信息融合到统一的鸟瞰坐标系里。和传统几何方法比BEV最大的优势是不再依赖平面假设。它可以通过训练数据隐式学习场景中的高度信息和遮挡关系能同时输出目标检测、车道线、可行驶区域等多种结果天然适合下游规划控制模块使用。当然代价也很明显需要大量标注数据、需要GPU算力、需要一套成熟的数据闭环。对比项传统IPM/AVM方案深度学习BEV方案对地面平整度的依赖高低可通过数据学习单帧可输出信息像素级俯视图检测框车道线可行驶区域算力要求低可嵌入式实时运行高通常需要GPU/NPU数据依赖仅需标定板需要大规模标注数据可解释性高几何关系明确低黑盒决策落地难度低高5.3 可以落地的应用场景盘点很多人一听到gods-eye-view就想到自动驾驶其实这套技术在非车场景里也有大量落地需求园区安防利用高位杆上的单目相机把监控画面转成俯视图实现人员越界检测、轨迹跟踪。单相机IPM成本很低一套软件就能盘活存量监控。体育赛事分析把球场斜角转播画面变换成战术视角用于足球、篮球的跑位分析。我见过一个业余项目用一台普通手机拍训练视频IPM后叠加跑动热力图效果相当直观。AGV/AMR导航仓储机器人顶部装一个朝下的鱼眼相机直接输出地面俯视图配合二维码或地面纹理做定位替代传统的磁条或激光导航的一部分功能。农机作业监测无人机或车载相机把农田影像转成俯视图计算作物行距、识别漏播区域比纯斜视图像算法简单得多。工程落地时要注意的点集中在三处第一是实时性嵌入式平台上尽量用整数化的单应变换避免每帧都做浮点remap第二是标定漂移要设计定期自检的流程比如用车道线消失点自动修正俯仰角第三是视角盲区俯视图再完整也弥补不了遮挡需要和毫米波雷达或超声波传感器做决策级融合。最后分享一个我自己的体会做这类项目最忌一上来就追最新方案。先写一个基于OpenCV的最简IPM跑通数据流理解几何边界在哪再决定要不要上多相机、上BEV。每一层复杂度都要有明确的业务收益去支撑否则就是给自己挖坑。
返回列表