3、OpenCV标定核心函数:cv2.findChessboardCorners()、cv2.calibrateCamera()、cv2.undistort()、cv2.initUndistortRectifyMap()

好,咱们直接进入正题。相机标定这件事,说白了就是搞清楚两件事:相机内部长什么样(内参),以及镜头把光掰弯了多少(畸变系数)。OpenCV给了我们一套非常成熟的工具链,今天我就把这四个核心函数掰开揉碎了讲给你听。

3.1 第一步:找到棋盘格角点 —— cv2.findChessboardCorners()

这是整个标定流程的起点。你拍了一堆棋盘格照片,计算机得先知道「棋盘格上的格子角都在哪」。这个函数就是干这个的。

函数签名:

ret, corners = cv2.findChessboardCorners(image, patternSize, flags=None)

参数说明:

  • image:输入图像,最好是灰度图。我习惯先转成灰度再传进去,省得函数内部还要转一次。
  • patternSize:棋盘格的内角点数,格式是 (cols, rows)。注意!是内角点,不是格子数。比如 9×6 的棋盘格,内角点就是 (8, 5)。
  • flags:可选标志位,一般用默认值就行。

返回值:

  • ret:布尔值,True 表示找到了角点。
  • corners:找到的角点坐标,形状是 (N, 1, 2)。

💡 我的经验:这个函数对光照很敏感。我在项目中遇到过,同样的棋盘格,白天拍能检测到,傍晚就检测不到。后来我加了一步自适应直方图均衡化,成功率提升了不少。

代码示例:

import cv2
import numpy as np

# 读取图像并转灰度
img = cv2.imread('chessboard.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 设置棋盘格内角点数量
pattern_size = (8, 5)  # 9x6 的棋盘格

# 查找角点
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)

if ret:
    print(f"找到 {len(corners)} 个角点")
    # 绘制角点
    cv2.drawChessboardCorners(img, pattern_size, corners, ret)
else:
    print("没找到角点,检查一下棋盘格图片吧")

3.2 亚像素级角点优化 —— cv2.cornerSubPix()

嗯,这里要注意。上面找到的角点精度其实不够,是像素级的。对于标定来说,我们需要亚像素级的精度。这个函数就是用来做精细化处理的。

函数签名:

corners = cv2.cornerSubPix(image, corners, winSize, zeroZone, criteria)

参数说明:

  • image:灰度图。
  • corners:上面找到的角点坐标。
  • winSize:搜索窗口大小,一般设为 (5, 5) 或 (11, 11)。
  • zeroZone:死区大小,一般设为 (-1, -1) 表示没有死区。
  • criteria:迭代终止条件。

⚠️ 我曾经踩过的坑:有一次我忘了做亚像素优化,直接拿像素级角点去标定,结果重投影误差高达 0.8 个像素。优化后降到了 0.15 以下。所以这一步千万别省。

# 亚像素优化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners_sub = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)

3.3 核心标定函数 —— cv2.calibrateCamera()

这个函数是整个标定流程的心脏。你给它一堆棋盘格的世界坐标和图像坐标,它就能算出相机的内参、畸变系数、旋转和平移向量。

函数签名:

ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, flags=None, criteria=None)

参数说明:

参数 说明
objectPoints 世界坐标系中的棋盘格角点坐标。注意,所有图片的棋盘格角点世界坐标是相同的。
imagePoints 图像坐标系中的角点坐标,就是上面亚像素优化后的结果。
imageSize 图像的尺寸,格式是 (width, height)。
cameraMatrix 初始内参矩阵,一般传 None 让函数自己算。
distCoeffs 初始畸变系数,一般传 None。
flags 标定选项,比如是否固定主点、是否固定焦距等。

返回值:

  • ret:重投影误差,越小越好。我一般要求小于 0.3。
  • mtx:相机内参矩阵,3×3。
  • dist:畸变系数,通常是 5 个参数 (k1, k2, p1, p2, k3)。
  • rvecs:每张图片的旋转向量。
  • tvecs:每张图片的平移向量。

💡 我个人习惯:标定前我会先检查一下角点检测的成功率。如果 20 张图里有 5 张以上检测失败,我会重新拍。你想想看,数据质量不行,标定结果能好到哪去?

