3. 环视系统架构

各位同学,今天我们进入环视系统最核心的一步——摄像头标定。说实话,我在刚入行时觉得标定就是个“拍棋盘格、跑脚本”的流水线活。直到有一次,我在某量产项目上发现环视拼接图总是有重影,怎么调参数都搞不定。最后排查了三天,才发现是标定板本身就不合格。嗯,从那以后,我再也不敢小看标定这个环节了。

标定的目的是什么?

简单说,标定就干两件事:去畸变和求内参。你想想看,车载摄像头用的都是广角镜头,甚至鱼眼镜头。拍出来的画面边缘是弯的——直线变成弧线,这就是畸变。如果不做矫正,环视拼接出来的图像根本对不齐。

核心目标:

  • 去畸变:把弯曲的直线拉直,消除径向畸变和切向畸变
  • 求内参:获取焦距(fx, fy)、光心(cx, cy)和畸变系数(k1, k2, p1, p2, k3)
  • 求外参:确定摄像头相对于车辆坐标系的位置和姿态(这个我们后面章节细讲)

内参矩阵长这样:

内参矩阵 K = [fx  0  cx]
              [0  fy  cy]
              [0   0   1]

fx和fy是焦距(单位是像素),cx和cy是光心偏移。畸变系数里,k1/k2/k3管径向畸变(就是那种“桶形”或“枕形”变形),p1/p2管切向畸变(镜头和传感器不平行导致的)。

我个人习惯把标定理解为“给摄像头做体检”。体检报告就是内参和畸变系数。有了这份报告,我们才能把摄像头看到的扭曲世界,还原成真实世界的几何关系。

标定板怎么选?棋盘格 vs 圆点阵

标定板是标定的“尺子”。选错了,后面全白干。市面上主流就两种:棋盘格和圆点阵。我分别说说它们的脾气。

对比项 棋盘格 圆点阵
角点检测 亚像素角点,精度高 圆心拟合,受透视影响小
光照敏感度 中等,反光时角点易丢 较低,圆形轮廓更鲁棒
部分遮挡 角点丢失后无法使用 只要看到足够多圆,仍可标定
OpenCV支持 findChessboardCorners 非常成熟 findCirclesGrid 需要调参
车载场景推荐 室内/实验室标定 产线/户外标定

我在项目中遇到过一个问题:用棋盘格在户外标定,阳光一照,棋盘格反光严重,角点检测直接崩了。后来换成哑光材质的圆点阵标定板,问题就解决了。所以我的建议是——实验室用棋盘格,产线或户外用圆点阵。

避坑指南:

我曾经在某个项目里用了A4纸打印的棋盘格,结果标定出来的内参偏差很大。后来发现纸不平,有褶皱。标定板必须贴在刚性平板上,最好是亚克力或铝板。纸板?千万别用。

标定板尺寸怎么定?

这里有个经验公式:标定板在画面中占比至少30%,最好50%以上。格子边长和圆点直径,我一般选20-40mm。太小了,远距离看不清;太大了,近距离拍不全。

棋盘格的格子数,我推荐9×6或11×8(内角点数)。圆点阵推荐7×7或9×9。记住:格子数必须是奇数×偶数,这样角点才有唯一的方向性。

OpenCV标定流程简介

OpenCV的标定流程,说白了就是三步:找角点 → 算内参 → 去畸变。我画了个流程图,你们感受一下。

OpenCV 摄像头标定流程 1. 采集标定板图像 2. 提取角点/圆心 3. 亚像素精细化 4. 调用 calibrateCamera() 5. 获取内参 + 畸变系数 6. 去畸变 + 验证重投影误差 关键参数 • 棋盘格尺寸 (mm) • 内角点数 (w, h) • 图像数量 ≥ 15 张

核心代码其实不长,我贴一段最常用的:

import cv2
import numpy as np

# 1. 准备棋盘格角点的世界坐标
pattern_size = (9, 6)  # 内角点数
square_size = 25.0     # 格子边长 (mm)
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)
objp *= square_size

# 2. 存储所有图像的世界坐标和像素坐标
objpoints = []  # 世界坐标
imgpoints = []  # 像素坐标

# 3. 遍历图像,找角点
for fname in image_files:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
    if ret:
        # 亚像素精细化
        criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
        corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
        objpoints.append(objp)
        imgpoints.append(corners2)

# 4. 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None
)

# 5. 去畸变
img_undistorted = cv2.undistort(img, mtx, dist, None, mtx)

注意:

标定用的图像至少拍15-20张。要覆盖画面的各个区域——左上、右下、中间、倾斜角度都要有。我曾经只拍了10张,结果标定出来的内参在画面边缘误差很大。后来补到20张,重投影误差从0.8像素降到了0.2像素。

重投影误差怎么看?

标定完成后,OpenCV会返回一个重投影误差(reprojection error)。这个值越小越好。我一般这样判断:

  • < 0.3 像素:优秀,可以量产
  • 0.3 - 0.5 像素:合格,需要检查是否有异常图像
  • > 0.5 像素:不合格,检查标定板或拍摄质量

如果误差偏大,别急着调参数。先检查:标定板平不平?图像有没有模糊?角点检测有没有漏检?我遇到过最离谱的一次,是标定板上有指纹印,导致角点检测偏移了半个像素。

我的习惯:

标定完成后,我会把去畸变后的图像和原图叠在一起看。重点关注画面边缘的直线——比如停车场的白线、建筑物的边缘。如果这些直线在去畸变后变直了,说明标定没问题。如果还有弯曲,那就得重新来过了。