在计算机视觉领域,将三维世界的真实场景准确地映射到二维图像上是我们面临的最基本挑战之一。你是否曾注意到,当你用手机拍摄一张方形纸片的照片时,照片边缘的线条往往看起来是弯曲的?或者在使用机器人视觉时,实际距离与测量距离总存在微妙的偏差?这些都是因为相机镜头存在畸变,且我们需要通过数学手段来修正它们。
在本文中,我们将深入探讨如何使用 Python 中的 OpenCV 库来解决这一问题。我们将通过 cv2.calibrateCamera() 函数,一步步揭开相机标定的神秘面纱。你不仅会理解背后的几何原理,还会掌握如何编写代码来自动校正这些物理扭曲,从而为你的计算机视觉项目打下坚实的基础。
相机标定的必要性:为什么我们不能简单地“所见即所得”?
首先,让我们思考一下拍摄的本质。相机通过镜头将现实世界中的 3D 物体转换为 2D 图像。虽然这个过程在我们的眼睛看来很自然,但在数学和物理上,它引入了两个主要问题:畸变和投影映射。
#### 1. 理解畸变
由于制造工艺的限制,相机镜头(尤其是广角镜头)无法完美地投射光线。这会导致图像出现扭曲,主要分为两类:
- 径向畸变:这是最常见的问题。它导致直线看起来变得弯曲。通常表现为“桶形畸变”(直线向外凸出)或“枕形畸变”(直线向内收缩)。这主要是因为镜头形状使得光线在边缘的折射率与中心不同。
- 切向畸变:这种畸变通常发生在成像平面与镜头不完全平行时。它会导致图像看起来稍微被拉长或倾斜,使得物体看起来比实际位置更近或更远。
#### 2. 坐标系转换
现实世界中的物体存在于 世界坐标系 (3D) 中。当相机捕捉它们时,这些点在 相机坐标系 (3D) 中被观察。最后,为了在屏幕上显示,它们被投影到 图像坐标系 (2D) 上。我们的目标就是找到这组完美的数学变换参数,将 3D 点无损地映射为 2D 像素点。
!Object to Image Coordinate System
> 转换示意图:从物体到图像坐标系的复杂变换过程。
因此,相机标定 的核心就在于确定相机的内部参数(焦距、光心)和外部参数(旋转、平移),以及畸变系数。OpenCV 提供的 calibrateCamera() 函数正是为了解决这个数学上的“最优估计”问题。
标定的关键参数:内参与外参
在使用函数之前,我们需要先理解它要计算什么。标定过程本质上是求解两个矩阵和畸变向量的过程。
#### 内部参数
这是相机固有的属性,不随场景移动而改变。主要包括:
- 焦距 ($fx, fy$):决定镜头的放大倍数。
- 光心 ($cx, cy$):光轴与成像平面的交点,通常接近图像中心。
在数学上,内参矩阵 $K$ 表示为:
$$
K = \begin{bmatrix}
fx & 0 & cx\\
0 & fy & cy\\
0 & 0 & 1
\end{bmatrix}
$$
#### 外部参数
这描述了相机在世界空间中的位置和姿态。每张照片可能都不同:
- 旋转矩阵:描述相机方向的旋转。
- 平移向量:描述相机相对于世界原点的位移。
通常合并表示为 $[R|T]$。
准备工作:选择标定物
为了计算上述参数,我们需要一个参照物。OpenCV 支持三种特定的图案,因为这些图案的几何特征很容易被算法自动检测:
- 经典的黑白棋盘格:最常用,容易打印。
- 对称圆形图案:圆心的排列非常规整。
- 非对称圆形图案:在某些工业场景下效果更好,因为它消除了二义性。
最佳实践提示:如果你是初学者,我们强烈建议使用打印在平整平面上的黑白棋盘格。标定板必须保持完全平整,任何纸张的弯曲都会导致计算出的内参不准确。
深入剖析 cv2.calibrateCamera() 语法
让我们来看看这个核心函数的语法和它背后的逻辑。
#### 语法结构
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, flags=None, criteria=None)
#### 参数详解
- objectPoints:这是一个 3D 点向量的列表。关键点:在 OpenCV 的坐标系中,我们假设标定板位于 Z=0 的平面上,因此我们只需要构建 (x, y, 0) 的坐标。例如,对于棋盘格,我们可以将一个方格定义为 1 个单位,那么点就是 (0,0,0), (1,0,0), (2,0,0)…
- imagePoints:这是对应的 2D 图像点向量。通过
cv2.findChessboardCorners()等函数在图像中找到的角点坐标。 - imageSize:图像的尺寸(宽, 高),用于初始化相机矩阵。
- cameraMatrix:输出的内参矩阵。如果不输入初始值,函数会自动计算。
- distCoeffs:输出的畸变系数向量 $(k1, k2, p1, p2, k_3)$。
- rvecs, tvecs:输出的旋转向量和平移向量。
- flags:不同的标定方法标志(例如
cv2.CALIB_ZERO_TANGENT_DIST设定切向畸变为 0)。 - criteria:迭代优化算法的终止条件(例如迭代次数或精度阈值)。
#### 返回值说明
- ret:RMS(均方根)重投影误差。这是一个非常重要的指标,它告诉我们计算出的 3D 点重新投影到图像平面上时,与实际检测到的 2D 点平均距离是多少。数值越小,标定越精确。
实战代码示例 1:经典的棋盘格标定
这是最标准的标定流程。我们将通过拍摄多张不同角度的棋盘格照片来计算参数。
import numpy as np
import cv2
import glob
# 终止标准:迭代 30 次或精度达到 0.001
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 准备对象点,类似 (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((6*7, 3), np.float32)
objp[:, :2] = np.mgrid[0:7, 0:6].T.reshape(-1, 2)
# 用于存储来自所有图像的对象点和图像点的数组
objpoints = [] # 真实世界空间中的 3d 点
imgpoints = [] # 图像空间中的 2d 点
# 获取标定图像目录下的所有图片
images = glob.glob(‘calibration_images/*.jpg‘)
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 寻找棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, (7, 6), None)
# 如果找到,添加对象点,图像点(在细化之后)
if ret == True:
objpoints.append(objp)
# 像素角点的亚像素精度优化
corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
imgpoints.append(corners2)
# 绘制并显示角点(用于调试)
cv2.drawChessboardCorners(img, (7, 6), corners2, ret)
cv2.imshow(‘img‘, img)
cv2.waitKey(500)
cv2.destroyAllWindows()
# --- 执行标定 ---
# 图像大小必须一致,这里以读取的第一张图为例
img_shape = cv2.imread(images[0]).shape[:2][::-1] # (width, height)
# 调用 calibrateCamera 函数
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, img_shape, None, None)
print("相机内参矩阵:
", mtx)
print("畸变系数:
", dist)
print("重投影误差:", ret)
代码解析:
-
objp:我们假设标定板是平的(Z=0)。我们构建了一个网格点阵来代表物理世界。 -
cornerSubPix:这是一个非常实用的技巧。默认检测到的角点可能是整数像素坐标,但实际角点可能位于像素之间(例如 100.4, 200.6)。这个函数会通过几何计算将角点位置精确化到亚像素级别,极大地提高了标定精度。 - 循环采集:我们需要至少 10-20 张不同角度的照片(有些旋转,有些倾斜),否则无法解出所有参数(特别是畸变系数)。
实战代码示例 2:使用非对称圆形网格(工业场景)
在工业自动化中,棋盘格有时难以在光照复杂的情况下检测。OpenCV 也支持使用圆点阵列。这里我们使用非对称圆形网格。
import cv2
import numpy as np
# 设置非对称圆点的尺寸 (行, 列)
# 注意:这里的尺寸取决于你的标定板物理布局
pattern_size = (4, 11)
# 初始化终止标准
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
obj_points = []
img_points = []
# 对于非对称圆点,生成 3D 坐标稍微复杂一些
# 假设圆心间距为 1.0 单位
obj_p = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32)
for i in range(pattern_size[0]):
for j in range(pattern_size[1]):
# 这种排列取决于具体的非对称模式定义,通常是交错排列
# 这里使用简单的通用生成逻辑
obj_p[i*pattern_size[1] + j] = [j * 1.0, i * 1.0 + (j % 2) * 0.5, 0]
# 读取图像
cap = cv2.VideoCapture(0) # 或者读取图片列表
# 假设我们要从视频流中提取几帧进行标定
count = 0
while count 0:
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, gray.shape[::-1], None, None)
print(f"标定完成,重投影误差: {ret}")
print("畸变系数:
", dist.ravel())
标定之后:利用结果去除畸变
一旦我们得到了 INLINECODE72e236fb (相机矩阵) 和 INLINECODE6f887892 (畸变系数),我们可以使用 cv2.undistort() 来修正新拍摄的图像。这是标定最实际的应用。
import cv2
import numpy as np
# 假设这是之前标定得到的参数
camera_matrix = np.array([[532.0, 0.0, 320.0], [0.0, 532.0, 240.0], [0.0, 0.0, 1.0]])
distortion_coeffs = np.array([-0.2, 0.05, 0.0, 0.0, 0.0])
# 读取一张带有畸变的图像
img = cv2.imread(‘distorted_image.jpg‘)
h, w = img.shape[:2]
# 方法 1:直接调用 undistort(最简单)
undistorted_img = cv2.undistort(img, camera_matrix, distortion_coeffs)
# 方法 2:使用 remapping(更适合视频流,只需计算一次映射表)
# 计算最优新相机矩阵(通过 alpha 参数控制是否裁剪黑边)
new_camera_mtx, roi = cv2.getOptimalNewCameraMatrix(camera_matrix, distortion_coeffs, (w, h), 1, (w, h))
mapx, mapy = cv2.initUndistortRectifyMap(camera_matrix, distortion_coeffs, None, new_camera_mtx, (w, h), 5)
undistorted_img_optimized = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
# 显示结果
cv2.imshow(‘Original‘, img)
cv2.imshow(‘Undistorted‘, undistorted_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
这里发生了什么?
cv2.undistort() 实际上内部执行了以下步骤:
- 利用畸变系数计算像素的位移。
- 利用内参矩阵重新映射像素,使得弯曲的直线变直。
- Alpha 参数:在使用
getOptimalNewCameraMatrix时,alpha=1 意味着保留所有原始像素(可能会有黑边),alpha=0 意味着裁剪掉所有无效像素(没有黑边但会丢失部分视野)。
常见问题与解决方案
Q: 我的标定结果重投影误差 很大(比如大于 1.0),这正常吗?
A: 通常 RMS 误差应小于 0.5 像素才算良好。如果误差过大,通常有几个原因:
- 光线问题:标定板上的反光导致角点检测错误。请使用哑光纸打印。
- 运动模糊:拍摄时手抖导致照片模糊,角点位置不准。使用三脚架或更快的快门。
- 畸变参数设置错误:对于普通镜头,通常只需要 $k1, k2, p1, p2$ 四个参数。可以使用
cv2.CALIB_FIX_K3等标志固定高阶参数。
Q: 我需要多少张图片才能成功标定?
A: 理论上最少需要几张图片来解方程,但为了鲁棒性,我们建议至少 15 到 20 张。这些图片必须覆盖镜头的各个角落(不仅仅是中心),否则径向畸变的计算会不准确。
总结与下一步
在这篇文章中,我们不仅学习了如何调用 calibrateCamera(),更重要的是理解了为什么要进行相机标定,以及如何通过代码流程确保数据的准确性。从 3D 世界坐标的构建,到亚像素级的角点精炼,再到最终的畸变去除,每一步都至关重要。
你可以尝试的后续步骤:
- 尝试手眼标定:如果你正在做机械臂视觉,可以进一步探索如何将相机坐标系与机械臂坐标系对齐。
- 立体视觉:使用两个相机进行标定,计算深度图。
- AR 应用:利用标定好的相机参数,在现实图像上叠加 3D 虚拟物体,确保透视关系正确。
掌握相机标定是通往高级计算机视觉应用的一把钥匙。希望这篇指南能帮助你更好地理解和实践 OpenCV 中的这一强大功能!