4、相机内参标定:张正友标定法原理、棋盘格制作、OpenCV标定流程、畸变矫正
相机内参标定,说白了就是搞清楚你的相机到底长什么样。每颗镜头都有脾气,有的画面边缘会扭曲,有的像素不是正方形,有的主点不在正中间。这些偏差如果不矫正,后续的激光雷达和相机融合就是空中楼阁。
我刚开始做融合的时候,就吃过这个亏。当时拿了一个标定好的相机直接上车,结果点云投影到图像上总是偏几个像素。查了两天,最后发现是内参文件拿错了版本。嗯,从那以后,我每次标定完都会亲手验证一遍重投影误差。
4.1 张正友标定法原理
张正友标定法,是1998年张正友教授提出的。它最大的贡献是什么?就是让你不需要一个精密的3D标定物,只需要一张打印的棋盘格,拍几张不同角度的照片就行。
核心思想其实不复杂。你想想看,棋盘格上的角点坐标是已知的(比如每个格子30mm),这些是3D世界坐标。相机拍到的图像上,这些角点变成了2D像素坐标。我们就是要找一个矩阵,能把3D点映射到2D点。
这个映射关系可以写成:
s * [u, v, 1]^T = K * [R|t] * [X, Y, Z, 1]^T
其中:
- K 是内参矩阵,包含fx, fy, cx, cy
- [R|t] 是外参,表示相机相对于棋盘格的位姿
- s 是尺度因子
张正友的巧妙之处在于:他假设棋盘格在Z=0平面上,这样就把3D-2D的映射简化成了单应性矩阵H的求解。然后通过多张不同角度的照片,把内参K和外参R,t解耦出来。
关键点:至少需要3张不同角度的棋盘格照片才能解出所有内参。但实际项目中,我建议拍15-20张,覆盖视野的各个角落。
4.2 棋盘格制作
棋盘格看起来简单,但制作起来有讲究。我在项目中见过有人用A4纸打印,贴在纸板上就开始标定。结果呢?纸板不平,角点检测全偏了。
制作棋盘格要注意以下几点:
- 材质要平整:最好用铝板或亚克力板,表面贴哑光打印纸。不要用普通纸板,容易弯曲。
- 格子尺寸要精确:比如30mm×30mm,误差控制在0.1mm以内。我习惯用游标卡尺量一下打印出来的实际尺寸。
- 黑白格子数量:一般用9×6或11×8的内角点。注意是内角点,不是格子数。比如9×6的棋盘格,实际有10×7个格子。
- 边缘留白:棋盘格四周留1-2cm的白色边框,方便检测时识别边界。
我的小技巧:如果条件允许,去淘宝定制一块陶瓷棋盘格,几十块钱,平整度和精度都比自己打印好太多。省下的时间,够你多标定两遍。
4.3 OpenCV标定流程
OpenCV提供了完整的标定工具,核心函数就几个。我直接给你看一个完整的标定流程代码:
import cv2
import numpy as np
import glob
# 1. 准备棋盘格参数
CHECKERBOARD = (9, 6) # 内角点数量
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 2. 准备世界坐标系中的点
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)
objp = objp * 30 # 乘以实际格子尺寸(mm)
# 3. 存储所有图片的角点
objpoints = [] # 3D点
imgpoints = [] # 2D点
images = glob.glob('calib_images/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 4. 检测棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None)
if ret:
objpoints.append(objp)
# 5. 亚像素精确化
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 6. 可视化角点
cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
cv2.imshow('img', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
# 7. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)
这个流程我用了上百次了,基本没出过问题。但有几个坑要注意:
- 图片数量:少于10张,标定结果不稳定。我一般拍20张左右。
- 角度变化:不要只拍正对着的,要拍倾斜的、旋转的、上下左右的。覆盖整个视野。
- 光照均匀:避免反光和阴影,否则角点检测会失败。
注意:如果cv2.findChessboardCorners返回False,先检查棋盘格尺寸是否设置正确。我曾经因为把(9,6)写成了(6,9),折腾了半小时才找到问题。
4.4 畸变矫正
畸变主要分两种:径向畸变和切向畸变。径向畸变是镜头本身造成的,表现为画面边缘的桶形或枕形变形。切向畸变是镜头和传感器不平行造成的。
OpenCV用5个畸变系数来描述:k1, k2, p1, p2, k3。其中k1,k2,k3是径向畸变,p1,p2是切向畸变。
矫正畸变的代码很简单:
# 读取一张原始图像
img = cv2.imread('test.jpg')
h, w = img.shape[:2]
# 获取最优的相机矩阵(可以裁剪掉畸变区域)
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
# 方法1: 使用undistort
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# 方法2: 使用remap(效率更高,适合视频流)
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
# 裁剪图像(去掉黑边)
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv2.imshow('original', img)
cv2.imshow('undistorted', dst)
cv2.waitKey(0)
这里有个细节:getOptimalNewCameraMatrix的alpha参数控制输出图像的缩放。alpha=0时,会裁剪掉所有畸变区域,图像变小但无黑边。alpha=1时,保留所有像素,但会有黑边。我一般用alpha=0.5,平衡一下。
验证标定质量:标定完成后,一定要算一下重投影误差。OpenCV返回的ret值就是平均误差,单位是像素。一般小于0.5像素就算合格。如果大于1像素,说明标定有问题,需要重新拍图。
4.5 知识体系总览
下面这张图总结了相机内参标定的核心流程和关键点:
整个流程走下来,你会发现相机内参标定其实是个体力活。原理不难,难的是把每个细节做到位。我见过太多人标定结果不好,最后发现是棋盘格没贴平,或者照片拍得太少。
记住一句话:标定质量决定了融合精度的上限。花一个小时把内参标定做好,后面能省你三天调参的时间。
可可精品资料库