第4章:相机标定实战——使用OpenCV进行单目标定、重投影误差分析、标定结果验证方法

相机标定这事儿,说难不难,说简单也不简单。我刚开始做结构光系统的时候,觉得标定不就是拍几张棋盘格照片跑个函数嘛。结果第一次做出来的三维重建,点云全是扭曲的,像哈哈镜里照出来的一样。后来才明白——标定是结构光系统的地基,地基歪了,上面盖的楼再漂亮也得塌。

这一章,咱们就手把手把OpenCV单目标定走一遍。我会把我在项目里踩过的坑、试出来的经验,都揉进去。

4.1 标定板的选择与准备

先说说标定板。很多人上来就问:「用多大的棋盘格?」「格子数量多少合适?」

我的建议是:

  • 棋盘格尺寸:7×10 或 8×11 的内角点,别太小也别太大。7×10 是我用得最多的,兼顾了覆盖范围和计算效率。
  • 方格边长:20mm~30mm 比较常见。我习惯用 25mm,不大不小,测量误差容易控制。
  • 打印质量:一定要用激光打印,别用喷墨。喷墨的格子边缘会洇墨,角点检测精度直接掉一截。
我的小技巧:把标定板贴在平整的亚克力板或玻璃板上,别贴纸板上。纸板容易弯,弯了标定出来的内参就偏了。我曾经吃过这个亏,标定结果重投影误差0.08像素,看着挺漂亮,实际重建误差大了三倍。

4.2 图像采集的讲究

图像采集这一步,很多人觉得「拍够20张就行」。嗯,数量是够了,但质量呢?

我一般会拍30~40张,然后从中挑出20~25张质量好的。怎么挑?看这几点:

  1. 姿态要丰富:标定板要在画面各个位置出现——左上、右下、中间、靠近边缘。还要有不同倾斜角度,前倾后仰左转右转,模拟实际使用时的光线入射角。
  2. 避免运动模糊:手持标定板时,快门速度要够快。我一般用1/100秒以上,或者干脆用三脚架固定相机,只移动标定板。
  3. 光照均匀:别让标定板上一半亮一半暗。角点检测对光照敏感,不均匀的光照会让检测到的角点位置偏移。
  4. 覆盖整个视场:标定板要出现在画面的边缘区域。很多人的标定板只放在画面中央,这样标定出来的畸变参数在边缘区域是「猜」出来的,不准。
注意:每移动一次相机或变焦,都必须重新标定。结构光系统里,相机和投影仪的相对位置一旦改变,标定就得重做。别偷懒,我见过有人用上次项目的标定参数,结果重建出来的物体像被揉过一样。

4.3 OpenCV标定核心代码

好,到了动手环节。下面是我在实际项目中打磨过的标定代码,去掉了花里胡哨的东西,只留干货。

import cv2
import numpy as np
import glob

# 棋盘格参数
CHECKERBOARD = (7, 10)  # 内角点数量
SQUARE_SIZE = 25.0      # 方格边长,单位mm

# 准备世界坐标系中的点
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

# 存储所有图像的点
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, CHECKERBOARD, 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, CHECKERBOARD, corners2, ret)
        cv2.imshow('Corners', img)
        cv2.waitKey(100)

cv2.destroyAllWindows()

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

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

这段代码里,有几点我想特别说一下:

  • 亚像素精确化cornerSubPix 这一步不能省。直接检测到的角点是像素级的,亚像素细化后精度能到0.1像素级别。标定结果差0.1像素,三维重建可能就差几毫米。
  • 世界坐标系单位:我用的是毫米。如果你用其他单位,记得保持一致。有人用米,有人用厘米,混用了标定出来的平移向量就全乱了。
  • 标定函数返回值ret 就是重投影误差的均方根值。这个值后面会详细分析。

4.4 重投影误差分析

重投影误差,说白了就是:我们把世界坐标系里的棋盘格角点,用标定出来的内参和外参投影到图像上,看看投影点和实际检测到的角点差了多少像素。

