实操

用 OpenCV 做相机标定:棋盘格与 ChArUco 完整代码

可直接运行、已实测的 Python 标定代码,适用于 OpenCV 4.7 及以上:棋盘格与 ChArUco 角点检测、calibrateCamera 标定、参数标准差与逐图误差、YAML 保存、去畸变和位姿检查。

要点

  • 本文代码适用于 OpenCV 4.7 及以上版本,已在 4.7.0、4.8.0、4.10.0 和 5.0.0 上实际运行通过。
  • 棋盘格:用 findChessboardCornersSB 检测内角点,要求整块棋盘都在画面内。
  • ChArUco:用 CharucoDetector 检测角点,用 matchImagePoints 建立三维–二维对应,允许只拍到部分板面。
  • 标定使用 calibrateCameraExtended,在得到参数的同时输出参数标准差和逐图误差。
  • 结果用 cv2.FileStorage 保存为 YAML,去畸变用 initUndistortRectifyMap 加 remap。

整体流程

不论用哪种标定板,代码都遵循同样的流程:

  1. 检测:在每张图中找到特征点的亚像素坐标;
  2. 配对:为每个检测到的点找到它在标定板坐标系中的三维坐标(z = 0),得到一组“三维点–像点”对应;
  3. 标定:calibrateCamera 先由各图的单应矩阵求出内参初值,再用 Levenberg–Marquardt 算法同时优化内参、畸变和每张图的位姿,使重投影误差最小;
  4. 评估:查看 RMS、参数标准差和逐图误差;
  5. 保存与使用:把结果写入 YAML,供去畸变、位姿估计和测量使用。

棋盘格与 ChArUco 的区别只在前两步:棋盘格要求整板可见,按固定顺序配对;ChArUco 依靠每个角点的 ID 配对,因此允许部分可见。

准备工作

安装 OpenCV 和 NumPy:

pip install "opencv-python>=4.7" numpy

从 OpenCV 4.7 起,ArUco 与 ChArUco 的功能已经并入主模块,不需要安装 contrib 版本。

建议的目录结构:

calib/
├── calib_common.py        # 标定、结果报告、YAML 读写
├── calib_checkerboard.py  # 棋盘格标定
├── calib_charuco.py       # ChArUco 标定
├── undistort.py           # 去畸变
├── pose_check.py          # 求位姿、检查重投影误差
└── images/                # 标定图像

标定图像的采集要求见采集标定图像的最佳实践:15–30 张以上,多角度倾斜,覆盖画面四角。

第一步:公共函数

calib_common.py 负责标定、打印结果和读写 YAML,两种标定板共用:

"""标定、结果报告与 YAML 读写(OpenCV 4.7+)。"""
import time

import cv2
import numpy as np


def calibrate_and_report(obj_points, img_points, image_size, names, flags=0):
    """标定并打印 RMS、参数及其标准差、误差最大的几张图。返回 (rms, K, dist)。"""
    (rms, K, dist, rvecs, tvecs,
     std_intr, std_extr, per_view) = cv2.calibrateCameraExtended(
        obj_points, img_points, image_size, None, None, flags=flags)
    n_pts = sum(len(p) for p in img_points)
    print(f"图像 {len(img_points)} 张,角点 {n_pts} 个,RMS 重投影误差 {rms:.4f} px")
    # std_intr 的顺序:fx fy cx cy k1 k2 p1 p2 k3 k4 k5 k6 s1 s2 s3 s4 τx τy
    labels = ["fx", "fy", "cx", "cy", "k1", "k2", "p1", "p2", "k3"]
    values = [K[0, 0], K[1, 1], K[0, 2], K[1, 2], *dist.ravel()[:5]]
    for name, v, s in zip(labels, values, std_intr.ravel()):
        print(f"  {name} = {v:12.6f}  ± {s:.6f}")
    worst = np.argsort(per_view.ravel())[::-1][:3]
    print("误差最大的 3 张:" + ",".join(f"{names[i]} {per_view[i, 0]:.3f} px" for i in worst))
    return rms, K, dist


def save_yaml(path, K, dist, image_size, rms, note=""):
    fs = cv2.FileStorage(path, cv2.FILE_STORAGE_WRITE)
    fs.write("calibration_time", time.strftime("%Y-%m-%d %H:%M:%S"))
    fs.write("note", note)
    fs.write("image_width", int(image_size[0]))
    fs.write("image_height", int(image_size[1]))
    fs.write("camera_matrix", K)
    fs.write("distortion_coefficients", dist)
    fs.write("rms_px", float(rms))
    fs.release()


