4、单目相机标定:张正友标定法原理、棋盘格角点检测、内参矩阵与畸变系数求解
说到相机标定,这可是我们做多摄像头融合的第一步,也是最基础的一步。你想想看,如果单目相机都没标准,后面谈什么融合?
我个人习惯把标定比作给相机「配眼镜」。每台相机出厂时都有点「近视」或「散光」,标定就是测出它的度数,然后配一副合适的镜片。今天我们就聊聊最经典的张正友标定法。
4.1 张正友标定法:为什么它成了行业标准?
张正友标定法,说白了就是拿一块棋盘格,在不同角度拍几张照片,然后算出相机的内参和畸变。这个方法1998年提出,到现在二十多年了,依然是工业界的主流。
为什么这么火?因为它解决了两个痛点:
- 不需要精密的3D标定物——一张打印的棋盘格就够了
- 操作简单——拍10-20张不同角度的照片就行
我在项目中遇到过用高精度标定板的情况,那玩意儿贵得要命,而且容易损坏。后来换了张正友法,打印一张A4纸,贴在硬纸板上,效果一样好。嗯,这里要注意,打印的棋盘格一定要平整,别皱巴巴的。
4.2 棋盘格角点检测:从图像到坐标
标定的第一步,就是找到棋盘格上那些黑白交界的角点。这些角点就是我们的「锚点」。
OpenCV里有个现成的函数 cv2.findChessboardCorners(),但它的原理你得懂:
- 图像预处理:转灰度图,做自适应阈值化
- 四边形检测:找到所有可能的棋盘格四边形
- 角点亚像素精化:用
cv2.cornerSubPix()把角点精度提升到亚像素级别
我曾经踩过一个坑:用手机拍标定照片时,棋盘格反光严重,导致角点检测失败。后来我调整了光源角度,让光线均匀散射,问题就解决了。所以建议你拍照时注意光照,别让棋盘格上有高光斑。
cv2.drawChessboardCorners() 可视化检查。如果角点位置偏了,后面的标定结果全是错的。这一步别偷懒。
4.3 内参矩阵:相机的「基因密码」
内参矩阵描述了相机内部的几何特性。它长这样:
K = [fx 0 cx]
[ 0 fy cy]
[ 0 0 1]
其中:
- fx, fy:焦距,单位是像素。说白了就是镜头到成像平面的距离
- cx, cy:主点坐标,通常是图像中心附近
你可能会问:为什么焦距的单位是像素?其实这是把物理焦距和像素尺寸结合了。比如物理焦距4mm,像素尺寸2μm,那fx就是2000像素。这样处理起来更方便。
我记得有一次标定出来的cx偏离中心特别远,检查后发现是相机模组装配有偏差。嗯,这种情况在量产车上挺常见的,所以标定这一步不能省。
4.4 畸变系数:给图像「矫正视力」
镜头畸变主要有两种:
- 径向畸变:图像边缘向里或向外弯曲,像鱼眼效果
- 切向畸变:镜头和成像平面不平行导致的变形
畸变模型用多项式表示:
x_distorted = x * (1 + k1*r^2 + k2*r^4 + k3*r^6) + 2*p1*x*y + p2*(r^2 + 2*x^2)
y_distorted = y * (1 + k1*r^2 + k2*r^4 + k3*r^6) + p1*(r^2 + 2*y^2) + 2*p2*x*y
这里k1、k2、k3是径向畸变系数,p1、p2是切向畸变系数。一般标定5个参数就够了,高精度场景才用到8个。
4.5 求解过程:从角点到参数
张正友法的求解分两步走:
- 闭式解:忽略畸变,用线性方法估算内参和外参的初值
- 非线性优化:用Levenberg-Marquardt算法,把重投影误差降到最小
重投影误差是什么?说白了就是:我们把3D世界点投影到图像上,算出来的位置和实际检测到的角点位置之间的差距。这个差距越小,标定越准。
我一般要求重投影误差小于0.5像素。如果超过1个像素,我会重新拍一组照片。你想想看,一个像素的误差在100米外可能对应几十厘米的位置偏差,这对自动驾驶来说是不能接受的。
4.6 实战代码:OpenCV标定流程
下面是我常用的标定代码框架,你直接拿去用:
import cv2
import numpy as np
import glob
# 棋盘格参数
CHECKERBOARD = (9, 6) # 内角点数量
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 准备3D点坐标
objp = np.zeros((CHECKERBOARD[0]*CHECKERBOARD[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1,2)
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)
# 角点检测
ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None)
if ret:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 可视化检查
cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
cv2.imshow('img', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
# 标定求解
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)
4.7 标定质量评估:别信一次结果
标定完别急着用,先做两件事:
- 重投影误差检查:所有图像的平均误差,我要求<0.3像素
- 畸变矫正可视化:把一张有明显直线的图像做畸变矫正,看看直线是否变直了
我曾经有一次标定结果重投影误差只有0.1像素,但矫正后的图像边缘还是弯的。后来发现是照片数量不够,只拍了8张。加到20张后,问题就解决了。所以别迷信单一指标,多角度验证才靠谱。
好了,单目相机标定就聊到这儿。下一章我们会讲双目相机的立体标定,到时候内外参一起标,更有意思。