OpenCV技术咨询:如何用四台鱼眼摄像头生成车辆360°鸟瞰图
解决牛津机器人汽车数据集360°鸟瞰图生成的逆透视变换问题
要生成车辆周围360°鸟瞰图,手动选getPerspectiveTransform的源/目标点不是最优方案——因为你的相机(尤其是180°FOV的鱼眼相机)是倾斜安装的,单应性变换无法覆盖大视野的透视畸变。正确的做法是结合相机内外参,通过投影模型逆推图像点到鸟瞰平面的映射,以下是具体步骤:
1. 先定义鸟瞰图的物理尺度与坐标系
目标是800x800分辨率的鸟瞰图,先明确其物理意义:
- 设定覆盖范围:比如车辆周围±20m(前后左右),对应0.05m/像素(20m ÷ 400像素),即每像素代表实际0.05米。
- 坐标系映射:
- 鸟瞰图中心
(400,400)对应车辆地面投影中心; - 鸟瞰图向上像素对应车辆前方,向右像素对应车辆右侧;
- 地面物理坐标(车辆坐标系:X向前、Y向左、Z向上)转鸟瞰像素坐标的公式:
u_bev = 400 + int(Y / 0.05) # Y为车辆坐标系下的横向坐标,正方向是左侧,所以符号需调整 v_bev = 400 - int(X / 0.05) # X为车辆坐标系下的纵向坐标,正方向是前方
- 鸟瞰图中心
2. 获取相机外参(核心前提)
牛津机器人汽车数据集会提供每个相机相对于车辆的外参(旋转矩阵R、平移向量T),表示相机坐标系到车辆坐标系的变换:
R:相机坐标系(X向右、Y向下、Z向前)到车辆坐标系的旋转关系;T:相机中心在车辆坐标系下的位置(比如安装高度T[2]通常在1-1.5m左右)。
如果没有直接提供外参,可通过数据集内的标定板、已知GPS坐标的地面点(如车道线、路缘石)结合内参求解。
3. 基于相机投影模型生成映射(替代手动选点)
对每个去畸变后的相机图像,通过逆投影计算每个像素对应的鸟瞰图位置,步骤如下:
3.1 图像点转相机归一化坐标
利用相机内参矩阵K,将图像像素(u,v)转换为相机坐标系下的射线方向:
import numpy as np # 以单目左相机内参为例 fx, fy = 400.0, 400.0 cx, cy = 500.107605, 511.461426 K = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dtype=np.float32) # 图像点转归一化相机坐标 uv_hom = np.array([u, v, 1]) cam_normalized = np.linalg.inv(K) @ uv_hom cam_normalized /= cam_normalized[2] # 得到(x,y,1)的射线方向
3.2 计算射线与地面的交点
通过外参将射线转换到车辆坐标系,求解与地面(Z=0)的交点:
# 示例外参(需替换为数据集实际值) R = np.array([[0, 0, 1], [-1, 0, 0], [0, -1, 0]], dtype=np.float32) # 相机向左安装的旋转矩阵 T = np.array([0.0, 1.5, 1.2], dtype=np.float32) # 相机中心在车辆坐标系的位置:横向1.5m、高度1.2m # 射线在车辆坐标系下的方向 cam_norm_vehicle = R @ cam_normalized # 计算射线与地面Z=0的交点参数t t = -T[2] / cam_norm_vehicle[2] if t <= 0: # 射线向上(如天空区域),不与地面相交,跳过 continue # 得到地面点的车辆坐标 X = cam_norm_vehicle[0] * t + T[0] Y = cam_norm_vehicle[1] * t + T[1]
3.3 转换为鸟瞰图像素坐标
用第一步定义的尺度,将地面物理坐标转为鸟瞰图像素位置,然后用cv2.remap完成图像变换。
4. 多相机拼接与融合
对每个相机生成对应的局部鸟瞰图后,处理重叠区域:
- 加权融合:对重叠区域的像素取加权平均值(权重可根据相机距离该区域的远近调整);
- 优先级覆盖:比如用立体相机的图像覆盖前后单目相机的重叠区域(立体相机分辨率更高)。
若必须用单应性变换(临时方案)
如果暂时无法获取外参,可尝试:
- 在图像中找到至少4个已知物理坐标的地面点(比如数据集标注的GPS点、标定板位置);
- 这些点的去畸变图像坐标作为源点,对应的鸟瞰图像素坐标(按物理尺度计算)作为目标点;
- 用
cv2.getPerspectiveTransform计算单应性矩阵,再用cv2.warpPerspective变换图像。
但此方法仅适合局部小范围,大FOV鱼眼相机的边缘区域畸变无法通过单应性修正。
核心代码示例
import cv2 import numpy as np # 鸟瞰图参数 BEV_SIZE = (800, 800) PIX_PER_METER = 20 # 20像素/米,即0.05米/像素 BEV_CENTER = (BEV_SIZE[0]//2, BEV_SIZE[1]//2) def create_bev_mapping(img_size, K, R, T): """生成图像到鸟瞰图的映射表""" map_x = np.zeros((img_size[0], img_size[1]), dtype=np.float32) map_y = np.zeros((img_size[0], img_size[1]), dtype=np.float32) for v in range(img_size[0]): for u in range(img_size[1]): # 图像点转归一化相机坐标 uv_hom = np.array([u, v, 1], dtype=np.float32) cam_norm = np.linalg.inv(K) @ uv_hom cam_norm /= cam_norm[2] # 计算地面交点 cam_norm_vehicle = R @ cam_norm t = -T[2] / cam_norm_vehicle[2] if t <= 0: map_x[v, u] = -1 map_y[v, u] = -1 continue X = cam_norm_vehicle[0] * t + T[0] Y = cam_norm_vehicle[1] * t + T[1] # 转鸟瞰像素坐标 u_bev = BEV_CENTER[0] + int(Y * PIX_PER_METER) v_bev = BEV_CENTER[1] - int(X * PIX_PER_METER) if 0 <= u_bev < BEV_SIZE[0] and 0 <= v_bev < BEV_SIZE[1]: map_x[v, u] = u_bev map_y[v, u] = v_bev else: map_x[v, u] = -1 map_y[v, u] = -1 return map_x, map_y # 处理单目左相机图像 img_left = cv2.imread("undistorted_left.jpg") map_x, map_y = create_bev_mapping(img_left.shape[:2], K, R, T) bev_left = np.zeros((BEV_SIZE[1], BEV_SIZE[0], 3), dtype=np.uint8) cv2.remap(img_left, map_x, map_y, cv2.INTER_LINEAR, dst=bev_left) # 同理处理其他相机后拼接融合 # ...
内容的提问来源于stack exchange,提问作者J. Random Luser
相关产品推荐
相关产品推荐

