如何对单目相机进行内参标定

通过 Python 编程,使用 OpenCV 和 NumPy 在 Ubuntu 上实现单目相机的标定

由 Anawaert 于 2025-02-04 发布   

如何对单目相机进行内参标定

  单目相机标定是一种通过捕获不同视角下的标定板图像,并利用这些图像计算相机内部参数(如焦距、光学中心等)和畸变系数的过程。这些参数对于校正相机视图中的畸变非常重要,也是进行精确图像测量和 3D 重建的基础。本文将使用 Python 作为编程语言,使用 OpenCV 和和 NumPy 在 Ubuntu 上编写代码以实现单目相机的标定。

  由于本文的重点并非标定算法本身,因此本文将不会着重叙述标定原理及标定算法的底层工作原理,而主要着重于代码的编写

标定原理简述

  标定的原理主要基于相机模型和投影几何原理。相机成像过程可以用针孔相机模型来描述,其中世界坐标系中的点通过相机的内外参数映射到图像平面上。通过对已知场景中的标定板(如棋盘格)进行拍摄,获取多个不同视角下的图像。通过对这些图像进行分析,可以求解出相机的内参。

Camera Model

  在完成标定后,将获得一系列标定内参。其中最重要的是相机矩阵(Camera Matrix)畸变向量(Distortion Coefficients)。观察相机矩阵 $ K $ 畸变向量 $ D $:

$$ K = \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix} $$

$$ D = \begin{bmatrix} k_1, k_2, p_1, p_2, k_3 \end{bmatrix} $$

  其中,相机矩阵中的 $f_x$ 和 $f_y$ 为相机镜头在 $x$ 和 $y$ 方向上的焦距,一般以像素(pixel)为单位;$c_x$ 和 $c_y$是相机图像的主点,即相机坐标系与图像坐标系的原点

Camera Matrix

  而畸变向量中,$ k_1, k_2, k_3 $ 为径向畸变系数,这些参数用于描述镜头的径向畸变,在图像中表现为桶形或枕形畸变;$ p_1, p_2 $ 为切向畸变系数,这些参数用于描述由于镜头或相机传感器等缘故导致的图像偏移。

Radial Distortion

Tangential Distortion

  当我们获得了这些参数以后,就可以将它们用于一系列函数与程序中,并利用这些参数来实现一系列功能了,如图像畸变校正和外参标定等。

标定前准备

  进行单目标定前,需要准备以下内容:

拍摄标定图像

  使用相机从不同角度拍摄(棋盘格)标定板约 20-30 张图像,并保存在本地磁盘上。请记住您拍摄的这一系列图像的尺寸,如 $ 1280 \times 720 $

Chess Board

安装 OpenCV 和 NumPy

  在终端中,使用以下命令来安装 OpenCV 和 NumPy。以笔者为例,安装 OpenCV-Python 4.9.0.80 和 NumPy 1.23.5:

# bash
pip install numpy==1.23.5
pip install opencv-python==4.9.0.80

使用合适的 IDE(可选)

  在本文中,使用 JetBrains PyCharm Community Edition 作为 IDE 进行标定演示。您也可以选择 Visual Studio Code 或 Visual Studio 等 IDE 在不同平台上进行代码编写,程序的表现应不受平台影响。

编写代码

导入必要的库或模块

  首先,导入以下模块以使用 OpenCV、Numpy,同时还需要导入 Python 标准库中的 glob() 函数。

# Python
import numpy as np
import cv2
import glob

初始化必要变量

  初始化 OpenCV 标定函数所需的一些变量,如棋盘格角点的世界坐标、生成的终止标准、世界坐标系中的三维坐标点以及图像坐标系中的二维点。在这一步中,我们使用 NumPy 数组以及 NumPy 中的一些 API 进行初始化和运算。

# 假设棋盘格的角点数为 11 × 8

# 生成棋盘格角点的世界坐标
# 11 与 8 可根据实际的标定板的棋盘格内角点数进行替换,比如说 9 和 6
objp = np.zeros((11 * 8, 3), np.float32)  # 注意不要漏掉乘号“*”或写成逗号“,”,这会导致数组维度错误
objp[:, :2] = np.mgrid[0:11, 0:8].T.reshape(-1, 2)