# 准备世界坐标
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)

# 收集所有图片的角点
objpoints = []  # 世界坐标
imgpoints = []  # 图像坐标

# 假设你有 20 张图片
for fname in ['img1.jpg', 'img2.jpg', ...]:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
    if ret:
        objpoints.append(objp)
        corners_sub = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        imgpoints.append(corners_sub)

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

print(f"重投影误差: {ret:.4f}")
print(f"内参矩阵:\n{mtx}")
print(f"畸变系数:\n{dist}")

3.4 图像去畸变 —— cv2.undistort()

标定完成后,我们拿到了畸变系数。接下来就是把拍出来的照片「掰直」。这个函数是最直接的用法,一张图进去,一张矫正后的图出来。

函数签名:

dst = cv2.undistort(src, cameraMatrix, distCoeffs, dst=None, newCameraMatrix=None)

参数说明:

  • src:输入图像。
  • cameraMatrix:内参矩阵。
  • distCoeffs:畸变系数。
  • newCameraMatrix:新的内参矩阵。如果传 None,就用原来的内参矩阵。

⚠️ 注意:这个函数虽然方便,但如果你要对视频流做实时去畸变,它的效率就不够看了。这时候要用下面那个函数。

# 单张图片去畸变
img_undistorted = cv2.undistort(img, mtx, dist, None, mtx)

cv2.imshow('Original', img)
cv2.imshow('Undistorted', img_undistorted)
cv2.waitKey(0)

3.5 高效去畸变方案 —— cv2.initUndistortRectifyMap()

这个函数才是生产环境下的首选。它先计算好一个映射表,然后每次用 cv2.remap() 来执行。说白了就是「一次计算,多次使用」。我做过一个项目,需要对 4K 视频做实时去畸变,用 undistort() 只能跑到 15 帧,换成这个方案直接干到 30 帧。

函数签名:

map1, map2 = cv2.initUndistortRectifyMap(cameraMatrix, distCoeffs, R, newCameraMatrix, size, m1type)

参数说明:

  • cameraMatrix:内参矩阵。
  • distCoeffs:畸变系数。
  • R:旋转矩阵,一般传 None 或单位矩阵。
  • newCameraMatrix:新的内参矩阵。
  • size:输出图像的尺寸。
  • m1type:映射表的类型,一般用 cv2.CV_32FC1
# 计算映射表
h, w = img.shape[:2]
new_mtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, new_mtx, (w, h), cv2.CV_32FC1)

# 应用映射表
img_undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

# 裁剪掉黑色边框
x, y, w, h = roi
img_undistorted = img_undistorted[y:y+h, x:x+w]

💡 我的经验:getOptimalNewCameraMatrix() 可以自动计算一个合适的新内参矩阵,还能返回一个 ROI 区域用来裁剪掉边缘的黑色区域。这个 ROI 很有用,我每次都会用它来裁剪。

3.6 完整标定流程总结

好了,四个核心函数都讲完了。我帮你理一下完整的流程:

  1. 准备数据:拍 15-20 张不同角度的棋盘格照片。
  2. 检测角点:用 cv2.findChessboardCorners() 找到角点。
  3. 亚像素优化:用 cv2.cornerSubPix() 提高精度。
  4. 执行标定:用 cv2.calibrateCamera() 计算内参和畸变系数。
  5. 评估结果:检查重投影误差,一般要小于 0.3。
  6. 去畸变:用 cv2.initUndistortRectifyMap() + cv2.remap() 进行高效去畸变。

核心要点:

  • 角点检测的质量直接决定标定结果的好坏。
  • 亚像素优化是必须的,别偷懒。
  • 重投影误差是衡量标定质量的关键指标。
  • 生产环境用 initUndistortRectifyMap + remap,别用 undistort

说实话,相机标定这个技术,看起来简单,但真正做起来坑不少。我刚开始做的时候,拍的照片角度不够丰富,结果标出来的内参偏差很大。后来我养成了一个习惯:拍照片时,棋盘格要覆盖画面的各个区域,尤其是边缘和角落。你想想看,如果棋盘格只在画面中央,那边缘的畸变信息怎么算得准呢?

下一章我们会讲如何评估标定结果,以及一些高级的标定技巧。到时候见。