def load_yaml(path):
    fs = cv2.FileStorage(path, cv2.FILE_STORAGE_READ)
    if not fs.isOpened():
        raise FileNotFoundError(path)
    K = fs.getNode("camera_matrix").mat()
    dist = fs.getNode("distortion_coefficients").mat()
    size = (int(fs.getNode("image_width").real()), int(fs.getNode("image_height").real()))
    fs.release()
    return K, dist, size

几点说明:

  • calibrateCameraExtended 与 calibrateCamera 的求解过程相同,额外返回内参的标准差(1σ)和每张图的 RMS 误差(单位为像素)。
  • flags=0 时使用 5 参数畸变模型(k1 k2 p1 p2 k3)。畸变很小的镜头可以传入 cv2.CALIB_FIX_K3;广角镜头可以试 cv2.CALIB_RATIONAL_MODEL,但要有充分的边角覆盖,否则容易过拟合,见相机模型怎么选。
  • 返回的 RMS 是所有点残差长度的均方根,含义和局限见看懂标定结果。

calibrateCamera 常用的 flags(可以用 | 组合后传给 calibrate_and_report 的 flags 参数):

flag 作用 什么时候用
CALIB_FIX_K3 k3 固定为 0 视场小、畸变轻,k3 约束不住时
CALIB_ZERO_TANGENT_DIST p1、p2 固定为 0 数据较少,或残差中看不到切向规律时
CALIB_RATIONAL_MODEL 启用 k4–k6,共 8 个畸变参数 广角、畸变大的镜头
CALIB_THIN_PRISM_MODEL 启用薄棱镜项 s1–s4 高精度应用,数据量大、覆盖好时
CALIB_FIX_ASPECT_RATIO 保持 fx/fy 等于初始相机矩阵中的比例;不传初值时即 fx = fy 像元为正方形、希望减少一个参数时
CALIB_FIX_PRINCIPAL_POINT 主点固定在图像中心,与 CALIB_USE_INTRINSIC_GUESS 合用时固定在给定位置 一般不建议,只在数据确实不足时考虑
CALIB_USE_INTRINSIC_GUESS 以传入的相机矩阵为初值 已有可靠初值,或配合上面需要初值的选项

calib_common.py 中传入的初始相机矩阵是 None。需要指定初值(例如固定一个已知的主点)时,把 None 换成 3×3 的初始相机矩阵,并加上 CALIB_USE_INTRINSIC_GUESS。

第二步:棋盘格标定

"""棋盘格标定:findChessboardCornersSB + calibrateCamera(OpenCV 4.7+)。
用法:python calib_checkerboard.py "images/*.png"
"""
import glob
import sys

import cv2
import numpy as np

from calib_common import calibrate_and_report, save_yaml

# ---- 按实际标定板修改 ----
SQUARES_X, SQUARES_Y = 11, 8      # 方格数(横向 × 纵向),不是角点数
SQUARE_MM = 25.0                  # 方格边长,mm
IMAGE_GLOB = sys.argv[1] if len(sys.argv) > 1 else "images/*.png"
# --------------------------

