第三章 车载摄像头标定:从理论到实战
摄像头标定,说白了就是给摄像头“配眼镜”。
你想想看,一个镜头装到车上,它拍出来的图像天生就有畸变。广角镜头尤其明显——画面边缘的直线全是弯的。如果不做标定,后面的车道线检测、障碍物测距,全都会跑偏。
我个人习惯把标定分成三块:内参标定、外参标定、畸变校正。咱们一个一个来聊。
3.1 内参标定:张正友法
内参标定,就是搞清楚摄像头本身的“脾气”。
焦距是多少?光心在图像中心吗?像素是正方形还是长方形?这些参数,统称内参矩阵 K。
内参矩阵 K 长这样:
K = [fx 0 cx
0 fy cy
0 0 1]
fx, fy 是焦距(像素单位),cx, cy 是光心坐标。
张正友法,是目前最主流的标定方法。它的核心思路很简单:拍一组棋盘格照片,然后利用棋盘格的角点,反算出内参。
具体步骤:
- 打印一张棋盘格标定板——格子大小要精确,比如 30mm x 30mm。
- 从不同角度拍 15-20 张照片——注意,角度要丰富。左右、上下、倾斜,都要有。
- 提取角点——OpenCV 的
findChessboardCorners()可以自动做。 - 计算内参——调用
calibrateCamera(),一步到位。
我的经验:拍标定照片时,千万别只在一个平面内移动。我见过有人把标定板放在桌上,只转角度不改变距离——结果标出来的内参全是错的。一定要让标定板在三维空间里“飞”起来。
代码示例,我直接贴一段常用的:
import cv2
import numpy as np
# 棋盘格尺寸
pattern_size = (9, 6) # 内角点数量
square_size = 30.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
# 存储所有图片的角点
objpoints = [] # 世界坐标
imgpoints = [] # 图像坐标
images = [...] # 你的图片列表
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
objpoints.append(objp)
imgpoints.append(corners)
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
注意:标定结果好不好,看重投影误差。一般小于 0.5 像素就算合格。如果误差大于 1 像素,说明照片质量有问题,或者角点提取不准。
3.2 外参标定:把摄像头装到车上
内参搞定了,接下来是外参。
外参解决的是“摄像头在车上的什么位置、朝哪个方向看”这个问题。说白了,就是摄像头坐标系和车辆坐标系之间的转换关系。
车辆坐标系一般这样定义:
- X 轴:车头方向
- Y 轴:车身左侧
- Z 轴:垂直向上
外参包含旋转矩阵 R 和平移向量 t。R 告诉你摄像头转了多大角度,t 告诉你摄像头装在车上的哪个位置。
标定方法:
- 在车前方放置标定板——标定板的位置要精确测量,比如距离车头 5 米,偏左 0.5 米。
- 测量标定板在车辆坐标系下的坐标——用卷尺或者激光测距仪。
- 提取标定板在图像中的角点——和前面一样。
- 用 PnP 算法求解外参——OpenCV 的
solvePnP()。
核心代码:
# 已知内参 mtx 和畸变系数 dist
# 世界坐标(车辆坐标系)和图像坐标
object_points = np.array([[0, 0, 0], [1, 0, 0], ...], dtype=np.float32)
image_points = np.array([[100, 200], [150, 220], ...], dtype=np.float32)
# 求解外参
ret, rvec, tvec = cv2.solvePnP(object_points, image_points, mtx, dist)
# 旋转向量转旋转矩阵
R, _ = cv2.Rodrigues(rvec)
print("旋转矩阵:\n", R)
print("平移向量:\n", tvec)
避坑指南:我曾经在标定外参时,把标定板放歪了 2 度。结果测距误差直接飙到 10% 以上。后来我学乖了——用水平仪确认标定板是垂直的,用激光测距仪确认距离。别偷懒,工具要用全。
3.3 畸变校正:让直线变直
畸变分两种:径向畸变和切向畸变。
径向畸变,是镜头本身的光学特性造成的。光线经过透镜边缘时,弯曲程度和中心不一样。结果就是——画面边缘的直线向内弯(桶形畸变)或向外弯(枕形畸变)。
切向畸变,是镜头和成像平面不平行造成的。说白了,就是镜头装歪了。这种畸变会让图像看起来像被“拧”了一下。
畸变模型用几个系数来描述:
- k1, k2, k3:径向畸变系数
- p1, p2:切向畸变系数
校正方法很简单,OpenCV 已经封装好了:
# 假设已经得到 mtx 和 dist
img = cv2.imread("raw_image.jpg")
h, w = img.shape[:2]
# 获取校正后的映射矩阵
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
# 校正
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# 裁剪掉黑边
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv2.imwrite("calibrated_image.jpg", dst)
注意:校正后的图像边缘会有黑边。可以用 getOptimalNewCameraMatrix() 的 alpha 参数控制。alpha=1 保留所有像素(黑边多),alpha=0 裁剪到最小(丢失边缘信息)。我一般用 alpha=0.5,折中一下。
3.4 标定板设计与采集
标定板的设计,直接影响标定精度。
设计要点:
- 棋盘格尺寸:格子边长 20-40mm 比较合适。太小了角点提取不准,太大了标定板不好拿。
- 格子数量:内角点 9x6 或 12x9 都行。格子越多,标定越稳。
- 打印精度:一定要用高精度打印,别用普通 A4 纸。我见过有人用喷墨打印,格子边缘都是锯齿——标出来全是废的。
- 底板:贴在硬质平板上,比如亚克力板或铝板。别贴纸板上,纸板会弯。
采集技巧:
- 光照均匀——避免反光和阴影。我一般在阴天或者室内拍。
- 标定板占画面 1/3 以上——太小了角点提取不准。
- 覆盖整个视场——标定板要出现在画面的各个角落,尤其是边缘。
- 不同距离——近的、远的都要拍。我一般从 0.5 米到 3 米,每隔 0.5 米拍一组。
- 不同角度——左右倾斜 30 度,上下倾斜 20 度,都要有。
我的习惯:每次标定至少拍 20 张照片。拍完后,我会用 drawChessboardCorners() 检查角点是否全部正确提取。如果有几张照片角点提取失败,直接删掉重拍。别凑合,凑合的结果就是标定误差大。
嗯,到这里,车载摄像头标定的核心内容就讲完了。
内参标定解决“摄像头怎么看世界”,外参标定解决“摄像头在车上怎么看世界”,畸变校正是把歪的图像掰正。这三步走完,你的摄像头才算真正“上岗”。
下一章,咱们聊聊图像预处理——从 RAW 图到 YUV,再到各种滤波。到时候见。