相机标定实战:张正友标定法原理、棋盘格角点检测、使用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,既保留视野又减少黑边。

总结一下标定流程:

  1. 打印棋盘格,贴在硬纸板上
  2. 拍摄15-20张不同角度的照片
  3. 用OpenCV检测角点
  4. 执行标定,获取内参和畸变系数
  5. 验证标定质量

最后说一句:相机标定是传感器融合的基石。你花半小时把相机标好了,后面做双目、做IMU融合都会顺风顺水。别嫌麻烦,这步省不了。

相机标定全流程 准备棋盘格 拍摄多张照片 角点检测 执行标定 验证标定质量 输出:内参 + 畸变系数 关键参数:fx, fy, cx, cy, k1, k2, p1, p2 重投影误差 < 0.5 像素为合格

专注资料整理