相机标定实战:张正友标定法原理、棋盘格角点检测、使用OpenCV进行相机标定
大家好,我是你们的老朋友。今天咱们来聊聊相机标定。
说实话,做机器人这么多年,我踩过最多的坑,就是传感器没标好。你想想看,一个没标定的相机,就像近视眼没戴眼镜——看啥都是模糊的,测距更是没谱。所以,多传感器融合的第一步,就是把相机给“校准”了。
张正友标定法:为什么它成了行业标准?
张正友标定法,说白了就是“用棋盘格搞定相机”。这个方法牛在哪?它不需要你知道相机在三维空间里的精确位置,只需要你拿着棋盘格在相机前晃一晃,拍几张照片就行。
我记得2008年刚入行那会儿,大家还在用传统的标定块,又贵又麻烦。后来张正友的论文一出来,整个行业都炸了。为什么?因为它把标定从“实验室专属”变成了“人人可做”。
它的核心思想其实很简单:
- 棋盘格上的角点——这些点的三维坐标我们是知道的(假设Z=0)
- 相机拍到的像素点——这些点的二维坐标我们能检测到
- 建立映射关系——通过多张照片,解算出相机的内参和外参
嗯,这里要注意:张正友法假设棋盘格是平面,所以Z坐标统一为0。这个假设很关键,也是它计算效率高的原因。
核心公式(简化版):
s * [u, v, 1]^T = K * [R | t] * [X, Y, 0, 1]^T
其中K是内参矩阵,R和t是外参,s是尺度因子。
棋盘格角点检测:别小看这一步
角点检测听起来简单,做起来全是坑。我在项目中遇到过最头疼的问题——光照不均匀。棋盘格一半亮一半暗,OpenCV直接罢工。
OpenCV提供了两个主要函数:
cv2.findChessboardCorners()——粗检测cv2.cornerSubPix()——亚像素精化
为什么要亚像素精化?因为粗检测只能到像素级,而标定需要亚像素精度。我习惯把亚像素迭代次数设到30次,精度基本就够了。
我的小技巧:
拍摄棋盘格时,尽量让棋盘格占画面1/3以上。太小了,角点检测容易失败。另外,棋盘格要稍微倾斜一点,别正对着相机——这样能更好地解算畸变参数。
使用OpenCV进行相机标定:实战代码
好了,理论说完了,咱们直接上代码。这是我常用的标定流程:
import cv2
import numpy as np
import glob
# 1. 准备棋盘格参数
CHECKERBOARD = (9, 6) # 内角点数量
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 2. 准备三维点
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 = [] # 三维点
imgpoints = [] # 二维点
# 3. 读取图片,检测角点
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('Corners', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
# 4. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
这段代码我用了不下百次。你可能会问:为什么用9x6的棋盘格?其实6x4、8x5都行,但9x6的角点数量适中,计算稳定。
避坑指南:
我曾经拍了一组照片,标定结果死活不对。后来发现,照片里棋盘格的位置太集中了——全在画面中央。标定需要棋盘格出现在画面的各个角落,尤其是边缘。畸变参数主要靠边缘数据来解算,中央区域的数据对畸变不敏感。
标定结果怎么看?
标定完成后,你会得到两个关键结果:
| 参数 | 含义 | 正常范围 |
|---|---|---|
| fx, fy | 焦距(像素单位) | 几百到几千 |
| cx, cy | 主点坐标 | 接近图像中心 |
| k1, k2, p1, p2 | 畸变系数 | 绝对值小于1 |
我一般会先看重投影误差。OpenCV会返回一个ret值,单位是像素。如果这个值小于0.5,说明标定质量不错。大于1.0的话,建议重新拍一组照片。
标定后的验证
标定完别急着用,先验证一下。我习惯用cv2.undistort()把一张原始图片去畸变,看看直线是不是变直了。
# 去畸变
img = cv2.imread('test.jpg')
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# 裁剪图像
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv2.imshow('Undistorted', dst)
cv2.waitKey(0)
嗯,这里有个细节:getOptimalNewCameraMatrix的alpha参数设为1,表示保留所有像素。设为0的话,会裁剪掉畸变校正后的黑色边缘。我一般设0.5,既保留视野又减少黑边。
总结一下标定流程:
- 打印棋盘格,贴在硬纸板上
- 拍摄15-20张不同角度的照片
- 用OpenCV检测角点
- 执行标定,获取内参和畸变系数
- 验证标定质量
最后说一句:相机标定是传感器融合的基石。你花半小时把相机标好了,后面做双目、做IMU融合都会顺风顺水。别嫌麻烦,这步省不了。