# 生成终止标准
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# 存储棋盘格角点的世界坐标和图像坐标
obj_points = []  # 世界坐标系中的三维点
img_points = []  # 图像坐标系中的二维点

读取标定图像并将添加坐标

  通过 Python 标准库中的 glob() 函数将所有先前拍摄的标定图像转化为 Python 字符串列表,遍历并使用 OpenCV 读取,最后将符合要求的标定图像内角点对应的图像点与世界坐标点添加至先前初始化的列表中:

# 假设拍摄的标定图像位于 /home/images/ 目录下,且全为 PNG 格式
images = glob.glob('/home/anawaert/images/*.png')

# 遍历图像序列
for image in images:
    # 使用 OpenCV 读取图像(必须记得要获取图像的黑白副本)
    img = cv2.imread(image)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # 查找棋盘格角点,其中 (11, 8) 为棋盘格的内角点数
    ret, corners = cv2.findChessboardCorners(gray, (11, 8), None)

    # 若找到角点,则添加到世界坐标和图像坐标中
    if ret:
        obj_points.append(objp)
        corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        img_points.append(corners2)

调用 cv2.calibrateCamera() 函数进行标定

  在获得了上述的一系列必要参数后,即可调用 cv2.calibrateCamera() 函数对相机进行标定,获得相机的内参:

# 假设拍摄的标定图像大小为 1280 × 720,则函数第三个参数填写 (1280, 720)
ret_val, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, (1280, 720), None, None)

  从命名上就能看出来,camera_matrixdist_coeffs 即为相机矩阵和畸变向量。但多出来的 rvecstvecs 又是什么呢?

  rvecs 为旋转向量,描述了相机坐标系相对于世界坐标系的旋转。这个向量提供了相机是如何围绕某个轴旋转的信息,通过旋转向量,可以将物体在世界坐标系中的位置转换为相机坐标系中的位置。而 tvecs 为平移向量,描述的是相机坐标系相对于世界坐标系的平移。它表示相机原点(相机中心)在世界坐标系中的位置,即相机相对于世界坐标系的偏移。将这两个向量结合使用,就能够完整地描述相机的外部参数,也就是相机在三维空间中的位置和朝向。看到这里,相信大家就明白了, rvecstvecs 实际上是相机外参,描述了相机自身与世界坐标系的变换关系。但本文主题为单目相机的内参标定,且畸变校正过程不需要外参,因此这两个向量暂时不需要理会。

细化标定参数

  截至目前,已获得了一个可用的相机矩阵和畸变向量。但是,OpenCV 提供了一个可以获取图像经过校正后可用区域的新内参的函数,该函数将有助于我们进行图像的校正:

# 假设拍摄的标定图像大小为 1280 × 720,则函数第三、五个参数填写 (1280, 720)
new_camera_matrix, roi = cv2.getOptimalNewCameraMatrix(camera_matrix, dist_coeffs, (1280, 720), 1, (1280, 720))

  此处的 roi 为畸变校正可用的区域,由于图像的畸变校正存在对图像的“拉伸”或“扭曲”,因此校正后图像与原图像尺寸不一定一致,而 roi 则给出了一个可用区域,可粗略地理解为这片区域将不包含畸变校正后的“黑边”与无效校正区域;而 new_camera_matrix 即为这片可用区域对应的新相机矩阵,在图像校正时将指引校正函数进行校正。

打印标定参数

  可以直接使用 Pythonprint()函数进行标定参数的打印:

print(f'Camera matrix is: {camera_matrix} \n')
print(f'Distortion coefficients is: {dist_coeffs} \n')
print(f'New camera matrix is: {new_camera_matrix} \n')
print(f'Available region of interest is: {roi} \n')

运行效果

  在 JetBrains PyCharm Community Edition 创建新项目后,在新创建的 Python 模块中输入这些代码并运行,即可看到控制台中输出了这些相机标定内参:

Effect

完整代码示例

import numpy as np
import cv2
import glob

# 生成棋盘格角点的世界坐标
objp = np.zeros((11 * 8, 3), np.float32)
objp[:, :2] = np.mgrid[0:11:1, 0:8:1].T.reshape(-1, 2)