pattern = (SQUARES_X - 1, SQUARES_Y - 1)   # 内角点数 = 方格数 - 1
objp = np.zeros((pattern[0] * pattern[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:pattern[0], 0:pattern[1]].T.reshape(-1, 2) * SQUARE_MM

# EXHAUSTIVE:检测更稳;ACCURACY:亚像素精度更高(较慢)。光照不均时可再加 CALIB_CB_NORMALIZE_IMAGE
flags = cv2.CALIB_CB_EXHAUSTIVE | cv2.CALIB_CB_ACCURACY
obj_points, img_points, names = [], [], []
image_size = None
for path in sorted(glob.glob(IMAGE_GLOB)):
    gray = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
    if gray is None:
        continue
    if image_size is None:
        image_size = gray.shape[::-1]          # (宽, 高)
    elif gray.shape[::-1] != image_size:
        sys.exit(f"{path} 的分辨率与其他图像不同")
    found, corners = cv2.findChessboardCornersSB(gray, pattern, flags=flags)
    if not found:
        print(f"[跳过] {path}:未检测到完整棋盘格")
        continue
    obj_points.append(objp)
    img_points.append(corners)
    names.append(path)

if len(img_points) < 10:
    sys.exit(f"有效图像只有 {len(img_points)} 张,至少需要 10 张(建议 15–30 张)")
rms, K, dist = calibrate_and_report(obj_points, img_points, image_size, names)
save_yaml("camera_checkerboard.yaml", K, dist, image_size, rms,
          note=f"checkerboard {SQUARES_X}x{SQUARES_Y} {SQUARE_MM} mm")
print("已保存 camera_checkerboard.yaml")

需要注意:

  • pattern 是内角点数,等于方格数减 1。11×8 个方格的板,pattern 是 (10, 7)。把方格数直接填进去,是检测失败的常见原因。
  • SQUARE_MM 决定外参平移和三维结果的尺度。填错不会影响内参,却会让平移量按比例出错,务必与实物核对。
  • findChessboardCornersSB 比传统的 findChessboardCorners 加 cornerSubPix 更稳健,直接返回亚像素角点。CALIB_CB_ACCURACY 会明显增加检测时间:在我们的测试中,它让每张 1920×1200 图像的检测耗时增加数倍,但 RMS 从约 0.12 px 降到约 0.065 px。
  • 棋盘格四周要留有白边,并且每张图都必须包含完整的棋盘。

第三步:ChArUco 标定

"""ChArUco 标定:CharucoDetector + matchImagePoints + calibrateCamera(OpenCV 4.7+)。
用法:python calib_charuco.py "images/*.png"
"""
import glob
import sys

import cv2

from calib_common import calibrate_and_report, save_yaml

# ---- 按实际标定板修改(与板上标签一致)----
SQUARES_X, SQUARES_Y = 11, 8          # 方格数(横向 × 纵向)
SQUARE_MM, MARKER_MM = 25.0, 18.75    # 方格边长、标记边长,mm
DICTIONARY = cv2.aruco.DICT_5X5_1000
LEGACY_PATTERN = False                # 仅当板子按 OpenCV 4.6 之前的规则生成且纵向方格数为偶数时设为 True
MIN_CORNERS = 8                       # 每张图至少需要的角点数
IMAGE_GLOB = sys.argv[1] if len(sys.argv) > 1 else "images/*.png"
# ------------------------------------------

CV_VERSION = tuple(int(v) for v in cv2.__version__.split(".")[:2])
dictionary = cv2.aruco.getPredefinedDictionary(DICTIONARY)
board = cv2.aruco.CharucoBoard((SQUARES_X, SQUARES_Y), SQUARE_MM, MARKER_MM, dictionary)
if LEGACY_PATTERN:
    board.setLegacyPattern(True)      # OpenCV 4.8 起提供
detector = cv2.aruco.CharucoDetector(board)

obj_points, img_points, names = [], [], []
image_size = None
for path in sorted(glob.glob(IMAGE_GLOB)):
    gray = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
    if gray is None:
        continue
    if image_size is None:
        image_size = gray.shape[::-1]
    elif gray.shape[::-1] != image_size:
        sys.exit(f"{path} 的分辨率与其他图像不同")
    ch_corners, ch_ids, marker_corners, marker_ids = detector.detectBoard(gray)
    n = 0 if ch_ids is None else len(ch_ids)
    if n < MIN_CORNERS or board.checkCharucoCornersCollinear(ch_ids):
        print(f"[跳过] {path}:角点 {n} 个,不足或共线")
        continue
    if CV_VERSION >= (4, 8):
        obj, img = board.matchImagePoints(ch_corners, ch_ids)   # 按角点 ID 配对三维点与像点
    else:  # 4.7.x 的 CharucoBoard.matchImagePoints 有缺陷,直接按 ID 取角点的板坐标
        obj = board.getChessboardCorners()[ch_ids.ravel()].reshape(-1, 1, 3)
        img = ch_corners
    obj_points.append(obj)
    img_points.append(img)
    names.append(path)

if len(img_points) < 10:
    sys.exit(f"有效图像只有 {len(img_points)} 张,至少需要 10 张(建议 15–30 张)")
rms, K, dist = calibrate_and_report(obj_points, img_points, image_size, names)
save_yaml("camera_charuco.yaml", K, dist, image_size, rms,
          note=f"charuco {SQUARES_X}x{SQUARES_Y} {SQUARE_MM}/{MARKER_MM} mm")
print("已保存 camera_charuco.yaml")

需要注意:

  • 字典、方格数、方格边长、标记边长必须与实物完全一致。BDB 标定板在留白足够时会印有参数标签,例如 CHARUCO 11x8 S25 M18.75mm 5X5_1000,其中 S 是方格边长、M 是标记边长;没有标签时以订购规格为准。方格数是“横向 × 纵向”,不要颠倒。
  • detectBoard 返回角点坐标、角点 ID、标记角点和标记 ID。正是因为每个角点都有 ID,部分遮挡时也能找到它对应的三维坐标。
  • matchImagePoints 按 ID 生成三维点与像点的对应关系。OpenCV 4.7.x 中 CharucoBoard 的 matchImagePoints 有缺陷:它按 ArUco 标记的方式处理输入,返回的点数和对应关系都是错的,标定结果会完全偏离。代码中对 4.7 改用 getChessboardCorners() 按 ID 取三维坐标,结果与 4.8 及以上版本一致。
  • checkCharucoCornersCollinear 用来剔除角点全部位于一条直线上的图像,这类图像提供不了有效的单应约束,可能导致初始化失败。
  • OpenCV 4.6 修改了 ChArUco 的排布规则:纵向方格数为偶数时,旧规则生成的板左上角是白格,新规则是黑格。如果你的板是按旧规则生成的,把 LEGACY_PATTERN 设为 True(setLegacyPattern 从 OpenCV 4.8 起提供)。BDB ChArUco 标定板采用左上角为黑格的新规则,无需设置。
  • OpenCV 定义的 ChArUco 板坐标原点在整块图案的左上角外角,第一个内角点的坐标是 (方格边长, 方格边长, 0)。这只影响外参的表达,不影响内参。

运行结果示例

我们用 BDB 渲染器按已知参数合成了两组 1920×1200 的图像(各 20 张),用来验证本文代码。真值为 fx = 1400、fy = 1398.6、cx = 965.3、cy = 597.8,畸变 k1 = −0.21、k2 = 0.085、p1 = 0.0006、p2 = −0.0004、k3 = −0.012。ChArUco 组中有不少图像只拍到部分板面。在 OpenCV 5.0.0 下的输出如下:

$ python calib_charuco.py "images/charuco/*.png"
图像 20 张,角点 1168 个,RMS 重投影误差 0.0988 px
  fx =  1400.082656  ± 0.509335
  fy =  1398.691877  ± 0.472190
  cx =   965.088410  ± 0.513075
  cy =   598.313778  ± 0.513679
  k1 =    -0.209922  ± 0.000631
  k2 =     0.084476  ± 0.001519
  p1 =     0.000559  ± 0.000052
  p2 =    -0.000473  ± 0.000062
  k3 =    -0.011580  ± 0.001237
误差最大的 3 张:images/charuco/img_17.png 0.347 px,images/charuco/img_16.png 0.174 px,images/charuco/img_04.png 0.072 px
已保存 camera_charuco.yaml

棋盘格组:

$ python calib_checkerboard.py "images/checkerboard/*.png"
图像 20 张,角点 1400 个,RMS 重投影误差 0.0653 px
  fx =  1399.643084  ± 0.299660
  fy =  1398.112794  ± 0.292270
  cx =   964.652290  ± 0.308468
  cy =   597.496384  ± 0.279134
  k1 =    -0.209976  ± 0.000442
  k2 =     0.084172  ± 0.001378
  p1 =     0.000527  ± 0.000027
  p2 =    -0.000401  ± 0.000029
  k3 =    -0.010926  ± 0.001504
误差最大的 3 张:images/checkerboard/img_15.png 0.143 px,images/checkerboard/img_00.png 0.140 px,images/checkerboard/img_18.png 0.135 px
已保存 camera_checkerboard.yaml

可以看到,各参数估计值与真值之差大多在 1–2 倍标准差以内,个别参数(如棋盘格组的 p1)接近 3 倍。标准差只反映随机误差,宜把它当作误差的下限来看。逐图误差中最大的一张,是标定板大角度倾斜且只有部分可见的图像。同一组图像在 OpenCV 4.7.0、4.8.0、4.10.0 下也都能正常运行;各版本的检测器有细微差别,角点数和结果的末几位会略有不同。另外,4.7 输出的标准差约为 4.8 及以上版本的 1.45 倍(参数值本身相同),跨版本比较标准差时要注意;OpenCV 5.0 默认多线程计算,标准差的末一两位在多次运行之间会有细微差别,参数值不受影响。

合成图像没有真实镜头的像差和真实的噪声特性,实拍的 RMS 一般会更高,不要把这里的数值当作实拍的标准。

第四步:保存的 YAML 文件

上面的脚本会生成 camera_charuco.yaml。以 OpenCV 4.10 生成的文件为例:

%YAML:1.0
---
calibration_time: "2026-10-05 22:03:38"
note: "charuco 11x8 25.0/18.75 mm"
image_width: 1920
image_height: 1200
camera_matrix: !!opencv-matrix
   rows: 3
   cols: 3
   dt: d
   data: [ 1399.8680813405788, 0., 965.6048456126922, 0.,
       1398.4599648420738, 598.9485686496929, 0., 0., 1. ]
distortion_coefficients: !!opencv-matrix
   rows: 1
   cols: 5
   dt: d
   data: [ -0.21004663424619494, 0.084939668454632522,
       0.00058300578928788672, -0.00045062490225190887,
       -0.011936195358638548 ]
rms_px: 0.10582644798264869

这是 OpenCV 自己的 YAML 格式:4.x 的文件头是 %YAML:1.0(5.0 写出的是 %YAML 1.2),矩阵带有 !!opencv-matrix 标签,通用的 YAML 库直接读取可能报错。建议始终用 cv2.FileStorage 读回,也就是 calib_common.py 中的 load_yaml。

第五步:去畸变

"""读取 YAML 标定结果并对图像去畸变。
用法:python undistort.py camera_charuco.yaml input.png
"""
import sys

import cv2

from calib_common import load_yaml

K, dist, (w, h) = load_yaml(sys.argv[1])
img = cv2.imread(sys.argv[2])
if img is None or img.shape[1] != w or img.shape[0] != h:
    sys.exit("图像读取失败,或分辨率与标定时不一致(裁剪/binning 后需重新标定或换算内参)")

# alpha=0:裁掉边缘无效区域;alpha=1:保留全部原始像素(四周会出现黑边)
new_K, roi = cv2.getOptimalNewCameraMatrix(K, dist, (w, h), alpha=0)
# 同一相机反复处理时,映射表只需计算一次
map1, map2 = cv2.initUndistortRectifyMap(K, dist, None, new_K, (w, h), cv2.CV_16SC2)
out = cv2.remap(img, map1, map2, interpolation=cv2.INTER_LINEAR)
cv2.imwrite("undistorted.png", out)
print("新相机矩阵:\n", new_K.round(2))

运行方法:python undistort.py camera_charuco.yaml input.png,结果保存为 undistorted.png。

需要注意:

  • alpha=0 时,OpenCV 会分别在 x、y 方向缩放,让有效区域填满输出图像,因此新相机矩阵的 fx、fy 可能不再相等。在去畸变后的图像上做测量或三维计算时,要用 new_K,而不是原来的 K。
  • 只需要对少量特征点去畸变时,用 cv2.undistortPoints(pts, K, dist, P=new_K) 直接计算点坐标,比对整幅图像做 remap 快得多。
  • 一个简单的检验方法:去畸变后,棋盘格的每一行、每一列角点都应在一条直线上。在上面的合成数据中,一张靠近画面角落的棋盘格图像,角点行列偏离直线的最大值从去畸变前的约 0.93 px 降到了约 0.05 px。

第六步:用标定结果求位姿,顺便做检查

标定的最终目的往往是测量或定位。下面的脚本读取 YAML,对一张图检测 ChArUco 角点,用 solvePnP 求标定板相对相机的位姿,再用 projectPoints 手工计算重投影误差,并把检测到的角点和板的坐标轴画到图上:

"""用标定结果求 ChArUco 标定板位姿,画出检测结果与坐标轴,并手工计算重投影误差。
用法:python pose_check.py camera_charuco.yaml image.png
"""
import sys

import cv2
import numpy as np

from calib_common import load_yaml

K, dist, _ = load_yaml(sys.argv[1])
dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_5X5_1000)
board = cv2.aruco.CharucoBoard((11, 8), 25.0, 18.75, dictionary)
detector = cv2.aruco.CharucoDetector(board)

img = cv2.imread(sys.argv[2])
ch_corners, ch_ids, _, _ = detector.detectBoard(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY))
if ch_ids is None or len(ch_ids) < 6:
    sys.exit("角点太少")
