用 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。
整体流程
不论用哪种标定板,代码都遵循同样的流程:
- 检测:在每张图中找到特征点的亚像素坐标;
- 配对:为每个检测到的点找到它在标定板坐标系中的三维坐标(z = 0),得到一组“三维点–像点”对应;
- 标定:calibrateCamera 先由各图的单应矩阵求出内参初值,再用 Levenberg–Marquardt 算法同时优化内参、畸变和每张图的位姿,使重投影误差最小;
- 评估:查看 RMS、参数标准差和逐图误差;
- 保存与使用:把结果写入 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 等图案。