4、单目相机标定:张正友标定法原理、棋盘格角点检测、内参矩阵与畸变系数求解

说到相机标定,这可是我们做多摄像头融合的第一步,也是最基础的一步。你想想看,如果单目相机都没标准,后面谈什么融合?

我个人习惯把标定比作给相机「配眼镜」。每台相机出厂时都有点「近视」或「散光」,标定就是测出它的度数,然后配一副合适的镜片。今天我们就聊聊最经典的张正友标定法。

4.1 张正友标定法:为什么它成了行业标准?

张正友标定法,说白了就是拿一块棋盘格,在不同角度拍几张照片,然后算出相机的内参和畸变。这个方法1998年提出,到现在二十多年了,依然是工业界的主流。

为什么这么火?因为它解决了两个痛点:

  • 不需要精密的3D标定物——一张打印的棋盘格就够了
  • 操作简单——拍10-20张不同角度的照片就行

我在项目中遇到过用高精度标定板的情况,那玩意儿贵得要命,而且容易损坏。后来换了张正友法,打印一张A4纸,贴在硬纸板上,效果一样好。嗯,这里要注意,打印的棋盘格一定要平整,别皱巴巴的。

4.2 棋盘格角点检测:从图像到坐标

标定的第一步,就是找到棋盘格上那些黑白交界的角点。这些角点就是我们的「锚点」。

OpenCV里有个现成的函数 cv2.findChessboardCorners(),但它的原理你得懂:

  1. 图像预处理:转灰度图,做自适应阈值化
  2. 四边形检测:找到所有可能的棋盘格四边形
  3. 角点亚像素精化:用 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个。

注意: 畸变系数不是越大越好。如果标定出来的k1超过0.5,说明你的镜头畸变很严重,或者标定照片质量有问题。我曾经见过有人标出k1=2.0,结果矫正后的图像边缘都扭曲了——那是照片拍得太少,角度不够丰富。

4.5 求解过程:从角点到参数

张正友法的求解分两步走:

  1. 闭式解:忽略畸变,用线性方法估算内参和外参的初值
  2. 非线性优化:用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)
经验之谈: 标定照片要覆盖整个视场角。我一般拍15-20张,包括左上、右上、左下、右下、中心五个区域,每张照片的棋盘格倾斜角度不同。千万别只拍正对着相机的照片,那样标定出来的畸变系数不准。

4.7 标定质量评估:别信一次结果

标定完别急着用,先做两件事:

  • 重投影误差检查:所有图像的平均误差,我要求<0.3像素
  • 畸变矫正可视化:把一张有明显直线的图像做畸变矫正,看看直线是否变直了

我曾经有一次标定结果重投影误差只有0.1像素,但矫正后的图像边缘还是弯的。后来发现是照片数量不够,只拍了8张。加到20张后,问题就解决了。所以别迷信单一指标,多角度验证才靠谱。

好了,单目相机标定就聊到这儿。下一章我们会讲双目相机的立体标定,到时候内外参一起标,更有意思。