obj = board.getChessboardCorners()[ch_ids.ravel()]          # 各版本通用的配对方式
ok, rvec, tvec = cv2.solvePnP(obj, ch_corners, K, dist)    # 标定板 -> 相机 的位姿

proj, _ = cv2.projectPoints(obj, rvec, tvec, K, dist)
err = np.linalg.norm(proj.reshape(-1, 2) - ch_corners.reshape(-1, 2), axis=1)
print(f"角点 {len(err)} 个,重投影 RMS {np.sqrt((err ** 2).mean()):.3f} px,最大 {err.max():.3f} px")
print("标定板原点在相机坐标系中的位置 (mm):", tvec.ravel().round(1))

# OpenCV 5.0 的 detectBoard 返回 (N,2)/(N,),4.x 返回 (N,1,2)/(N,1);绘制前统一形状
cv2.aruco.drawDetectedCornersCharuco(img, ch_corners.reshape(-1, 1, 2), ch_ids.reshape(-1, 1), (0, 0, 255))
cv2.drawFrameAxes(img, K, dist, rvec, tvec, 50)             # 坐标轴长 50 mm
cv2.imwrite("pose.png", img)
$ python pose_check.py camera_charuco.yaml images/charuco/img_03.png
角点 70 个,重投影 RMS 0.061 px,最大 0.142 px
标定板原点在相机坐标系中的位置 (mm): [-469.8 -130.3  637.5]

