第三章 相机内参标定实战:使用OpenCV进行棋盘格标定
相机内参标定,说白了就是搞清楚你的相机到底是怎么「看」世界的。每台相机都有自己独特的脾气——镜头畸变、焦距偏差、主点偏移,这些都需要通过标定来摸清楚。我在项目里见过太多人拿着没标定过的相机直接做三维重建,结果出来的点云歪七扭八,那叫一个惨。
这一章,我们就用OpenCV把棋盘格标定这件事彻底讲透。从角点检测到标定流程,再到结果验证,每一步我都会把实战中踩过的坑告诉你。
3.1 棋盘格标定的基本原理
棋盘格标定,本质上是在求解一个「从三维世界到二维图像」的映射关系。这个映射由两部分组成:
- 内参矩阵:焦距(fx, fy)、主点(cx, cy),描述相机本身的几何特性
- 畸变系数:径向畸变(k1, k2, k3)和切向畸变(p1, p2),描述镜头的光学缺陷
你想想看,棋盘格上的每个角点,在三维空间中的位置是已知的(比如角点间距30mm)。相机拍下来后,这些角点在图像上的像素坐标也是已知的。有了这些对应关系,我们就能反推出相机的内参和畸变。
核心公式(简化版):
s * [u, v, 1]^T = K * [R|t] * [X, Y, Z, 1]^T
其中K就是内参矩阵,[R|t]是外参,[X,Y,Z]是世界坐标,[u,v]是像素坐标。
3.2 棋盘格的选择与准备
棋盘格的选择,我建议你注意这几点:
- 格子数量:内角点数量建议在9×6到12×9之间。太少解不准确,太多容易过拟合
- 格子尺寸:打印出来后用游标卡尺量一下实际尺寸,别信打印机的设置。我吃过这个亏,打印出来的格子实际是29.8mm,不是30mm
- 材质:用哑光纸打印,别用光面纸。光面纸反光严重,角点检测容易失败
- 贴在硬板上:软纸会弯曲,影响标定精度。我习惯贴在亚克力板或者铝板上
我的小技巧:在棋盘格四周留一圈白边,方便手持时不会遮挡角点。另外,棋盘格不要太小,A4纸大小就挺好。
3.3 图像采集的实战要点
图像采集这一步,很多人觉得随便拍几张就行。嗯,这里要注意,采集质量直接决定标定结果的好坏。
我一般会拍20-30张,覆盖以下场景:
- 不同角度:左右倾斜、上下倾斜、旋转,让棋盘格在图像中呈现各种姿态
- 不同距离:从近到远,覆盖相机的工作距离范围
- 不同位置:棋盘格出现在图像的四个角和中心区域
- 光照均匀:避免强光直射或阴影遮挡棋盘格
避坑指南:我曾经为了省事只拍了8张图,结果标定出来的焦距偏差了3%。后来老老实实拍了25张,结果就稳定了。记住,数量不够,精度来凑——但精度凑不了。
3.4 角点检测实战代码
OpenCV提供了现成的角点检测函数,但用起来有几个坑。先看代码:
import cv2
import numpy as np
import glob
# 棋盘格内角点数量(列数-1,行数-1)
CHECKERBOARD = (9, 6)
# 棋盘格单个格子尺寸(毫米)
SQUARE_SIZE = 30.0
# 准备世界坐标系中的点
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 *= SQUARE_SIZE
# 存储所有图像的点
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)
# 亚像素级角点优化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
imgpoints.append(corners2)
# 可视化角点
cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
cv2.imshow('Corners', img)
cv2.waitKey(100)
else:
print(f"角点检测失败: {fname}")
cv2.destroyAllWindows()
这段代码里,我特别想强调两个地方:
- 亚像素优化:
cornerSubPix这一步不能省。原始角点检测的精度是像素级的,优化后能达到0.1像素级别。标定精度差0.1像素,三维重建可能就差几毫米 - 角点顺序:OpenCV返回的角点顺序是从左上角开始,按行排列。如果你自己写可视化代码,要注意这个顺序
3.5 标定流程与参数求解
角点检测完成后,标定就简单了:
# 相机标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:")
print(mtx)
print("\n畸变系数:")
print(dist)
print(f"\n重投影误差: {ret:.4f} 像素")
calibrateCamera函数内部用的是张正友标定法,它会自动求解内参、畸变和外参。返回的ret就是重投影误差,这个值后面会详细分析。
输出示例:
| 参数 | 值 |
|---|---|
| fx | 823.4567 |
| fy | 824.1234 |
| cx | 640.2345 |
| cy | 360.6789 |
| k1 | -0.2345 |
| k2 | 0.1234 |
| p1 | 0.0012 |
| p2 | -0.0008 |
3.6 结果验证:重投影误差分析
重投影误差,说白了就是「用标定结果反算回去,看看角点位置偏差了多少」。这个值越小,标定越准。
我一般会做两件事来验证标定结果:
- 计算平均重投影误差:小于0.5像素算合格,小于0.2像素算优秀
- 可视化每张图的误差分布:看看是不是某几张图误差特别大,如果是,说明那些图有问题
# 计算每张图的误差
total_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)
total_error += error
print(f"图像 {i+1} 的重投影误差: {error:.4f} 像素")
print(f"\n平均重投影误差: {total_error/len(objpoints):.4f} 像素")
我的经验:如果某张图的误差超过1像素,我建议直接删掉那张图重新标定。通常是因为那张图拍糊了或者棋盘格没放平。删掉后重新标定,整体误差能降30%以上。
3.7 畸变校正与可视化
标定完,当然要看看效果。畸变校正后的图像,直线应该是直的:
# 畸变校正
img = cv2.imread('test_image.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)
dst2 = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
# 裁剪掉黑边
x, y, w2, h2 = roi
dst = dst[y:y+h2, x:x+w2]
cv2.imshow('Original', img)
cv2.imshow('Undistorted', dst)
cv2.waitKey(0)
这里有个细节:getOptimalNewCameraMatrix的alpha参数控制着图像缩放。alpha=1时保留所有像素(会有黑边),alpha=0时裁剪掉黑边。我一般用alpha=0.5,既保留大部分视野,又去掉黑边。
3.8 常见问题与解决方案
做标定这么多年,我总结了一些高频问题:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 角点检测失败 | 光照不均、棋盘格反光 | 调整光照,使用哑光棋盘格 |
| 重投影误差过大 | 图像数量不足、棋盘格未贴平 | 增加图像数量,确保棋盘格平整 |
| 畸变校正后图像扭曲 | 畸变系数异常 | 检查标定图像是否覆盖了图像边缘 |
| fx和fy差异过大 | 相机传感器像素不是正方形 | 检查相机型号,部分工业相机确实如此 |
曾经踩过的坑:有一次标定结果畸变系数特别大,k1到了-0.8。我排查了半天,发现是棋盘格没贴平,中间鼓起来一块。重新贴平后,k1变成了-0.23,正常了。所以,棋盘格一定要贴在绝对平整的表面上。
3.9 标定结果的保存与加载
标定结果当然要保存下来,方便后续使用:
# 保存标定结果
np.savez('calibration_result.npz', mtx=mtx, dist=dist,
newcameramtx=newcameramtx, roi=roi)
# 加载标定结果
with np.load('calibration_result.npz') as data:
mtx = data['mtx']
dist = data['dist']
newcameramtx = data['newcameramtx']
roi = data['roi']
我个人习惯把标定结果和对应的图像一起保存,方便以后复现。毕竟,标定参数变了,整个系统的精度都会受影响。
3.10 本章小结
相机内参标定,说白了就是一次「相机和世界的对话」。通过棋盘格这个翻译官,我们让相机说出了自己的秘密——内参和畸变。
这一章我们走完了完整流程:从棋盘格准备、图像采集、角点检测,到标定求解、结果验证、畸变校正。每一步都有坑,但只要你按照流程来,结果不会差。
下一章,我们会进入多相机标定,看看如何让两台相机「对齐」它们的视野。到时候,你会发现内参标定只是热身,真正的挑战才刚刚开始。
核心要点回顾:
- 棋盘格标定是相机内参标定的标准方法,精度可达0.1像素级
- 图像采集要覆盖不同角度、距离和位置,20-30张为宜
- 亚像素角点优化是提升精度的关键步骤
- 重投影误差小于0.5像素算合格,小于0.2像素算优秀
- 畸变校正后要检查直线是否变直,这是最直观的验证方法
公众号:蓝海资料掘金营,微信deep3321