ChArUco 标定不是多拍几张:用姿态覆盖与参数可观性收敛
从部分可见角点到法方程条件数,用覆盖、逐视图误差与留出集判断内参是否真的可辨识。

1. 五十张正面板,不如十张互补姿态
产线相机换镜头后,对桌面 ChArUco 板连拍五十张,OpenCV 全局 RMS 很小;板移到边缘,PnP 深度却随位置漂移。再补相似照片,、、 仍在批次间跳动。原因是观测反复约束同一组合:焦距、板距和径向畸变在窄姿态内可以互相补偿。
ChArUco 用 ArUco ID 建立局部可见的全局对应,再以棋盘交点取得亚像素位置;它允许遮挡,却不会创造几何激励。标定要问:角点是否覆盖工作区域;法向、距离和位置是否让参数产生不同响应;留出数据能否复现同一组内参。
针孔加畸变模型对第 个视图、第 个板角点的残差可写为
内参 跨视图共享,每张图的 是干扰变量。重复姿态只增加同一方向的权重,不一定增加雅可比的秩。互补姿态加入后参数、预测误差与不确定性都稳定,才算收敛。
2. 先锁定板的几何与 OpenCV 版本
squaresX/squaresY 是方格数,不是内部角点数;squareLength 与 markerLength 同单位,tvec 随之取同一单位。打印缩放、覆膜会改变比例或平整度,应实测横纵方格、检查 X/Y 缩放并记录板号与文件哈希。板翘曲时,二维角点再准也不符合 。
现代 OpenCV 4.x Python 接口可直接构造对象:
import cv2
dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_5X5_100)
board = cv2.aruco.CharucoBoard(
(8, 6), # 横、纵方格数,不是角点数
0.030, # squareLength,单位 m
0.022, # markerLength,单位 m
dictionary,
)
detector_params = cv2.aruco.DetectorParameters()
charuco_params = cv2.aruco.CharucoParameters()
detector = cv2.aruco.CharucoDetector(
board, charuco_params, detector_params)较早的 4.x/contrib 绑定使用 CharucoBoard_create()、detectMarkers() 加 interpolateCornersCharuco();CharucoDetector 是较新接口。记录 cv2.__version__ 并检查类是否存在。calibrateCameraCharuco() 与 calibrateCameraCharucoExtended() 在现代 OpenCV 4.x 中仍可能保留 Python 绑定,但官方已把这组 ChArUco 专用标定函数标为 deprecated,不能把“当前还能调用”写成长期接口承诺。新代码应通过 board.matchImagePoints() 生成每个视图的 3D/2D 对应,再调用通用的 calibrateCamera() 或 calibrateCameraExtended()。
OpenCV 4.6.0 前后的 board 坐标约定存在不兼容变化:marker object points 的环绕次序以及由此确定的 Z 轴方向改变。新约定下应以 matchImagePoints() 返回的 object points 为准,并用 drawFrameAxes() 核对原点与轴向。若下游系统仍消费旧版本定义的位姿,必须显式乘一个约定好的刚体变换;切换 pattern 选项不会替你恢复旧坐标系。
另一个独立问题是棋盘图案兼容。OpenCV 4.6.0 改变了偶数行 ChArUco 板的黑白格起始方式和 marker 布局;OpenCV 4.8+ 才提供 board.setLegacyPattern(True),用于识别或生成 4.6.0 之前的旧 pattern。它只处理 pattern,不改变当前版本的 object-point 坐标约定。奇数行旧板通常不受这项 pattern 变化影响;使用 4.6/4.7 时也不能假设该方法存在。内参文件因此应分别绑定板规格、实体板 pattern/legacy 设置、位姿坐标系定义和 OpenCV 版本。
3. 部分可见不是“角点越少越随意”
detectBoard() 先检测有 ID 的 marker,再插值得到棋盘角点。提供内参时可借粗略位姿重投影;无内参时依赖局部单应,遇到大倾角、边缘和少量 marker 会变弱。部分板出画可用,不代表挤在一起的少数角点信息充分。
all_corners, all_ids, accepted = [], [], []
image_size = None
for path in image_paths:
image = cv2.imread(path, cv2.IMREAD_COLOR)
if image is None:
continue
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
image_size = (gray.shape[1], gray.shape[0])
corners, ids, marker_corners, marker_ids = detector.detectBoard(gray)
if ids is None or len(ids) < 6:
continue
ids = ids.astype("int32")
if board.checkCharucoCornersCollinear(ids):
continue
all_corners.append(corners.astype("float32"))
all_ids.append(ids)
accepted.append(path)六个角点只是示例下限,关键是数量、二维分布和不共线。每张图还应保存 marker 数、凸包面积、图像半径、清晰度与饱和比例。窄边上角点虽多仍可能退化;checkCharucoCornersCollinear() 也不能判断它对 或主点是否有价值。
模糊、过曝和过小 marker 会产生方向偏差或错 ID。调整 DetectorParameters、corner refinement 或 minMarkers 后必须整批重检。可先用无内参的 homography 模式求初值,再设置 charuco_params.cameraMatrix = K、charuco_params.distCoeffs = dist 并重建 detector,对全部原图统一二次检测,比较角点位移与参数变化。
4. 把采集变成覆盖状态机
姿态覆盖有四维:角点凸包进入中心、四边和四角以约束径向畸变;板法向绕水平、竖直轴都变化以解耦焦距与 Z 距离;近中远尺度都出现;滚转改变角点相对像素行列的方向,帮助区分切向畸变与板误差。
采集 UI 应显示未覆盖网格。第一轮计算角点占用、凸包、中心、主方向和尺度;得到临时内参后,用 matchImagePoints() 与 solvePnP() 估计法向、距离和旋转:
import numpy as np
obj, img = board.matchImagePoints(corners, ids)
ok, rvec, tvec = cv2.solvePnP(obj, img, K, dist)
if ok:
R, _ = cv2.Rodrigues(rvec)
normal_camera = R[:, 2]
view_feature = np.r_[
np.mean(corners.reshape(-1, 2), axis=0) / image_size,
np.linalg.norm(tvec),
normal_camera,
]角度不设跨项目绝对阈值。应确认外圈有独立视图、法向在两个轴上有正负变化、近中远有样本,且重复姿态不主导权重。可按特征向量距离拒绝连拍,只保留少量重复帧估计检测重复性。
部分可见可把角点送到传感器边缘,但单图外参约束变弱,应与全板可见、不同倾角的锚点视图混合。先以中心倾斜图建初值,再扫边角并补距离、滚转空缺,每保存一张就更新覆盖。
5. 从 RMS 走到参数可观性
标定优化在解附近线性化后,残差变化近似为 。若假设角点噪声方差为 ,局部信息矩阵和协方差近似为
有很小特征值时,一组参数可共同变化而几乎不改重投影。因焦距、畸变、平移量纲不同,原始条件数会失真;先按参数尺度或雅可比列范数归一化,再看最小奇异值、条件数和相关参数对。它只适合在同一模型与归一化规则下比较批次。
extended 接口返回 stdDeviationsIntrinsics,顺序为 ,未估计项为零。它只有局部对角不确定性,没有相关矩阵。可有限差分构造雅可比,并用 Schur complement 消去每视图外参:
采集工具可看候选视图加入后,归一化 的最小奇异值或 是否改善。重复正面帧很快饱和,边缘倾斜帧可能补上弱方向。
普通镜头先用 。CALIB_RATIONAL_MODEL 新增 ,若只由少量外圈点约束,会吸收板翘曲或热漂移。鱼眼应评估 cv2.fisheye。只有留出空间误差改善、参数稳定且不确定性可接受,才升级模型。
6. 标定、逐视图残差与稳定性重采样
现代接口先把每张图的 ChArUco ID 映射为标准标定接口需要的 object/image points。matchImagePoints() 已按 board 当前几何与坐标约定完成 ID 匹配,不要自行假设角点连续或按检测顺序生成三维点。需要内参标准差与逐视图误差时调用 cv2.calibrateCameraExtended():
flags = 0 # 先用常规模型;有证据再增加参数
criteria = (
cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
100,
1e-9,
)
object_points, image_points = [], []
for corners, ids in zip(all_corners, all_ids):
obj, img = board.matchImagePoints(corners, ids)
object_points.append(obj.astype("float32"))
image_points.append(img.astype("float32"))
(rms, K, dist, rvecs, tvecs,
std_intr, std_extr, per_view) = \
cv2.calibrateCameraExtended(
object_points,
image_points,
image_size,
None,
None,
flags=flags,
criteria=criteria,
)
print("global RMS:", float(rms))
print("per-view RMS:", per_view.ravel())
print("intrinsic std:", std_intr.ravel())已有工程若必须兼容旧 OpenCV 或既有结果,可暂时保留 deprecated 的 ChArUco 专用 extended 调用;它的 Python 返回值仍是全局 RMS、内参、畸变、逐视图外参、内外参标准差和逐视图误差共八项:
# 兼容路径:现代 OpenCV 文档已将该接口标为 deprecated。
(rms, K, dist, rvecs, tvecs,
std_intr, std_extr, per_view) = \
cv2.aruco.calibrateCameraCharucoExtended(
all_corners, all_ids, board, image_size, None, None,
flags=flags, criteria=criteria)迁移前后应固定同一批检测结果、flags 与 termination criteria,对比角点对应数、RMS、、畸变参数和逐视图外参,防止接口迁移同时悄悄改变 board convention。若环境只有不带 extended 输出的接口,可用 board.matchImagePoints() 取得 object points,再用 projectPoints() 自行计算逐点和逐视图残差。二维残差箭头比单个平均数更有诊断力:边缘一致向外提示畸变或板尺度,整图同向提示角点/外参异常。
高 per-view error 不能机械删除。先查原图、角点、板平整度,以及它是否是唯一边缘/倾斜约束;删除困难姿态可能降低可观性。按明确质量原因剔除,再做 leave-one-view-out 或姿态簇 bootstrap。若一张普通图就能大幅改变 ,应继续补独立姿态。
固定相机与板拍重复图,用同一 ID 的散布估计像素噪声;移动板数据用于几何可观性。两者不能混为“更多样本”,必要时按姿态簇均衡采样,避免中心连拍淹没稀疏区。
7. 留出验证:按姿态留出,不按角点随机切分
每图随机留角点会泄漏外参;应按整视图、最好按姿态簇留出,让某个边缘位置、距离层或倾斜方向不参与拟合。固定 后为留出图估位姿,比较 RMS 与残差结构,检验工作域外推。
重投影仍是自洽指标。尺度任务还需独立刚体或位移台真值,固定内参做 PnP 后比较距离与方向。不能用同一打印板的标称方格做尺度验收,否则比例错误会进入“真值”;直线测试也应使用独立长直边。
建议保存以下验收产物,而不是只导出 YAML:
- 训练与留出视图清单、每张图的角点 ID 和检测参数;
- 角点覆盖图、板中心/尺度/法向分布,以及未覆盖区域;
- 全局 RMS、逐视图 RMS、逐点残差向量和半径分桶统计;
- 、flags、标准差,以及重采样后的参数分布;
- 独立 metric 目标的误差,按距离和画面位置分层;
- OpenCV 版本、图像分辨率、裁剪/缩放、板规格和温度。
阈值来自下游任务;先写像素与 metric 预算,再看结果,不能见到漂亮 RMS 后倒推“够用”。
8. 热漂移与上线边界:一次收敛不是永久常量
焦距会随温度、对焦和机械应力变化。记录冷启动时间、温度、曝光及对焦值;在工作温度稳定后主标定,再在温区两端拍固定留出姿态,比较内参、径向残差和独立 metric 误差。
变化小于预算时规定预热条件;变化可重复时按温区维护参数;不重复时先处理镜头固定、自动对焦或散热。把不同温度图片混合只会得到折中内参,不能用高阶畸变吸收时间变化。
上线核对分辨率、binning、ROI、缩放与标定一致,裁剪后正确变换 cameraMatrix,legacy 约定一致,留出集未参与模型选择,并打印版本、镜头号与温区。换镜头、对焦、分辨率或 ISP 几何后,旧标定应失效。
停止采集的判断因此很具体:目标图像区域和姿态区间已经覆盖;新增互补视图对归一化信息矩阵的改善趋于饱和;逐视图与空间残差没有未解释结构;整视图留出和独立尺度验证满足业务预算;按姿态重采样后参数稳定;工作温度范围内结论仍成立。满足这些条件时,二十张可能足够;不满足时,两百张近重复照片也只是把错误答案算得更自信。ChArUco 解决的是可靠对应,真正让标定收敛的是可观测的实验设计。
相关
也可以看看
johan's blog