ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Carla仿真四路相机标定与BEV环视拼接实战

Carla仿真四路相机标定与BEV环视拼接实战 1. 项目缘起与整体设计思路1.1 为什么要在Carla里折腾四路相机标定做自动驾驶感知的人迟早会碰到一个坎单车前视相机能看到的东西太有限了泊车、窄路会车、低速避障这些场景你必须要有一个从上往下看的“上帝视角”。BEVBird‘s Eye View环视拼接就是干这个的——把车身前后左右四个鱼眼相机拍到的画面经过标定和透视变换拼成一张从车顶正上方往下看的全景图。在真车上做这套东西成本高、周期长、标定场地还得预约。Carla仿真器给了我们一个几乎零成本的试验田你可以随意摆放相机、调整内参外参、导出图像甚至能拿到仿真器里精确到小数点后好几位的真值位姿。这意味着你可以把标定算法的每一个环节都拆开验证不用担心“到底是算法错了还是我卷尺量错了”。这个项目的核心目标很明确在Carla里搭建一套四路相机系统完成从相机内参标定、畸变校正到基于单应矩阵的透视变换最后拼接成一张完整的BEV环视图。整套流程走通之后你得到的不只是一张图而是一套可以迁移到真车上的完整方法论。1.2 整体方案选型与背后的考量做BEV环视拼接市面上大致有三条路第一种是纯几何方法用单应矩阵做透视变换后拼接第二种是查表法LUT提前把每个像素的映射关系算好存成表第三种是深度学习方案比如用逆透视变换网络直接生成BEV特征图。这个项目选的是第一条路——单应矩阵。原因有三第一Carla里的地面是理想平面单应矩阵的假设天然成立不需要考虑坡度带来的误差第二几何方法的每一步都是可解释、可调试的你能清楚知道哪个环节出了问题第三这套方法迁移到真车上时只需要替换标定参数算法框架完全不用动。整个流程拆成四个阶段相机架设与数据采集、内参标定与畸变校正、外参标定与单应矩阵求解、BEV拼接与融合。每个阶段都有独立的验证手段不会出现“最后拼出来不对但不知道哪步错了”的情况。提示如果你之前没接触过Carla建议先把仿真器跑起来确认能正常出图再往下走。相机标定本身不复杂但环境问题会消耗大量时间。1.3 适合谁来参考这套流程这篇文章面向的是有一定Python基础、了解基本图像处理概念比如矩阵运算、透视变换的开发者。如果你正在做自动驾驶感知、机器人导航、或者任何需要多相机融合的项目这套流程可以直接抄作业。哪怕你用的是真车数据标定和拼接的逻辑是完全一样的只是数据采集环节需要换成实际的标定布和卷尺。对于完全没接触过Carla的读者我会在关键步骤补充必要的背景知识确保你能跟上节奏。但如果你连OpenCV的cv2.warpPerspective都没用过建议先花半小时补一下基础再回来读这篇。2. 核心细节解析与实操要点2.1 Carla中四路相机的架设参数在Carla里添加相机本质上就是往仿真世界里扔四个camera传感器。但扔在哪里、朝哪个方向、用什么镜头参数直接决定了后面标定的难度和BEV图的质量。先说服安装位置。真车上环视相机通常装在前后保险杠和左右后视镜下方离地高度大约在0.5到0.8米之间。在Carla里我建议把相机安装在车身坐标系的四个角上具体位置如下前相机x1.5my0z0.6m朝向车头方向yaw0°后相机x-1.5my0z0.6m朝向车尾方向yaw180°左相机x0y-0.9mz0.6m朝向车身左侧yaw-90°右相机x0y0.9mz0.6m朝向车身右侧yaw90°这个布局的好处是四个相机的地面覆盖区域能形成一个大致对称的环形相邻相机之间有足够的重叠区域用于后续的融合。重叠区域建议控制在15%到25%之间——太少会导致拼接缝明显太多则浪费分辨率。再说镜头参数。Carla的相机传感器支持设置fov视场角。环视相机通常用大广角我一般设成100°到120°。但要注意FOV越大边缘畸变越严重后面畸变校正的难度也越大。如果你只是做算法验证建议先用90°的FOV跑通流程再逐步加大到实际需要的角度。分辨率方面Carla默认是800x600我建议改成1280x720或者1920x1080。分辨率越高BEV图的细节越丰富但计算量也越大。实测下来1280x720是一个比较好的平衡点。注意Carla的相机坐标系和OpenCV的相机坐标系定义不同。Carla用的是左手坐标系x向前、y向右、z向上OpenCV用的是右手坐标系x向右、y向下、z向前。在计算外参时一定要做坐标转换否则单应矩阵算出来完全是错的。2.2 内参标定从棋盘格到畸变系数内参标定的目的是拿到相机的焦距、主点坐标和畸变系数。在真车上你需要打印一张棋盘格举着它在相机前面晃来晃去拍几十张照片。在Carla里事情简单得多——你可以直接生成一个棋盘格纹理贴在一个平面上然后用相机去拍。具体操作是这样的在Carla场景里放一个static.prop.chessboard如果没有现成的可以用Python脚本生成一个棋盘格纹理贴到一个平面mesh上。然后控制相机从不同角度拍摄这个棋盘格至少拍15到20张。角度要覆盖正对、左倾、右倾、上仰、下俯以及各种旋转组合。拍完之后用OpenCV的cv2.findChessboardCorners检测角点再用cv2.calibrateCamera计算内参。这里有一个关键点Carla的相机没有真实的镜头畸变所以理论上畸变系数应该全是零。但为了模拟真实情况你可以在Carla的相机设置里手动加一点畸变或者干脆跳过畸变校正这一步直接用理想针孔模型。我个人的做法是先按理想模型跑通全流程确认BEV拼接没问题之后再手动加畸变测试畸变校正的效果。这样可以把问题隔离开避免一开始就陷入“到底是畸变没校正好还是单应矩阵算错了”的困境。内参标定的代码框架大致如下import cv2 import numpy as np # 棋盘格参数 pattern_size (9, 6) objp np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objpoints [] # 3D点 imgpoints [] # 2D点 for img_path in image_paths: img cv2.imread(img_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if ret: objpoints.append(objp) corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)) imgpoints.append(corners2) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)跑完之后mtx就是内参矩阵dist是畸变系数。在Carla的理想情况下dist应该接近全零mtx的焦距应该和FOV设置一致。你可以用这个来验证标定结果是否合理。2.3 外参标定与单应矩阵求解外参标定是BEV拼接里最核心也最容易出错的一步。我们需要知道每个相机相对于车体坐标系的精确位姿旋转矩阵R和平移向量t。在Carla里这个信息可以直接从仿真器API拿到——每个传感器都有一个get_transform()方法返回它的位置和旋转。但拿到外参只是第一步。我们要做的是把每个相机拍到的地面图像通过透视变换映射到同一个BEV平面上。这个变换可以用单应矩阵H来描述。单应矩阵的推导逻辑是这样的假设地面是一个平面在车体坐标系下地面的方程是z0。相机拍到的图像上的点(u,v)和地面上的点(X,Y,0)之间通过相机投影模型建立关系。经过推导可以得到一个3x3的单应矩阵H使得[u, v, 1]^T H * [X, Y, 1]^T反过来如果我们想把图像上的点映射到BEV平面就用H的逆矩阵。在实际操作中我不建议直接用手推的公式去算H。更稳妥的做法是在Carla里在地面上放四个已知坐标的标记点比如四个不同颜色的方块然后用相机拍下来检测这四个点在图像上的像素坐标。有了四组“图像坐标-BEV坐标”的对应点就可以用cv2.findHomography直接求解H。这个方法的好处是它不依赖相机外参的精确值而是直接用图像上的对应点来求解。即使Carla给的位姿有微小误差只要标记点的像素坐标检测准确H就是准的。标记点的选择有讲究四个点要尽量分散覆盖相机视野的大部分区域。如果四个点挤在一起求解出来的H在边缘区域会很不稳定。我一般把标记点放在BEV平面的四个角上对应到图像上就是视野的四个角落附近。提示Carla里可以用debug.draw_point在指定位置画点然后用相机拍下来。但更可靠的方法是放一个带纹理的平面纹理上画好标记点这样检测更稳定。2.4 BEV拼接中的融合策略四个相机各自变换到BEV平面之后会得到四张BEV图。这四张图在重叠区域会有内容重复直接叠加会出现明显的拼接缝。融合策略的选择直接影响最终BEV图的观感。最简单的融合是“硬切”——在重叠区域取固定权重比如左相机取左边50%前相机取右边50%。但这样在拼接缝处会有明显的亮度突变。好一点的方案是“羽化融合”——在重叠区域用一个渐变权重从0到1平滑过渡。具体实现时可以先生成一个权重掩码然后在重叠区域做加权平均。我实测下来效果最好的是“多频段融合”Multi-band Blending但那个计算量比较大在实时系统里不太实用。对于Carla仿真验证来说羽化融合已经足够好了。羽化融合的权重计算可以用距离变换对每个相机的BEV图计算每个像素到该相机覆盖区域边界的距离距离越大权重越高。然后把四张图的权重归一化做加权平均。def feather_blend(bev_images, weight_masks): # bev_images: 四张BEV图 # weight_masks: 四张权重掩码 total_weight np.sum(weight_masks, axis0) total_weight[total_weight 0] 1 # 避免除零 blended np.zeros_like(bev_images[0], dtypenp.float32) for img, mask in zip(bev_images, weight_masks): blended img.astype(np.float32) * mask blended / total_weight return blended.astype(np.uint8)权重掩码的生成可以用cv2.distanceTransform对每个相机的有效区域做距离变换然后归一化。3. 实操过程与核心环节实现3.1 Carla环境搭建与相机数据采集先把Carla跑起来。如果你用的是Windows直接下载预编译包解压运行就行。Linux下需要先装好显卡驱动和Vulkan然后运行./CarlaUE4.sh。启动之后你会看到一个默认的城镇场景。接下来用Python脚本连接Carla。需要安装carla这个Python包版本要和仿真器版本匹配。我一般用0.9.13或0.9.14比较稳定。import carla import numpy as np import cv2 client carla.Client(localhost, 2000) client.set_timeout(10.0) world client.get_world() # 获取车辆蓝图 blueprint_library world.get_blueprint_library() vehicle_bp blueprint_library.filter(vehicle.*)[0] # 生成车辆 spawn_point carla.Transform(carla.Location(x0, y0, z1.0), carla.Rotation(yaw0)) vehicle world.spawn_actor(vehicle_bp, spawn_point)车辆生成之后开始挂相机。四个相机的安装位置和朝向按前面说的来。Carla的相机传感器有一个image_size_x和image_size_y参数设成1280和720。fov设成100。camera_bp blueprint_library.find(sensor.camera.rgb) camera_bp.set_attribute(image_size_x, 1280) camera_bp.set_attribute(image_size_y, 720) camera_bp.set_attribute(fov, 100) # 前相机 front_transform carla.Transform( carla.Location(x1.5, y0, z0.6), carla.Rotation(pitch0, yaw0, roll0) ) front_camera world.spawn_actor(camera_bp, front_transform, attach_tovehicle)四个相机都挂好之后需要注册回调函数来保存图像。Carla的相机数据是异步来的所以要用一个队列或者直接存到列表里。image_queue [] def save_image(image): array np.frombuffer(image.raw_data, dtypenp.uint8) array array.reshape((image.height, image.width, 4)) array array[:, :, :3] # 去掉alpha通道 image_queue.append(array) front_camera.listen(save_image) # 其他三个相机同理采集数据的时候让车辆在场景里慢慢开一段或者干脆静止不动只让相机拍。对于标定来说静止就够了。但如果你想测试BEV拼接在运动中的效果可以让车辆沿直线行驶每隔几帧保存一次图像。3.2 棋盘格标定实操与结果验证在Carla里做棋盘格标定最方便的方法是直接在场景里放一个棋盘格平面。Carla的static.prop库里可能没有现成的棋盘格但你可以用Python生成一个棋盘格纹理然后通过world.spawn_actor把它贴到一个平面上。更简单的做法是不用真的放棋盘格而是直接用Carla的相机内参真值来验证。Carla的相机传感器有一个get_intrinsics()方法返回焦距和主点。你可以用这个真值来对比你的标定结果。但为了走通完整流程我还是建议实际做一次棋盘格标定。步骤如下生成棋盘格纹理用numpy生成一个9x6的棋盘格每个格子80像素保存成PNG。在Carla里创建一个平面mesh贴上这个纹理。控制相机从不同角度拍摄这个平面至少20张。用OpenCV的calibrateCamera计算内参。对比标定结果和Carla真值验证误差。实测下来在Carla的理想条件下标定误差可以控制在0.1像素以内。如果你加了畸变误差会大一些但经过畸变校正后也能降到0.5像素以内。注意Carla的相机没有真实的镜头畸变所以dist系数应该接近零。如果你标定出来的dist很大说明棋盘格图像有问题可能是角点检测错了。3.3 单应矩阵求解的完整代码实现单应矩阵求解是整个BEV拼接的核心。我把它拆成三个步骤标记点检测、对应点匹配、H矩阵求解。标记点检测在Carla里我在地面上放四个不同颜色的方块分别放在车体坐标系的四个角上。比如红色方块(3, 2, 0)绿色方块(3, -2, 0)蓝色方块(-3, 2, 0)黄色方块(-3, -2, 0)然后用每个相机拍一张图检测这四个方块在图像上的像素坐标。检测方法可以用颜色阈值加轮廓检测。def detect_marker(image, color_lower, color_upper): hsv cv2.cvtColor(image, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, color_lower, color_upper) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) 0: return None # 取最大轮廓的中心 c max(contours, keycv2.contourArea) M cv2.moments(c) cx int(M[m10] / M[m00]) cy int(M[m01] / M[m00]) return (cx, cy)对应点匹配对于每个相机我们得到四个图像坐标点。同时这四个标记点在BEV平面上的坐标是已知的就是它们在车体坐标系下的XY坐标。这样就有了四组对应点。H矩阵求解用cv2.findHomography传入图像坐标和BEV坐标得到H矩阵。src_points np.array(image_points, dtypenp.float32) # 图像坐标 dst_points np.array(bev_points, dtypenp.float32) # BEV坐标 H, status cv2.findHomography(src_points, dst_points, cv2.RANSAC, 5.0)得到H之后用cv2.warpPerspective把整张图像变换到BEV平面。bev_image cv2.warpPerspective(image, H, (bev_width, bev_height))这里有一个关键点BEV平面的尺寸和分辨率要提前定义好。我一般设成800x800像素对应车体周围8米x8米的区域。这样每个像素代表1厘米分辨率足够看清车道线和障碍物。3.4 四路BEV拼接与融合的完整流程四个相机各自变换到BEV平面之后得到四张BEV图。这四张图需要融合成一张。融合的第一步是生成权重掩码。对每个相机的BEV图计算有效区域非零区域的距离变换然后归一化。def generate_weight_mask(bev_image): gray cv2.cvtColor(bev_image, cv2.COLOR_BGR2GRAY) mask (gray 0).astype(np.uint8) dist cv2.distanceTransform(mask, cv2.DIST_L2, 5) dist dist / (dist.max() 1e-6) return dist然后做加权平均bev_images [front_bev, rear_bev, left_bev, right_bev] weight_masks [generate_weight_mask(img) for img in bev_images] total_weight np.sum(weight_masks, axis0) total_weight[total_weight 0] 1 blended np.zeros_like(bev_images[0], dtypenp.float32) for img, mask in zip(bev_images, weight_masks): blended img.astype(np.float32) * mask[:, :, np.newaxis] blended / total_weight[:, :, np.newaxis] blended blended.astype(np.uint8)最后把融合后的BEV图显示出来或者保存成视频。实测下来这套流程在Carla里跑一遍大概需要5到10分钟主要时间花在数据采集和标定上。一旦标定参数确定后续的BEV拼接是实时的在普通笔记本上也能跑到30帧以上。4. 常见问题与排查技巧实录4.1 标定结果不准确怎么办标定结果不准最常见的原因是棋盘格图像质量不够。在Carla里虽然图像是合成的但如果你用的棋盘格纹理分辨率太低角点检测也会出问题。我建议棋盘格的每个格子至少占50个像素整张棋盘格在图像里占1/3以上的面积。另一个常见问题是棋盘格的角度不够丰富。如果你只拍了正对相机的几张图标定出来的焦距和主点会有很大偏差。一定要从不同角度拍至少覆盖±30°的倾斜和旋转。如果标定出来的重投影误差大于1像素先检查角点检测的结果。用cv2.drawChessboardCorners把检测到的角点画出来肉眼看一下有没有明显错位的。4.2 单应矩阵求解失败的排查思路单应矩阵求解失败通常是因为四个标记点共线或者太接近。cv2.findHomography需要至少四个不共线的点而且点与点之间要有足够的距离。排查步骤检查标记点检测结果。把检测到的点画在图像上确认没有检测错。检查BEV坐标是否对应正确。四个标记点的顺序要和图像上的顺序一致。如果标记点检测不稳定换一种颜色或者增大标记点的尺寸。如果还是不行试试用更多的标记点比如6个或8个然后用cv2.findHomography的RANSAC模式。提示在Carla里你可以直接用debug.draw_point在3D世界里画点然后从相机图像里找到这些点的像素坐标。但这种方法不如颜色标记稳定因为3D点投影到图像上可能被遮挡。4.3 BEV拼接缝明显的解决方法拼接缝明显根本原因是重叠区域的融合权重不够平滑。如果用的是硬切拼接缝一定明显。换成羽化融合之后如果还有缝可能是权重掩码生成得不对。检查权重掩码把四个权重掩码可视化出来看看重叠区域的权重过渡是否平滑。如果某个相机的权重在边界处突然降到零就会产生缝。另一个可能的原因是四个相机的BEV图在重叠区域的亮度不一致。Carla的相机是理想相机理论上亮度应该一致。但如果你开了不同的曝光或者gamma亮度就会有差异。检查一下四个相机的传感器设置是否完全一致。如果以上都没问题但缝还是可见可以试试在融合之后做一个轻微的高斯模糊把缝模糊掉。但这只是权宜之计根本解决还是要从权重掩码入手。4.4 性能优化与实时性调优BEV拼接的计算量主要来自cv2.warpPerspective和融合。在1280x720的分辨率下四个相机的透视变换大概需要10到15毫秒融合需要5到10毫秒。总共20到25毫秒理论上能跑到40帧。但如果你的BEV平面分辨率很高比如1600x1600计算量会翻倍。这时候可以考虑用查表法LUT来加速透视变换。具体做法是提前把每个像素的映射关系算好存成一个索引表然后直接用cv2.remap做映射。cv2.remap比cv2.warpPerspective快很多尤其是在大分辨率下。# 生成映射表 map_x, map_y cv2.initUndistortRectifyMap( camera_matrix, dist_coeffs, None, H, (bev_width, bev_height), cv2.CV_32FC1 ) # 用remap做透视变换 bev_image cv2.remap(image, map_x, map_y, cv2.INTER_LINEAR)实测下来用cv2.remap能把透视变换的时间降到3到5毫秒整体帧率能提到60帧以上。4.5 常见问题速查表问题现象可能原因排查方法解决方案标定重投影误差大棋盘格图像质量差可视化角点检测结果提高棋盘格分辨率增加拍摄角度单应矩阵求解失败标记点共线或太近检查标记点分布增加标记点数量分散布局BEV拼接缝明显融合权重不平滑可视化权重掩码改用羽化融合检查相机曝光BEV图边缘模糊透视变换插值方式对比不同插值方法用cv2.INTER_LINEAR或cv2.INTER_CUBIC实时性不够透视变换计算量大计时各环节耗时改用查表法cv2.remap相机坐标系混乱Carla与OpenCV坐标系定义不同检查坐标转换矩阵统一转换到OpenCV坐标系再计算5. 从仿真到真车的迁移经验5.1 仿真里好用的参数真车上要改什么Carla里跑通之后最自然的问题就是这套东西搬到真车上要改什么我的经验是算法框架完全不用动但有三类参数必须重新标定。第一类是相机内参。真车的镜头畸变比Carla大得多尤其是广角镜头。你需要用真实的棋盘格重新标定而且畸变系数要用到5个参数k1, k2, p1, p2, k3甚至更多。Carla里畸变系数接近零真车上可能是0.1到0.3的量级。第二类是外参。真车上相机安装位置有公差Carla里你可以精确到毫米真车上可能差几厘米。所以真车标定必须用地面标记点来求解单应矩阵不能依赖设计图纸上的安装位置。第三类是融合权重。真车四个相机的曝光和色彩响应不可能完全一致融合之前需要做色彩校正。Carla里可以跳过这一步真车上必须做。5.2 真车标定的场地布置与操作技巧真车标定需要一块平整的场地地面最好是水泥或沥青不能有大的坡度。标定布要铺在车周围前后左右各一块每块标定布上画好棋盘格或标记点。操作技巧标定前把车停正方向盘回正胎压正常。标定布的位置要用卷尺精确测量误差控制在1厘米以内。四个相机同时拍照避免车辆移动导致外参变化。如果场地光照不均匀可以在标定布周围加补光灯。标定完成后开车在场地里转一圈看看BEV图有没有明显的错位或变形。如果有说明标定参数还需要微调。5.3 后续扩展方向这套BEV拼接框架可以扩展出很多有用的功能。比如在BEV图上叠加障碍物检测结果做成一个完整的环视感知系统。把BEV图输入到泊车规划算法里实现自动泊车。用多帧BEV图做光流估计得到车辆的运动轨迹。把BEV图和激光雷达点云融合得到更精确的3D环视感知。我在实际项目里把BEV图用在了低速避障上。车辆在窄路行驶时BEV图能清楚看到车身两侧的障碍物距离比超声波雷达直观得多。这套流程从Carla仿真到真车部署大概花了两周时间其中大部分时间花在真车标定和色彩校正上。算法本身在仿真里已经验证得很充分了迁移到真车时几乎没有遇到算法层面的问题。最后分享一个小技巧在Carla里做标定时可以写一个自动化脚本让相机自动从不同角度拍摄棋盘格然后自动跑标定和验证。这样你可以在几分钟内迭代几十次标定参数快速找到最优的相机布局和标定方案。这个脚本我到现在还在用每次改相机参数之后跑一遍心里就有底了。
返回列表