200字
从四路鱼眼到鸟瞰图:BEV 环视原理与 surround-view-system-introduction 工程精读
2026-05-12
2026-06-02
title: 从四路鱼眼到鸟瞰图:BEV 环视原理与 surround-view-system-introduction 工程精读
slug: bev-surround-view-system-introduction

从四路鱼眼到鸟瞰图:BEV 环视原理与 surround-view-system-introduction 工程精读


摘要

本文围绕开源工程 surround-view-system-introduction,梳理四路鱼眼 BEV 环视系统的技术链路。

内容包括:BEV 的核心数学工具(Homography、IPM、地面平面假设),四路鱼眼环视的数据流,该工程的四个阶段(内参标定→投影矩阵→融合权重→实时运行),参数模型解析,以及复现过程中遇到的常见问题。


写在前面

实习中涉及到了BEV相关的内容。这个仓库我也复现了下。这篇博客是个人关于这部分的知识记录。(虽然移植到公司车上的效果并不是很好,孩子们,想做什么东西之前先看看机构行不行,what can i say)

1. 什么是 BEV / 环视?

BEV(Bird's Eye View,鸟瞰图)是将车周围的多路相机图像,统一变换到一个从上往下看的俯视视角。最终效果类似于你在车顶上装了一个摄像头直直朝下拍。

典型应用场景包括泊车辅助、狭窄空间驾驶、自动泊车等,在这些场景中需要看清车身周围地面上的标线和障碍物。

环视 BEV 的核心挑战:怎么把几个不同方向、不同视角的相机图像,拼到同一张俯视图里,让地面上的物体和标线对齐?

BEV 环视最终效果:四路鱼眼图像拼接为车身周围鸟瞰图


2. 核心数学工具

2.1 Homography(单应性矩阵)

假设地面上有一个标定点,在相机图像中的像素坐标是 (u, v),在俯视图(BEV 图)中的坐标是 (x, y)。我们可以用一个 3×3 的矩阵 H 来描述这两个坐标之间的透视变换关系:

s · [x, y, 1]^T = H · [u, v, 1]^T

H 就是单应性矩阵。已知至少 4 对对应点,通过 OpenCV 的 cv2.getPerspectiveTransform(src, dst) 求解。

有了 H 之后,用 cv2.warpPerspective(image, H, output_size) 就可以把整张相机图像"拉成"鸟瞰图。

2.2 地面平面假设

Homography 做 BEV 有一个重要前提:被投影的点都在同一个地面平面上

单应性矩阵描述的是两个平面之间的映射关系。图像中不在地面上的点(墙面、车身、行人等),用针对地面计算的 H 投影后会产生拉伸和错位。因此 Homography + IPM 方案只对地面内容准确——这是它的主要限制。

2.3 IPM(Inverse Perspective Mapping,逆透视变换)

IPM 本质上就是 Homography 的一种特例。只不过它更强调"透视去除":把相机的透视效果去掉,恢复成地面上物体在俯视图中的真实形状和尺寸。

两者的关系可以用一句话概括:

如果知道相机的内参 K、外参 R/t 和地面平面方程,可以直接推导出 H。如果不知道这些,也可以在地面上放几个已知物理位置的点,通过图像像素和地面坐标的对应关系直接求解 H。

很多环视工程(包括本文分析的这个)直接用第二种方法:在地面上铺标定布/放标志点,手动或自动点击图像中的对应点,然后用 getPerspectiveTransform 求 H


3. 四路鱼眼环视的大致流程

一个典型的四路鱼眼 BEV 系统,数据流如下:

四路鱼眼原始图像
   ↓
去畸变(每个相机用自己的 K 和 D)
   ↓
透视变换(每个相机用自己的 project_matrix H 映射到 BEV 平面)
   ↓
四路投影图拼到同一张画布上
   ↓
重叠区域融合(避免拼接缝)
   ↓
亮度均衡(让四路图像的亮度看起来一致)
   ↓
输出最终 BEV 鸟瞰图

第一步(标定)是离线跑好的:算每个相机的内参 K/D,以及从相机视角到 BEV 俯视的映射矩阵 H。

第二步(实时运行)就是不断采集图像→去畸变→投影→拼接→显示。

四路鱼眼 BEV 数据流图:从原始图像到鸟瞰图的完整链路


4. surround-view-system-introduction 仓库概述

  • 仓库地址:https://github.com/neozhaoliang/surround-view-system-introduction
  • 许可证:MIT
  • 定位:用最少 Python 代码展示鱼眼环视全景系统的完整流程,面向初学者,追求可读性而非工程优化
  • 架构:标定阶段离线执行(3 个脚本),运行阶段在线多线程(PyQt5 QThread)
  • 依赖:Python 3, OpenCV 3+, PyQt5, NumPy

5. 阶段 1:棋盘格内参标定

脚本run_calibrate_camera.py

功能:对每个相机单独采集棋盘格图像,用 OpenCV fisheye 模型标定内参。

典型命令

python run_calibrate_camera.py \
  -i 0 \                          # 摄像头设备号
  -grid 9x6 \                     # 棋盘格内角点数
  -out fisheye.yaml \             # 输出 YAML 路径
  -framestep 20 \                 # 每 20 帧取一帧
  --resolution 640x480 \          # 采集分辨率
  --fisheye                       # 使用 fisheye 模型

输出:每个相机一个 YAML 文件(如 yaml/front.yaml),包含:

  • camera_matrix(3×3):鱼眼内参 K
  • dist_coeffs(4×1):鱼眼畸变系数 [k1, k2, k3, k4]
  • resolution(2×1):图像分辨率

6. 阶段 2:手动选点求投影矩阵

脚本run_get_projection_maps.py

功能:在去畸变后的图像上手动点击 4 个地面标志点,计算图像到 BEV 平面的透视变换矩阵。

典型命令

python run_get_projection_maps.py \
  -camera front \
  -scale 0.7 0.8 \      # 去畸变后缩放(方便看到标定点)
  -shift -150 -100       # 去畸变后平移

操作流程

  1. 弹出窗口显示去畸变后的图像
  2. 用鼠标按顺序点击标定布上的 4 个特征点
  3. 按回车预览投影结果
  4. 满意则保存,不满意按 q 重来

代码逻辑:

src = np.float32(gui.keypoints)   # 用户点的 4 个图像像素坐标
dst = np.float32(dst_points)      # 这 4 个点在 BEV 图中的目标坐标
project_matrix = cv2.getPerspectiveTransform(src, dst)

输出:向 YAML 文件中追加:

  • project_matrix(3×3):图像到 BEV 的透视变换矩阵
  • scale_xy:用户指定的缩放系数
  • shift_xy:用户指定的平移量

注意:用户点击的顺序必须和 project_keypoints 中定义的 dst 点顺序一致,否则投影结果会旋转或镜像。

手动点选界面:在去畸变图像上按顺序点击标定布角点


7. 阶段 3:生成融合权重

脚本run_get_weight_matrices.py

功能:计算 4 个相机在重叠区域的融合权重,让拼接缝尽可能自然。

算法(距离加权法)

  1. 4 个相机的投影图各自裁出在 BEV 中的对应区域
  2. 在相邻两路的重叠区中,提取两个相机各自的独占区域轮廓
  3. 对重叠区中每个像素,计算 w = d_B² / (d_A² + d_B²),其中 d_A 是该像素到相机 A 独占区的距离
  4. 这意味着:像素离谁的独占区近,谁的权重就大

输出

  • weights.png:RGBA 四通道图像,每个通道存储一个相机在重叠区的权重矩阵
  • masks.png:RGBA 四通道图像,每个通道存储一个相机的重叠区 mask

下面是前方相机单独投影到 BEV 后的效果,可以看到标定布区域被拉成了规整矩形,但远处非地面区域出现了拉伸:

前方相机单独投影到 BEV 后的效果


8. 阶段 4:实时多线程运行

脚本run_live_demo.py

功能:多线程实时采集四路相机图像,在线完成去畸变→投影→拼接→显示。

线程架构

  • 4 个采集线程(CaptureThread):各自从一个摄像头读取帧
  • 1 个帧同步缓冲(MultiBufferManager):保证四路图像时间戳对齐
  • 4 个处理线程(CameraProcessingThread):各自完成 undistort → project → flip
  • 1 个拼接对象(BirdView):stitch_all_parts() 完成四路融合

每帧处理流水线

# fisheye_camera.py
undistorted = cv2.remap(frame, map1, map2, cv2.INTER_LINEAR)
projected = cv2.warpPerspective(undistorted, project_matrix, project_shape)
flipped = flip(projected)  # 按相机方向翻转/转置

# birdview.py
birdview = stitch_all_parts(front, back, left, right)
birdview = make_luminance_balance(birdview, masks)  # 亮度均衡
final = place_car_image(birdview, car_image)         # 车体图覆盖

实时运行线程模型:采集线程→帧同步→处理线程→融合输出


9. 参数模型解析

param_settings.py 中的参数定义的是 BEV 俯视图的物理几何布局,不是可以随意调整的数值。理解这组参数的含义是使用和适配这个工程的关键。

9.1 横向布局

shift_w | board_w | inn_shift_w | car_w | inn_shift_w | board_w | shift_w
  • car_w:车宽(单位与像素 1:1 时就是 cm)
  • board_w:侧面标定布的宽度
  • inn_shift_w:标定布内侧边界到车身侧面的距离
  • shift_w:标定布外侧再向外扩展的留白

9.2 纵向布局

shift_h | board_h | inn_shift_h | car_h | inn_shift_h | board_h | shift_h

对应的四个参数与横向类似,只是沿车身前后方向。

9.3 从参数计算关键量

total_w = car_w + 2*inn_shift_w + 2*board_w + 2*shift_w   # BEV 画布总宽度
total_h = car_h + 2*inn_shift_h + 2*board_h + 2*shift_h   # BEV 画布总高度

xl = shift_w + board_w + inn_shift_w       # 车体左边界在 BEV 图中的 x 坐标
xr = xl + car_w                             # 车体右边界
yt = shift_h + board_h + inn_shift_h       # 车体上边界
yb = yt + car_h                             # 车体下边界

9.4 project_shapes 和 project_keypoints

project_shapes[camera]:该相机 warpPerspective 后输出图像的尺寸 (width, height)

project_keypoints[camera]:该相机画面中被点选的 4 个标定点,在最终 BEV 图中的目标坐标。

这两个参数的关键在于:

  • 它们定义的是你期望的投影结果,不是标定算法自动算出来的
  • 它们的值取决于标定布的尺寸和在 BEV 图中的位置
  • 如果你点的是整个标定布的外角点,坐标就应该对应标定布整体的位置;如果你点的是棋盘格内部的角点,坐标就应该缩进

9.5 拼接区域(9 宫格)

最终 BEV 图被 xl, xr, yt, yb 四个边界线切成一个 3×3 = 9 宫格:

 FL  |  F  |  FR
-----+-----+-----
  L  |  C  |  R
-----+-----+-----
 BL  |  B  |  BR
  • F/B/L/R = 前/后/左/右单视野区
  • FL/FR/BL/BR = 四个角的重叠区(需要融合权重)
  • C = 车体区域(用车标图片覆盖)

四路相机重叠区域示意图:白色区域表示相邻相机视野的重叠部分

9.6 适配新车辆时的修改顺序

如果要把这个工程用到自己的车上,按以下顺序改参数:

  1. car_w / car_h(车身尺寸)
  2. inn_shift_w / inn_shift_h(标定布到车的距离)
  3. shift_w / shift_h(扩展视野范围)
  4. 重算 total_w / total_h / xl / xr / yt / yb
  5. 更新 project_shapesproject_keypoints
  6. 重跑阶段 1 + 2(内参不变的话可以只重跑阶段 2 + 3)

BEV 画布几何参数示意图:标注 shift、board、innerShift、car 等关键参数的含义


10. 复现时候踩过的坑

  1. project_keypoints 和点选顺序不一致 → 投影图旋转/镜像
  2. 改了 xl/xr/yt/yb 但没重跑阶段 3 → weights.png 用旧的区域定义,拼出来对不齐
  3. 棋盘格只集中在画面中间 → 畸变矫正不充分,边缘投影不准
  4. 四个相机的内参不匹配 → 去畸变后的图像尺度不一致,拼接错位
  5. OpenCV 版本差异:OpenCV 4.x 的 cv2.FileStorage 加载 2×1 矩阵返回 2D array,需要 .flatten() 才能当 1D 使用
  6. PyYAML 和项目内的 yaml/ 目录冲突:Python 导入优先级导致 import yaml 导到了项目目录而不是 PyYAML 库
从四路鱼眼到鸟瞰图:BEV 环视原理与 surround-view-system-introduction 工程精读
作者
若离
发表于
2026-05-12
License
CC BY-NC-SA 4.0

评论