这个值多少算好?

重投影误差(像素) 评价 说明
< 0.1 优秀 标定精度很高,适合高精度结构光系统
0.1 ~ 0.3 良好 大多数场景够用,结构光重建精度可接受
0.3 ~ 0.5 一般 需要检查图像质量或标定板是否平整
> 0.5 较差 建议重新采集图像,检查标定流程

我个人的经验是:重投影误差在0.15像素以下,结构光重建的精度就比较靠谱了。如果超过0.3像素,我会回去检查图像,看看是不是有模糊的、光照不好的、或者标定板没拍全的。

怎么计算每张图像的重投影误差?可以这样:

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("平均重投影误差: ", total_error / len(objpoints))

这里有个坑:cv2.projectPoints 用的畸变系数要和标定时一致。如果你标定时用了 calibrateCamera 的默认参数(5个畸变系数),投影时也要用同样的5个系数。否则算出来的误差是错的。

4.5 标定结果验证方法

重投影误差低,不代表标定结果就一定好。为什么?因为重投影误差是在标定用的同一组图像上算的,存在过拟合的可能。说白了,就是标定参数「记住」了这组图像的噪声,而不是真正的相机模型。

所以,我一般会做以下验证:

4.5.1 独立验证集法

拍标定图像时,多拍10张不参与标定。标定完成后,用这10张独立图像计算重投影误差。如果独立验证集的误差和标定集的误差差不多,说明标定结果泛化能力好。

如果独立验证集的误差明显更大——比如标定集0.12像素,验证集0.35像素——那说明标定过拟合了。这时候我会减少标定图像数量,或者检查是否有低质量的图像混进来了。

4.5.2 直线度验证

这个方法很直观:拍一张有直线的场景(比如墙角的线条、门框),然后用标定参数去畸变。如果去畸变后的直线变直了,说明畸变参数是准的。

img = cv2.imread('test_line.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)
cv2.imwrite('undistorted.jpg', dst)

我习惯在去畸变后的图像上画几条水平线和垂直线,看看边缘有没有弯曲。如果边缘还是弯的,那畸变参数肯定有问题。

4.5.3 三维重建验证

这是最实在的方法。用标定好的相机,拍一个已知尺寸的物体(比如一个标准球体或者一个精密加工的平板),然后做三维重建。看看重建出来的尺寸和实际尺寸差多少。

我在项目中常用一个100mm×100mm的陶瓷平板,表面有精密加工的标记点。重建后测量标记点之间的距离,误差在0.5mm以内就算合格。如果超过1mm,我会回去重新标定。

核心要点:标定不是跑完代码就完事了。验证环节才是真正检验标定质量的试金石。我见过太多人标定完看一眼重投影误差0.08就欢呼,结果三维重建一塌糊涂。记住:重投影误差是必要条件,不是充分条件。

4.6 常见问题与避坑指南

最后,分享几个我在项目中遇到的实际问题:

  • 角点检测失败:棋盘格反光太强,或者图像太暗。解决办法是调整光照,或者用哑光纸打印标定板。
  • 标定结果不稳定:每次标定出来的内参都不一样。这通常是因为图像数量不够,或者图像姿态太相似。我一般保证至少20张不同姿态的图像。
  • 畸变参数异常大:比如径向畸变k1超过0.5。这可能是标定板不平整,或者图像中有运动模糊。检查一下标定板是否贴在平整表面上。
  • 重投影误差低但重建效果差:这往往是标定板和实际场景的深度范围不匹配。标定板只在200mm处拍,但实际物体在500mm处,那畸变模型在500mm处就不准了。解决办法是在不同距离都拍一些标定图像。

嗯,这一章的内容差不多就这些了。标定这件事,多做几次就有感觉了。刚开始可能会觉得繁琐,但当你看到重建出来的点云精准地还原了物体形状时,那种成就感是值得的。


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