第一章:相机标定技术——张正友标定法

相机标定,说白了就是给相机做「体检」。

我入行那会儿,第一次拿到工业相机拍出来的图像,边缘全是弯的,心里直犯嘀咕:这镜头是不是坏了?后来才明白,这不是坏了,是广角镜头天生的畸变在作怪。你想想看,光线经过一堆透镜,怎么可能还走直线?

所以,标定的核心任务就两个:算出相机的内参(焦距、主点、畸变系数),再算出外参(旋转、平移)。有了这些,我们才能把图像里的像素坐标,还原成真实世界的几何关系。

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

张正友老师在1998年提出的这个方法,我个人认为是相机标定领域最优雅的解决方案之一。为什么这么说?

  • 只需要一个平面棋盘格——不需要精密的3D标定块,打印一张纸就行。
  • 操作简单——从不同角度拍十几张照片,算法自动解算。
  • 精度够用——重投影误差能控制在0.1像素以内,对绝大多数项目来说绰绰有余。

我记得第一次在产线上部署标定程序时,工人师傅问我:「这玩意儿准吗?」我让他随便拍了8张不同角度的棋盘格照片,跑完标定一看,重投影误差0.08像素。师傅竖了个大拇指,从此再没质疑过。

核心思想:张正友法假设标定板是平面(Z=0),通过单应性矩阵建立2D图像点与3D世界点的对应关系,然后利用约束条件求解内参和外参。

1.2 标定板设计——棋盘格 vs 圆点

选哪种标定板?这问题我经常被问到。直接说结论:

类型 优点 缺点 推荐场景
棋盘格 角点检测稳定,亚像素精度高 边缘模糊时容易丢角点 室内、光照均匀、高精度需求
圆点阵列 对畸变不敏感,中心定位准 透视变形后椭圆检测复杂 大广角、鱼眼镜头、光照变化大

我个人习惯用棋盘格,因为OpenCV的findChessboardCorners函数太成熟了,几乎不用调参。但如果你用的是超广角镜头(比如视场角>120°),我建议你试试圆点标定板。为什么?因为棋盘格在图像边缘的角点会被严重拉长,检测算法容易失败。我曾经在一个项目里被这个问题折磨了两天,换成圆点后一次通过。

设计建议:

  • 棋盘格尺寸:7×10或8×11个内角点,方格边长20-30mm
  • 圆点阵列:5×7或7×9个圆点,直径10-15mm,间距2倍直径
  • 打印时用哑光纸,避免反光
  • 贴在刚性平板上,不要弯曲

1.3 OpenCV标定流程——从拍照到出结果

嗯,这里要注意,标定流程看似简单,但每一步都有坑。我直接给你看核心代码,然后逐行解释。

import cv2
import numpy as np
import glob

# 1. 准备世界坐标系中的点
# 假设棋盘格内角点数为 (9, 6),方格边长 25mm
pattern_size = (9, 6)
square_size = 25.0  # mm

objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)
objp *= square_size

# 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)
    
    # 3. 查找棋盘格角点
    ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
    
    if ret:
        objpoints.append(objp)
        
        # 4. 亚像素精细化
        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)
        
        # 5. 可视化(可选)
        cv2.drawChessboardCorners(img, pattern_size, corners2, ret)
        cv2.imshow('Corners', img)
        cv2.waitKey(100)

cv2.destroyAllWindows()

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

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)

这段代码我用了不下百次,有几个关键点你得注意:

  • 角点检测失败? 检查图像是否过曝或过暗,棋盘格是否占画面1/4以上。
  • 亚像素精细化不能省——直接跳过这步,标定精度会掉一个数量级。
  • 图像数量——我建议至少15张,覆盖不同角度和距离。别偷懒,我曾经只拍了8张,结果标定出来的焦距偏差了3%。

1.4 标定结果评估——重投影误差

标定完了,怎么知道结果好不好?最直观的指标就是重投影误差

简单说,就是把标定板上的3D点,用算出来的内参和外参投影回图像,看看跟实际检测到的2D点差多少像素。这个差值越小,说明标定越准。

经验阈值:

  • 优秀:< 0.1 像素
  • 良好:0.1 - 0.3 像素
  • 可接受:0.3 - 0.5 像素
  • 需要重做:> 0.5 像素

计算重投影误差的代码也很简单:

def compute_reprojection_error(objpoints, imgpoints, rvecs, tvecs, mtx, dist):
    total_error = 0
    total_points = 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
        total_points += 1
    
    return total_error / total_points

rms = compute_reprojection_error(objpoints, imgpoints, rvecs, tvecs, mtx, dist)
print(f"重投影误差: {rms:.4f} 像素")

避坑指南:

  • 我曾经遇到过重投影误差0.05像素,但实际矫正效果很差的情况。后来发现是标定板没贴平,产生了微小的弯曲。所以,标定板的平面度比你想的重要得多
  • 如果误差突然变大,先检查是不是有图像没检测到角点,或者标定板在图像边缘被严重遮挡。
  • 不要只看平均值,建议逐张图像查看误差分布。如果某张图的误差明显偏大,果断删掉重新标定。

1.5 知识体系总览

下面这张图是我自己整理的标定技术框架,你可以把它当作本章的「地图」:

相机标定技术知识体系 张正友标定法 标定板设计 多角度图像采集 角点检测 → 亚像素精细化 → 单应性求解 → 内参外参优化 内参矩阵 + 畸变系数 外参(旋转+平移) 重投影误差评估

这张图把整个标定流程串起来了。你从「张正友标定法」出发,左边准备标定板,右边采集图像,中间走完核心算法流程,最后用重投影误差来验收成果。每一步都有讲究,但整体逻辑很清晰。

我的个人习惯:每次标定完,我都会把内参矩阵和畸变系数保存成一个YAML文件,方便后续调用。同时,我会保留几张没参与标定的图像做验证,看看矫正效果是否真的OK。这一步虽然简单,但能帮你发现很多隐藏问题。


公众号:蓝海资料掘金营,微信deep3321