# 生成终止标准
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# 存储棋盘格角点的世界坐标和图像坐标
obj_points = []  # 世界坐标系中的三维点
img_points = []  # 图像坐标系中的二维点

# 假设拍摄的标定图像位于 /home/images/ 目录下,且全为 PNG 格式
images = glob.glob('/home/anawaert/images/*.png')

# 遍历图像序列
for image in images:
    # 使用 OpenCV 读取图像(必须记得要获取图像的黑白副本)
    img = cv2.imread(image)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # 查找棋盘格角点,其中 (11, 8) 为棋盘格的内角点数
    ret, corners = cv2.findChessboardCorners(gray, (11, 8), None)

    # 若找到角点,则添加到世界坐标和图像坐标中
    if ret:
        obj_points.append(objp)
        corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        img_points.append(corners2)

# 假设拍摄的标定图像大小为 1280 × 720,则函数第三个参数填写 (1280, 720)
ret_val, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, (1280, 720), None, None)

# 假设拍摄的标定图像大小为 1280 × 720,则函数第三、五个参数填写 (1280, 720)
new_camera_matrix, roi = cv2.getOptimalNewCameraMatrix(camera_matrix, dist_coeffs, (1280, 720), 1, (1280, 720))

print(f'Camera matrix is: {camera_matrix} \n')
print(f'Distortion coefficients is: {dist_coeffs} \n')
print(f'New camera matrix is: {new_camera_matrix} \n')
print(f'Available region of interest is: {roi} \n')

使用第三方库来简化标定流程

  总的来看,单目相机的标定代码似乎也不算难写,但是每次利用图像进行标定前,都要写这么一长串的代码似乎也不妥。因此,本文将介绍一个简单的方法,使得各位读者能快速利用一些第三方库的 API 来获取单目标定参数。

安装 unireo

  unireo 是一款小型的计算机视觉 SDK,专为支持 UVC 协议的彩色双目相机打造,目前仍在不断开发和测试中。在 unireo 中,包含了若干与相机标定的 API,所以接下来我们通过 pip 包管理器安装 unireo:

pip install unireo

使用 unireo 并获取单目标定参数

  在代码编辑器中,导入如下模块或子模块:

from unireo.calib import mono_calib

  接着,使用 mono_calib 模块中的 get_calib_data 函数来获取单目标定参数。该函数接受三个参数,分别是棋盘格内角点数、图像的宽高尺寸以及标定所用图像路径的列表,并且返回一个 MonoCalibData 类型的标定数据:

# 假设棋盘格的角点数为 11 × 8,图像尺寸 1280 × 720
# 本例中标定图像所在目录在 /home/anawaert/Images/calib_img/left/
data = mono_calib.get_calib_data((11, 8), (1280, 720), glob.glob('/home/anawaert/Images/calib_img/left/*.jpg'))

  最后使用 . 成员运算符即可获取单目标定参数中的相机矩阵与畸变向量等参数:

print(f'Camera matrix: {data.camera_matrix} \n')
print(f'Distortion coefficients: {data.dist_coeffs} \n')
print(f'New Camera matrix is: {data.new_camera_matrix} \n')
print(f'Available region of interest is: {data.remap_roi} \n')

Get Mono Calib Data with Unireo

  当然,若您当前的 Python 解释器对应环境暂未安装 OpenCV-Python 的话,请先使用 pip install opencv-python==4.9.0.80 命令来安装 OpenCV-Python —— unireo 强依赖于 cv2 模块。

了解更多

  更多关于 unireo 的使用方法,欢迎访问 Anawaert Documents

总结

  本文详细介绍了单目相机标定的基本原理及实现方法,重点展示了如何使用 Python、OpenCV 和 NumPy 在 Ubuntu 系统上编写标定代码。单目相机的标定过程需要通过拍摄多个视角的标定板图像,从而计算相机的内参(如相机矩阵和畸变系数)和外参。当然,这篇文章实际上内容非常简单,其的目的是计算机视觉初学者提供一个实用的指南,帮助读者通过实际代码实现相机标定,把重点放在标定过程中的代码实现和操作步骤而避免深入底层的算法细节。在未来,本文将作为基点而延伸出相机画面的畸变校正、双目相机的内、外参数标定等内容,若各位有其它关于相机单目标定的意见与建议,欢迎在评论区发表。