4. 相机标定实战:使用OpenCV进行相机标定、获取内参和畸变系数、去畸变操作

好,咱们进入实战环节。

前面聊了那么多理论,什么针孔模型、畸变模型、张正友标定法……说实话,光看公式容易晕。我刚开始学的时候也是,对着那堆矩阵看了三天,感觉眼睛都快瞎了。后来我发现,最好的学习方式就是动手跑一遍代码

这一章,我们就用 OpenCV 把整个标定流程走一遍。从拍棋盘格照片开始,到拿到内参和畸变系数,最后把一张畸变的图像矫正回来。你跟着我一步步做,做完之后,你会觉得——哦,原来就这么回事。

4.1 准备工作:棋盘格与拍摄

首先,你需要一个棋盘格标定板。淘宝上几块钱一张,或者自己用 A4 纸打印一个也行。

关键点: 棋盘格的尺寸要量准。我指的是每个小方格的物理边长,比如 25mm 还是 30mm。这个值在后面计算时会用到。

拍摄时,我建议你注意几点:

  • 棋盘格要占满画面的不同区域(左上、右下、中间)
  • 棋盘格要有不同的倾斜角度(前倾、后仰、侧转)
  • 拍摄 15-20 张左右就够了
  • 保证光照均匀,不要有过曝或过暗
我的小习惯: 我一般会拍 25 张。拍完之后先快速扫一遍,把那些模糊的、棋盘格没拍全的删掉。剩下 15-20 张质量好的就够了。别贪多,质量比数量重要。

4.2 核心流程:角点检测与标定

整个标定流程,说白了就三步:

  1. 找角点 —— 在每张图片里找到棋盘格的内角点
  2. 亚像素精细化 —— 把角点坐标精度提到亚像素级别
  3. 求解参数 —— 用这些角点去解内参、外参和畸变系数

嗯,这里要注意:OpenCV 找角点时,棋盘格的内角点数量需要你手动指定。比如一个 9×6 的棋盘格,内角点就是 8×5。为什么?因为最外圈的角点容易被遮挡,所以 OpenCV 只找内部的。

我曾经遇到过一个问题:拍出来的棋盘格反光严重,导致角点检测失败。后来我换了哑光纸打印,问题就解决了。所以,标定板最好用哑光的

4.3 代码实战:一步步来

下面这段代码,是我在实际项目中反复用过的。你可以直接复制去跑。

import cv2
import numpy as np
import glob

# 1. 设置棋盘格参数
CHECKERBOARD = (8, 5)  # 内角点数量
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# 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)
# 注意:这里假设方格大小为1个单位,实际使用时需要乘以物理尺寸

# 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, CHECKERBOARD, None)
    
    if ret:
        objpoints.append(objp)
        # 亚像素精细化
        corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
        imgpoints.append(corners2)
        
        # 可视化(可选)
        cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
        cv2.imshow('img', img)
        cv2.waitKey(500)

cv2.destroyAllWindows()

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

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

跑完这段代码,你会得到两个最重要的东西:mtxdist

  • mtx 是内参矩阵,包含 fx, fy, cx, cy
  • dist 是畸变系数,包含 k1, k2, p1, p2, k3
避坑指南: 我曾经把棋盘格的物理尺寸设错了,导致标定出来的焦距值完全不对。后来我养成了一个习惯:在 objp 里直接乘以物理尺寸,比如 objp[:, :2] = np.mgrid[...].T.reshape(-1, 2) * 25.0(假设方格边长 25mm)。这样内参里的 fx, fy 就直接是毫米单位了,方便后续计算。

4.4 去畸变操作:让图像变回"正常"

拿到畸变系数之后,下一步就是去畸变。说白了,就是把拍出来变形的图像,矫正成我们想要的"直线是直线"的样子。

OpenCV 提供了两种方式:

  • cv2.undistort() —— 一步到位,简单粗暴
  • cv2.initUndistortRectifyMap() + cv2.remap() —— 分两步走,适合视频流处理

我个人更推荐第二种方式。为什么?因为 remap 可以预先计算好映射表,后面每一帧直接查表就行,速度飞快。做实时视频处理时,这个优势很明显。

# 方式一:直接去畸变
img_undistorted = cv2.undistort(img, mtx, dist, None, mtx)

# 方式二:使用 remap(推荐)
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)

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

你想想看,去畸变之后,图像边缘原本弯曲的直线是不是变直了?这就是标定的意义。

注意: 去畸变后,图像边缘可能会出现黑边。这是因为矫正后的像素点超出了原图范围。使用 getOptimalNewCameraMatrix 可以自动计算一个合适的裁剪区域,把黑边去掉。但代价是损失了一部分视野。如果你需要保留全部视野,可以把 alpha 参数设为 0。

4.5 评估标定结果:重投影误差

标定完了,怎么知道标得好不好?

重投影误差。简单说,就是把世界坐标系中的点,通过标定出来的参数投影到图像上,看看和实际检测到的角点差了多少像素。

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))

一般来说,重投影误差小于 0.5 个像素就算不错了。如果大于 1 个像素,我建议你检查一下:

  • 是不是有模糊的图片混进来了?
  • 棋盘格是不是贴得不平?
  • 拍摄角度是不是太单一了?

我记得有一次,标定出来的误差高达 2.3 个像素。排查了半天,发现是棋盘格打印在褶皱的纸上,导致角点位置有偏差。换了一张平整的标定板,误差直接降到了 0.3 像素。

4.6 本章知识体系

下面这张图,帮你把整个标定流程串起来:

相机标定实战流程 ① 准备棋盘格 打印/购买,测量方格尺寸 ② 拍摄多张图片 15-25张,不同角度/位置 ③ 角点检测 findChessboardCorners ④ 亚像素精细化 cornerSubPix 提高精度 ⑤ 执行标定 calibrateCamera 输出:内参矩阵 + 畸变系数 + 重投影误差 用于去畸变、立体校正、三维重建

这张图把整个流程串起来了。你跟着走一遍,就能拿到相机标定的核心参数。

4.7 小结

这一章我们做了三件事:

  • 用 OpenCV 的 findChessboardCorners 找到棋盘格角点
  • calibrateCamera 解出内参和畸变系数
  • remap 对图像做去畸变

说实话,标定这件事,第一次做可能会觉得有点繁琐。但一旦你跑通了流程,后面再做就轻车熟路了。而且,标定精度直接决定了后续立体匹配的效果。所以,花点时间把这一步做好,绝对值得。

嗯,下一章我们会用标定好的参数,去做立体校正。到时候你会发现,标定这一步做得越扎实,后面的路就越顺。


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