4、相机内参标定:张正友标定法原理、OpenCV实现、畸变校正、标定结果评估

相机内参标定,说白了就是搞清楚你的相机到底长什么样。每颗镜头都有脾气,有的画面边缘会拉变形,有的像素不是正方形,有的主点不在正中间。这些偏差,就是内参要解决的问题。

我最早接触标定是在一个移动机器人项目上,当时用的工业相机,随便找了个标定板拍了几张图就跑OpenCV脚本。结果视觉里程计跑起来,误差大得离谱。后来才发现——标定没做好,后面全是白搭。所以这一章,咱们把张正友标定法掰开揉碎讲清楚。

核心观点:相机内参标定是传感器融合的基石。内参不准,外参标定就是空中楼阁。

4.1 张正友标定法原理

张正友标定法,是1998年张正友教授提出的。它最大的贡献是——你不需要知道标定板在三维空间中的精确位置,只需要一个平面棋盘格,拍几张不同角度的照片就行。

为什么能这样?其实原理不复杂。棋盘格上的角点坐标,在标定板坐标系下是已知的(比如每个格子宽30mm)。相机拍到的图像,是这些三维点投影到二维像素平面的结果。这个投影过程,可以用一个3×3的内参矩阵K来描述。

张正友的思路是:先忽略畸变,用单应性矩阵把棋盘格平面和图像平面联系起来。然后利用多张图片的约束,解出内参的闭式解。最后再用非线性优化,把畸变参数也一起迭代出来。

嗯,这里要注意——他假设标定板是平面,所以至少需要3张不同姿态的图片才能解出所有内参。我个人的习惯是拍15到20张,覆盖视野的各个角落。

张正友标定法核心流程 拍摄多张棋盘格图像 至少3张,建议15-20张 提取亚像素角点 findChessboardCorners 计算单应性矩阵 每张图一个H矩阵 闭式解 内参初值 非线性优化 加入畸变参数 迭代优化 Levenberg-Marquardt 输出内参+畸变系数 fx, fy, cx, cy, k1, k2, p1, p2 标定结果:内参矩阵K + 畸变系数D

4.2 OpenCV实现

OpenCV里实现张正友标定,核心就三个函数:findChessboardCorners、calibrateCamera、undistort。但实际用起来,坑不少。

先看代码骨架:

import cv2
import numpy as np
import glob

# 棋盘格参数
CHECKERBOARD = (9, 6)  # 内角点数量
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# 准备世界坐标系中的点
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)

我的经验:标定板一定要打印平整,贴在硬质板上。我曾经用普通A4纸贴在纸箱上,结果标定出来的焦距偏差了3%。另外,拍摄时让棋盘格出现在画面边缘和角落,这对畸变参数的估计至关重要。

4.3 畸变校正

畸变主要分两种:径向畸变和切向畸变。径向畸变是镜头形状导致的,画面边缘的直线会变弯。切向畸变是镜头和成像平面不平行导致的。

OpenCV里畸变模型用5个参数表示:k1, k2, p1, p2, k3。其中k1、k2、k3是径向畸变系数,p1、p2是切向畸变系数。

校正畸变有两种方式:

  • 直接校正:用cv2.undistort(),一步到位
  • 重映射:先算映射表,再用cv2.remap(),适合视频流处理
# 方法一:直接校正
undistorted = cv2.undistort(img, mtx, dist, None, mtx)

# 方法二:重映射(推荐用于视频)
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

注意:校正后图像边缘会被裁剪。用getOptimalNewCameraMatrix的alpha参数可以控制保留多少边缘像素。alpha=0时全部裁剪,alpha=1时全部保留但会有黑边。

4.4 标定结果评估

标定完怎么知道结果好不好?我一般看三个指标:

指标 含义 合格标准
重投影误差 3D点投影到图像上的位置,与实际检测到的角点位置的像素差 < 0.5像素
内参稳定性 多次标定结果的方差 焦距变化 < 1%
畸变校正效果 校正后直线是否变直 目视无明显弯曲

重投影误差可以直接从calibrateCamera的返回值ret拿到。我个人的习惯是,如果误差超过0.3像素,就会检查一下是不是有模糊的图片或者角点提取错了。

# 计算重投影误差
mean_error = 0
for i in range(len(objpoints)):
    imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
    error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
    mean_error += error

print("总重投影误差: {:.3f} 像素".format(mean_error / len(objpoints)))

避坑指南:我曾经遇到一个情况——重投影误差只有0.1像素,但实际视觉SLAM跑起来还是飘。后来发现是标定板只拍了中心区域,边缘畸变根本没被约束。所以评估时一定要看校正后的图像,边缘的直线是不是真的变直了。

最后说一句,标定不是一次性的活。镜头松动、温度变化、对焦环转动,都会改变内参。我一般在每次传感器融合实验前,都会快速跑一遍标定,确保内参是最新的。

可可精品资料库