4. 相机标定实战:OpenCV标定流程、畸变校正、重投影误差评估

相机标定,说白了就是给相机做「视力矫正」。

你想想看,仿生眼要重建三维世界,第一步就得知道相机本身的「脾气」——镜头畸变有多大?焦距是多少?光心偏不偏?这些参数不搞清楚,后面的深度估计全是空中楼阁。

我个人习惯把标定比作「给相机量三围」。量准了,后续的立体匹配、三维重建才有底气。今天我们就用OpenCV把这套流程走一遍。

4.1 标定的核心:内参、外参、畸变系数

先理清概念。相机标定最终要得到三样东西:

  • 内参矩阵:焦距(fx, fy)、光心(cx, cy)。这是相机本身的固有属性。
  • 畸变系数:径向畸变(k1, k2, k3)和切向畸变(p1, p2)。镜头不是完美的,边缘会变形。
  • 外参:每张标定图片对应的旋转矩阵和平移向量。说白了就是相机相对于标定板的位置和姿态。

我在项目中遇到过最坑的事:有人把内参和外参搞混了。内参是相机自己的,换镜头就得重新标;外参是每张图都不同的,标定完一般就扔了。

核心公式(简化版)

世界坐标 → 相机坐标(外参) → 图像坐标(内参) → 加上畸变 → 最终像素

标定就是反向求解这个过程。

4.2 OpenCV标定流程:从拍摄到参数输出

OpenCV的标定流程其实很固定。我做了这么多年,基本就是这五步:

  1. 准备标定板:打印一张棋盘格,贴在硬板上。格子数建议9×6或10×7。
  2. 拍摄多张图片:从不同角度拍15-20张。注意覆盖视野的各个角落。
  3. 提取角点:用cv2.findChessboardCorners()找到棋盘格的内角点。
  4. 执行标定:调用cv2.calibrateCamera(),一步到位。
  5. 评估结果:看重投影误差,判断标定质量。

嗯,这里要注意:拍摄时标定板要占画面1/3以上,太远太小角点检测会失败。我曾经偷懒只拍了8张,结果标定出来的畸变参数完全不对,后来老老实实补到20张才搞定。

4.2.1 代码实战:完整标定流程

下面是我常用的标定代码框架。你直接拿去用,改一下棋盘格尺寸和图片路径就行。

import cv2
import numpy as np
import glob

# 1. 准备棋盘格参数
chessboard_size = (9, 6)  # 内角点数
square_size = 25.0        # 格子边长,单位mm

# 2. 生成世界坐标系中的点
objp = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2)
objp *= square_size

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

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, chessboard_size, 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, chessboard_size, corners2, ret)
        cv2.imshow('Corners', img)
        cv2.waitKey(100)

cv2.destroyAllWindows()

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

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

个人经验cv2.calibrateCamera()返回的ret就是重投影误差。我一般要求这个值小于0.5像素才算合格。超过1.0的,建议重新拍一组图片。

4.3 畸变校正:让图像变「直」

标定完拿到畸变系数,下一步就是校正图像。OpenCV提供了两个函数:

  • cv2.undistort():一步到位,简单粗暴。
  • cv2.initUndistortRectifyMap() + cv2.remap():先算映射表,再重映射。适合视频流,效率更高。

我个人习惯用第二种。为什么?因为视频流里每帧都调用undistort()太慢了,而映射表只需要算一次。

# 方法一:直接校正
img_undistorted = cv2.undistort(img, mtx, dist, None, mtx)

# 方法二:映射表方式(推荐)
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w, h), 5)
img_undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

避坑指南:我曾经直接用了undistort(),结果发现图像边缘被裁掉了。后来改用getOptimalNewCameraMatrix(),设置alpha=1保留所有像素,才解决了问题。alpha值控制保留范围,0表示全裁,1表示全留。

4.4 重投影误差评估:标定质量的「照妖镜」

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

它的原理很简单:用标定得到的参数,把世界坐标点重新投影到图像上,算算和实际检测到的角点差了多少像素。

OpenCV的cv2.calibrateCamera()已经帮我们算好了,就是返回值ret。但如果你想自己算,可以这样:

def compute_reprojection_error(objpoints, imgpoints, rvecs, tvecs, mtx, dist):
    total_error = 0
    total_points = 0
    
    for i in range(len(objpoints)):
        imgpoints_proj, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
        error = cv2.norm(imgpoints[i], imgpoints_proj, cv2.NORM_L2) / len(imgpoints_proj)
        total_error += error
        total_points += len(imgpoints_proj)
    
    return total_error / len(objpoints)

mean_error = compute_reprojection_error(objpoints, imgpoints, rvecs, tvecs, mtx, dist)
print("平均重投影误差: {:.3f} 像素".format(mean_error))

经验阈值

  • < 0.3像素:优秀,可以用于高精度三维重建
  • 0.3 - 0.8像素:良好,大多数场景够用
  • 0.8 - 1.5像素:及格,建议检查标定图片质量
  • > 1.5像素:不合格,重新拍吧

为什么会误差大?我遇到过几种情况:标定板不平整、图片模糊、棋盘格反光太强。有一次在户外标定,阳光直射导致棋盘格反光,角点检测全偏了,重投影误差直接飙到2.3像素。后来换了哑光纸打印才解决。

4.5 本章知识体系

下面这张图总结了相机标定的完整流程和关键环节:

相机标定知识体系 ① 准备标定板 棋盘格 9×6 或 10×7 ② 多角度拍摄 15-20张,覆盖全视野 ③ 提取角点 findChessboardCorners ④ 执行标定 calibrateCamera() 输出:内参 + 畸变 + 外参 ⑤ 重投影误差评估 误差 < 0.5像素 合格 不合格则返回②重新拍摄 不合格,重新拍摄 标定输出结果 内参矩阵 fx, fy, cx, cy 畸变系数 k1, k2, p1, p2, k3 畸变校正 undistort / remap

4.6 实战中的几个坑

最后分享几个我踩过的坑,希望能帮你省点时间:

  • 标定板要平:贴在玻璃上或者用亚克力板夹住。软纸板一弯,角点位置全偏。
  • 图片要清晰:运动模糊、对焦不准都会导致角点检测偏差。我一般用三脚架拍。
  • 角度要多样:不要只拍正面,倾斜、旋转、上下左右都要有。覆盖视野边缘尤其重要。
  • 数量要够:15张是底线,20张以上更稳。别学我当年偷懒只拍8张。

小技巧:标定完成后,保存内参和畸变系数到.npz.yaml文件。下次直接用,不用重新标。我一般这样存:

np.savez('calib_params.npz', mtx=mtx, dist=dist, newcameramtx=newcameramtx)

好了,相机标定这块就聊到这儿。记住一句话:标定质量决定三维重建的天花板。花半小时把标定做好,后面能省三天调参的时间。


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