4、相机内参标定:张正友标定法原理、OpenCV实现、畸变校正、标定结果评估
相机内参标定,说白了就是搞清楚你的相机到底长什么样。每颗镜头都有脾气,有的画面边缘会拉变形,有的像素不是正方形,有的主点不在正中间。这些偏差,就是内参要解决的问题。
我最早接触标定是在一个移动机器人项目上,当时用的工业相机,随便找了个标定板拍了几张图就跑OpenCV脚本。结果视觉里程计跑起来,误差大得离谱。后来才发现——标定没做好,后面全是白搭。所以这一章,咱们把张正友标定法掰开揉碎讲清楚。
核心观点:相机内参标定是传感器融合的基石。内参不准,外参标定就是空中楼阁。
4.1 张正友标定法原理
张正友标定法,是1998年张正友教授提出的。它最大的贡献是——你不需要知道标定板在三维空间中的精确位置,只需要一个平面棋盘格,拍几张不同角度的照片就行。
为什么能这样?其实原理不复杂。棋盘格上的角点坐标,在标定板坐标系下是已知的(比如每个格子宽30mm)。相机拍到的图像,是这些三维点投影到二维像素平面的结果。这个投影过程,可以用一个3×3的内参矩阵K来描述。
张正友的思路是:先忽略畸变,用单应性矩阵把棋盘格平面和图像平面联系起来。然后利用多张图片的约束,解出内参的闭式解。最后再用非线性优化,把畸变参数也一起迭代出来。
嗯,这里要注意——他假设标定板是平面,所以至少需要3张不同姿态的图片才能解出所有内参。我个人的习惯是拍15到20张,覆盖视野的各个角落。
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跑起来还是飘。后来发现是标定板只拍了中心区域,边缘畸变根本没被约束。所以评估时一定要看校正后的图像,边缘的直线是不是真的变直了。
最后说一句,标定不是一次性的活。镜头松动、温度变化、对焦环转动,都会改变内参。我一般在每次传感器融合实验前,都会快速跑一遍标定,确保内参是最新的。
可可精品资料库