4、相机标定实战:张正友标定法原理、使用OpenCV进行棋盘格标定、标定结果评估

相机标定,说白了就是给相机做一次「视力矫正」。你想想看,镜头天生就有畸变,传感器也不是完美平面,拍出来的图像跟真实世界之间总差那么一口气。我刚开始做机器视觉项目时,总觉得标定是件麻烦事,能省就省。结果呢?测量精度差了2个毫米,项目差点黄了。从那以后,我再也不敢跳过这一步了。

这一章,咱们就聊聊张正友标定法——目前工业界最主流的标定方法。我会带着你从原理到代码,再到结果评估,完整走一遍。

4.1 张正友标定法:为什么它成了行业标准?

张正友标定法,是1998年张正友教授提出的一种基于平面棋盘格的标定方法。它最大的优点就是:你不需要一个精密的3D标定块,只需要打印一张棋盘格,拍几张不同角度的照片就行。

它的核心思想其实不复杂:

  1. 建立对应关系:棋盘格上的角点,在真实世界中的坐标是已知的(比如每个格子30mm),在图像中的坐标可以通过角点检测得到。
  2. 求解单应性矩阵:每张棋盘格照片,都对应一个从世界平面到图像平面的单应性矩阵。
  3. 分解内参外参:利用多张照片的单应性矩阵,通过最小二乘法求解出相机的内参矩阵和畸变系数。
  4. 非线性优化:最后用最大似然估计对所有参数进行全局优化。

关键点:张正友法假设棋盘格是平面,所以只需要一个平面标定板。这大大降低了标定门槛。

我在一个自动化产线项目中用过这个方法。当时客户要求一天内完成标定,我打印了一张A3棋盘格,用手机支架拍了15张照片,半小时就搞定了。要是用传统的3D标定块,光找块高精度标定块就得等三天。

4.2 棋盘格标定的完整流程

咱们把整个流程拆成6步,每一步都有坑,我一个个说。

步骤 操作内容 常见问题
1. 制作棋盘格 打印棋盘格,贴在硬板上 纸张不平整、格子尺寸不准
2. 拍摄标定图像 从不同角度拍摄10-20张 角度变化不够、光照不均
3. 角点检测 使用OpenCV的findChessboardCorners 棋盘格被遮挡、反光严重
4. 亚像素细化 使用cornerSubPix提高精度 迭代次数不足
5. 标定计算 调用calibrateCamera 图像数量不够、分布不均
6. 结果评估 计算重投影误差 误差大于0.5像素需重新标定

4.3 使用OpenCV进行棋盘格标定

好了,咱们直接上代码。我习惯用Python+OpenCV,因为调试方便,工业部署时再转C++。

先准备棋盘格。我建议用9×6的棋盘格(内部角点8×5),格子大小30mm。为什么是9×6?因为角点数是(9-1)×(6-1)=8×5=40个点,足够求解参数了。

import cv2
import numpy as np
import glob

# 1. 准备棋盘格参数
CHECKERBOARD = (8, 5)  # 内部角点数量
SQUARE_SIZE = 30.0     # 格子大小,单位mm

# 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)
objp *= SQUARE_SIZE

# 3. 存储所有图像的点
objpoints = []  # 世界坐标点
imgpoints = []  # 图像坐标点

# 4. 读取所有标定图像
images = glob.glob('calib_images/*.jpg')

for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 5. 查找棋盘格角点
    ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None)
    
    if ret:
        objpoints.append(objp)
        
        # 6. 亚像素细化
        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)
        
        # 7. 绘制并显示角点
        cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
        cv2.imshow('Corners', img)
        cv2.waitKey(500)

cv2.destroyAllWindows()

# 8. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None
)

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)

我的经验:拍摄时,让棋盘格占图像面积的1/3到1/2。太小了角点检测不准,太大了边缘畸变信息不够。我一般拍15-20张,覆盖图像的9个区域(左上、中上、右上、左中...),再加几张倾斜的。

4.4 标定结果评估:别被低误差骗了

标定完,第一件事就是看重投影误差。这个值怎么算的?就是把标定得到的参数,把世界坐标点投影回图像,跟实际检测到的角点位置比一下,算平均距离。

一般来说:

  • 小于0.3像素:优秀,可以用于高精度测量
  • 0.3-0.5像素:良好,大多数应用够用
  • 0.5-1.0像素:一般,需要检查标定过程
  • 大于1.0像素:重新标定吧,肯定哪里出问题了

我曾经踩过的坑:有一次标定误差只有0.15像素,我特别高兴。结果实际测量时,误差还是很大。后来发现,我拍的15张照片全是在同一个角度拍的,虽然每张的角点检测很准,但整体约束不够。记住:低重投影误差不代表标定正确,还要看参数物理合理性。

怎么判断参数是否合理?我一般看这几个:

  1. 焦距:跟镜头标称值对比,偏差不超过10%
  2. 主点:应该在图像中心附近,偏差不超过图像尺寸的5%
  3. 畸变系数:k1、k2一般在-1到1之间,太大或太小都不正常

咱们可以写个函数,把标定结果可视化一下:

def evaluate_calibration(mtx, dist, objpoints, imgpoints, img_size):
    # 计算每张图像的重投影误差
    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"平均重投影误差: {total_error/len(objpoints):.4f} 像素")
    
    # 检查主点位置
    h, w = img_size
    cx, cy = mtx[0, 2], mtx[1, 2]
    print(f"主点位置: ({cx:.1f}, {cy:.1f}), 图像中心: ({w/2:.1f}, {h/2:.1f})")
    print(f"偏移量: ({abs(cx - w/2):.1f}, {abs(cy - h/2):.1f}) 像素")

4.5 标定流程全景图

我把整个标定流程画成了一张图,方便你对照着操作:

张正友标定法实战流程 1. 制作棋盘格 打印9×6格子,贴硬板 2. 拍摄标定图像 15-20张,多角度 3. 角点检测 findChessboardCorners 4. 亚像素细化 cornerSubPix 5. 标定计算 calibrateCamera 6. 结果评估 重投影误差 误差 合格? 是 ✅ 标定完成,保存参数 内参矩阵 + 畸变系数 否 ❌ 重新标定 检查图像质量、增加角度 💡 实战要点 • 棋盘格必须平整,建议贴在玻璃或铝板上 • 拍摄时避免反光,使用漫射光源 • 图像数量不是越多越好,15-20张足够 • 重投影误差 < 0.3像素才算优秀

4.6 常见问题与避坑指南

做标定这么多年,我总结了几条最常踩的坑:

我曾经犯过的错:

  • 棋盘格不平整:有一次我用普通打印纸贴在三合板上,结果纸受潮起皱了。标定出来的畸变系数完全不对。后来我改用PVC板,再也没出过问题。
  • 只拍中心区域:新手最容易犯的错。棋盘格永远在图像中心,边缘的畸变信息完全没拍到。标定出来的边缘畸变校正效果很差。
  • 忽略光照:光照不均会导致角点检测偏移。我习惯用两个漫射灯从两侧打光,保证棋盘格表面亮度均匀。

我的小技巧:标定完成后,我通常会做一次「验证标定」——拍一张棋盘格放在视野边缘的照片,用标定参数校正后,看看边缘的格子是不是变直了。如果边缘还是弯的,说明标定不够好。

嗯,相机标定这块内容就这些。记住一句话:标定是机器视觉的基石,地基没打好,上层建筑再漂亮也是白搭。花半小时把标定做好,后面能省你三天的时间去排查问题。