这里有两点值得注意。第一,三维点直接用 board.getChessboardCorners()[ids] 按 ID 取出,这种写法在所有版本中都正确,也便于理解 matchImagePoints 实际做了什么。第二,OpenCV 5.0 中 detectBoard 返回的数组形状有变化:角点由 4.x 的 (N,1,2) 变为 (N,2),ID 由 (N,1) 变为 (N,)。solvePnP、calibrateCamera 等函数两种形状都能接受,但 drawDetectedCornersCharuco 在 5.0 中直接传入 (N,2) 会报断言错误,所以代码在绘制前统一做了 reshape。

单张图的重投影误差可以用来检查新拍的图像:如果某张图的误差明显高于标定时的 RMS,可能是图像本身有问题(模糊、误检),也可能是相机状态发生了变化(被碰过、重新对焦过),需要进一步排查。

鱼眼镜头和其他软件怎么办?

鱼眼镜头:视场接近或超过 180° 时,应改用 cv2.fisheye 模块(Kannala-Brandt 模型):标定用 cv2.fisheye.calibrate,去畸变用 cv2.fisheye.estimateNewCameraMatrixForUndistortRectify 和 cv2.fisheye.initUndistortRectifyMap。几个常见的坑:输入的三维点和像点要整理成形状为 (N,1,3)、(N,1,2) 的 float64 数组;常用的 flags 是 CALIB_RECOMPUTE_EXTRINSIC 与 CALIB_FIX_SKEW 的组合,这些常量在 OpenCV 4.x 中位于 cv2.fisheye 下(如 cv2.fisheye.CALIB_FIX_SKEW),在 OpenCV 5.0 中移到了 cv2 顶层(如 cv2.CALIB_FIX_SKEW);启用 CALIB_CHECK_COND 后,条件数很差的图像会直接导致报错,需要找出并去掉这类图像后重试。

在其他程序中使用结果:C++ 程序可以用 cv::FileStorage 直接读取本文保存的 YAML。ROS 的相机标定文件使用另一种 YAML 格式,其中 plumb_bob 畸变模型的 5 个系数与 OpenCV 的 (k1, k2, p1, p2, k3) 顺序相同,可以直接对应填写。

常见问题

检测不到棋盘格? 先检查 pattern 是否填成了方格数;再看板四周有没有白边、是否有一角出画、是否过曝或反光、倾斜是否过大。

ChArUco 能检测到标记,却没有角点? 多半是参数与实物不符:字典、方格数(横纵是否颠倒)、方格与标记边长、新旧排布规则。也可能是标记太小,或 CharucoDetector 的参数过严。

RMS 很大(超过 1 px)? 检查方格尺寸和图案参数是否填对,图像里是否混入了不同分辨率、不同对焦状态的图像,以及是否有误检的图像。

calibrateCamera 报错或结果离谱? 有效图像太少、角点共线、或者输入数组的类型不是 float32。在 OpenCV 4.7 上使用 ChArUco 时,确认没有直接调用 matchImagePoints。

两次标定结果差别很大? 通常是采集的姿态不够多样,尤其是倾斜不足、边角覆盖不够。参见采集标定图像的最佳实践。

如果不想自己维护代码,也可以用 BDB 相机标定工作台完成检测、标定、结果评估和导出,它支持本文两种标定板以及圆点、AprilGrid 等图案。

需要帮您选标定板?

把相机分辨率、像元尺寸、焦距、工作距离和应用场景告诉我们,我们给出图案、尺寸与基材建议。