OpenCV ChArUco 标定板实战:从板图生成、角点检测到位姿估计的完整指南
ChArUco(Charuco)板把棋盘格的角点精度与 ArUco 标记的检测灵活性合二为一,是 OpenCV aruco 模块中面向高精度相机标定与位姿估计的核心标定物。本文以 charuco_detection.markdown 官方教程为主线,结合本仓库中 detect_board_charuco.cpp、create_board_charuco.cpp 及 charuco_detector.hpp 源码,系统讲解 ChArUco 板的创建、角点插值检测原理、两种位姿估计调用链,并提供可直接运行的命令行示例与关键 API 说明。读完本文,你将能独立完成 ChArUco 板图生成、无标定/带标定两种模式下的角点检测,以及基于 solvePnP 的高精度位姿估计。
为什么要用 ChArUco 板:两种传统方案的取舍
ArUco 标记与 ArUco 板检测快、容错强(允许部分遮挡、部分视野),但它的一个短板是:即使做了亚像素细化,角点位置的精度依然不够高。
与之相对,棋盘格的每个角点都被两个黑色方块包围,角点定位可以被细化得非常精确;但棋盘格的检测非常"娇气"——必须完整可见,不允许任何遮挡,灵活性远不如 ArUco 板。
ChArUco 板的设计目标正是融合二者的优点:
- ArUco 部分负责插值棋盘格角点的位置,因此它继承了标记板的灵活性——允许遮挡与部分视野;
- 插值出的角点从属于棋盘格结构,因此天然具备很高的亚像素精度。
当精度成为首要诉求时(典型场景就是相机标定),ChArUco 板是比普通 ArUco 板更优的选择。这也是 charuco_detector.hpp 中对 CharucoBoard 的官方注释:它"同时提供 ArUco 标记的通用性与棋盘格角点的精度,对标定与位姿估计至关重要"。
本教程的目标
- 如何创建一个 ChArUco 板?
- 如何在不进行相机标定的情况下检测 ChArUco 角点?
- 如何在携带相机标定参数的情况下检测 ChArUco 角点并完成位姿估计?
示例代码位置与运行环境
ChArUco 全套示例位于仓库的 samples/cpp/tutorial_code/objectDetection/ 目录下,其中本文涉及两个核心程序:
| 文件 | 作用 |
|---|---|
| create_board_charuco.cpp | 生成一张可打印的 ChArUco 板图片 |
| detect_board_charuco.cpp | 读取图像/视频/相机,检测 ChArUco 角点并估计位姿 |
两个程序均通过 cv::CommandLineParser 解析命令行参数,并共用工具头文件 aruco_samples_utility.hpp(内含相机参数读取、字典解析等辅助函数)。
ChArUco 相关的公共 API 声明集中在两个头文件中,均需包含到你的工程里:
// 使用 ChArUco 板必须包含的头文件
#include <opencv2/objdetect/charuco_detector.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/calib3d.hpp> // solvePnP、FileStorage 所在模块
ChArUco 板的创建(Board Creation)
构造所需的五个要素
aruco 模块提供 cv::aruco::CharucoBoard 类表示 ChArUco 板,它继承自 cv::aruco::Board。定义一个 CharucoBoard 需要:
- X、Y 两个方向上的棋盘格方块数;
- 方块边长(square side length);
- 标记边长(marker side length);
- 标记使用的字典(dictionary);
- 所有标记的 id。
与 cv::aruco::GridBoard 一样,模块提供了非常简洁的构造方式。从 create_board_charuco.cpp 中可见其用法:
cv::aruco::CharucoBoard board(Size(squaresX, squaresY),
(float)squareLength,
(float)markerLength,
dictionary);
参数含义与注意事项:
- 第一个参数
Size(squaresX, squaresY):X、Y 方向上的棋盘格方块数。注意这与标定中常用的"内角点数"概念不同,这里给的是完整方块数。 - 第二、三个参数(
squareLength/markerLength):方块边长与标记边长,二者必须使用同一单位(通常用米)。此单位将直接决定后续估计出的位姿平移量的量纲。 - 最后一个参数
dictionary:标记字典。标记会按字典顺序嵌入棋盘的白色方块中。 - id:默认按从 0 开始的升序自动分配,与
cv::aruco::GridBoard构造行为一致;若需自定义,可直接访问父类Board的board.ids向量修改。构造函数的ids形参默认为空(noArray()),传入自定义 id 数组亦可,详见 aruco_board.hpp。
将板渲染为可打印图片
拿到 CharucoBoard 对象后,有两种方式生成打印用的板图:
- 使用脚本
apps/pattern-tools/generate_pattern.py(同时可输出 DICT JSON 与 SVG,仓库apps/pattern-tools/目录下已预置各尺寸的 DICT_4X4/5X5/6X6/7X7、ARUCO_ORIGINAL、APRILTAG、MIP 等字典压缩包,适合大批量/离线生成); - 调用
cv::aruco::CharucoBoard::generateImage(),这是最直接的代码方式。
generateImage() 的调用方式同样取自 create_board_charuco.cpp:
Mat boardImage;
Size imageSize;
imageSize.width = squaresX * squareLength + 2 * margins; // 外边距左右各一份
imageSize.height = squaresY * squareLength + 2 * margins;
board.generateImage(imageSize, boardImage, margins, borderBits);
参数说明:
| 参数 | 含义 |
|---|---|
outSize(此处为 imageSize) |
输出图像尺寸(像素)。若不与板尺寸成比例,板会被居中绘制在图像中央。样本中把尺寸设为"板的总边长 + 2 倍边距",恰好严丝合缝 |
img |
输出的板图 Mat |
marginSize |
可选外边距(像素),保证任何标记都不贴到图像边缘;样本默认取 squareLength - markerLength |
borderBits |
标记黑色边框占的位(模块)数,语义与 cv::aruco::generateImageMarker() 一致,默认 1 |
生成出的板图大致如下(此处为仓库教程目录内的样例图):
命令行生成示例
create_board_charuco.cpp 的全部可选参数如下,程序会在未传参时打印用法:
const char* keys =
"{@outfile |res.png| Output image }"
"{w | 5 | Number of squares in X direction }"
"{h | 7 | Number of squares in Y direction }"
"{sl | 100 | Square side length (in pixels) }"
"{ml | 60 | Marker side length (in pixels) }"
"{d | | dictionary: ... }"
"{cd | | Input file with custom dictionary }"
"{m | | Margins size (in pixels). Default is (squareLength-markerLength) }"
"{bb | 1 | Number of bits in marker borders }"
"{si | false | show generated image }";
官方教程给出的调用示例为(w/h/sl/ml 此处以像素为单位,-d=10 对应 DICT_6X6_250):
_output_path_/chboard.png -w=5 -h=7 -sl=100 -ml=60 -d=10
-d 字典 id 与字典名的完整映射见样本源码注释(DICT_4X4_50=0 … DICT_ARUCO_MIP_36h12=21);若都不传 -d 且不传 -cd,aruco_samples_utility.hpp 中的辅助函数会默认选用 DICT_4X4_50。
ChArUco 板角点检测(Detection)
检测的本质与总体流程
检测 ChArUco 板,实际检测的是板上每一个棋盘格内角点(chessboard corners)。板上每个角点都有唯一 id,从 0 递增到板的总角点数减一。
官方教程把整条流水线拆成如下几个阶段,以下逐一展开:
① 读取输入图像。 detect_board_charuco.cpp 中从 VideoCapture 取帧。这张原始图像必须保留,因为后续要对 ChArUco 角点做亚像素细化,细化过程需要原始灰度纹理信息。
② 读取相机标定参数(仅在带标定的检测流程中需要)。 核心是 aruco_samples_utility.hpp 中的 readCameraParamsFromCommandLine → readCameraParameters:
bool readOk = readCameraParameters(parser.get<std::string>("c"), camMatrix, distCoeffs);
readCameraParameters 通过 cv::FileStorage 从 yml 中读取键 camera_matrix 与 distortion_coefficients,返回布尔值表示标定参数是否有效。若仅做无标定角点检测,这一步可以完全跳过。
③ 检测 ArUco 标记,并从标记插值出 ChArUco 角点。 ChArUco 角点的检测建立在已检出的 ArUco 标记之上:先检标记,再从标记反推棋盘角点。这一步由 cv::aruco::CharucoDetector::detectBoard() 完成,是整套流程的枢纽:
// 组装 detector:板 + ChArUco 参数 + ArUco 标记检测参数
aruco::CharucoBoard charucoBoard(Size(squaresX, squaresY), squareLength, markerLength, dictionary);
aruco::CharucoParameters charucoParams;
charucoParams.tryRefineMarkers = refine; // 为 true 时 detectBoard 内部会先做 refineDetectedMarkers()
charucoParams.cameraMatrix = camMatrix; // 可为 detectBoard() 提供相机内参
charucoParams.distCoeffs = distCoeffs; // 可为 detectBoard() 提供畸变系数
aruco::CharucoDetector charucoDetector(charucoBoard, charucoParams, detectorParams);
// 逐帧检测:同时输出 ChArUco 角点与中间层 ArUco 标记结果
charucoDetector.detectBoard(image, charucoCorners, charucoIds, markerCorners, markerIds);
注意 CharucoDetector 在构造函数阶段就一次性绑定板与全部参数(板、CharucoParameters、DetectorParameters,以及可选的 RefineParameters),见 charuco_detector.hpp;循环体内只调用 detectBoard(),因此多帧/视频场景下每帧的额外开销很小。
detectBoard() 的参数语义
依据 charuco_detector.hpp 的函数签名:
| 参数 | 类型 | 含义 |
|---|---|---|
image |
InputArray |
输入图像,用于亚像素细化(以及必要时内部触发 detectMarkers()) |
charucoCorners |
OutputArray |
检出的角点图像坐标列表 |
charucoIds |
OutputArray |
与 charucoCorners 一一对应的角点 id |
markerCorners |
InputOutputArrayOfArrays |
检测到的标记角点(输入/输出均可) |
markerIds |
InputOutputArray |
检测到的标记 id |
关键行为(header 注释与官方文档双重确认):
- 若传入的
markerCorners/markerIds为空,函数会先自动执行 ArUco 标记检测再插值; - 若提供了相机标定参数(
CharucoParameters中带cameraMatrix/distCoeffs),角点插值走"先由 ArUco 标记粗估计位姿、再把 ChArUco 角点反投影回图像"的路线; - 若未提供标定参数,插值改走"计算 ChArUco 平面与其图像投影之间的单应(homography)"的路线;
- 只返回"可见"的角点,即仅返回其周围标记确实被检测到的那些角点。
单应 vs 反投影:两种插值方式的取舍
两种插值策略在 charuco_detector.hpp 中被明确注释为:
If camera parameters are provided, the process is based in an approximated pose estimation, else it is based on local homography. Only visible corners are returned.
- 单应(homography)路线(无标定):无需内参即可工作,但对图像畸变更敏感。为抑制畸变影响,算法只使用每个 ChArUco 角点邻近的标记来计算局部单应,而非整板一张单应。
- 近似位姿 + 反投影路线(带标定):需要相机内参与畸变系数,但能抵消镜头畸变,精度更高,是标定/精密测量的首选。
影响插值质量的三个关键细节
(1)建议关闭 ArUco 标记的角点细化。 在 ChArUco 检测(尤其走单应路线)中,官方文档明确建议禁用标记的亚像素细化:因为棋盘方块彼此贴得很近,对标记做亚像素细化反而可能产生明显的角点偏移,且这种偏移会传播到 ChArUco 角点插值结果中,导致整体精度变差。示例程序中通过 -rs 开关控制 charucoParams.tryRefineMarkers,默认关闭。
(2)棋盘方块与标记之间要保持足够边距。 官方注释给出量化建议:方块边与标记之间的空白应大于一个标记模块(module)的 70%,否则插值易受干扰产生偏差。
(3)只信任"两个环绕标记都被检出"的角点。 每个 ChArUco 角点被若干方块围住。只有当其周围标记被全部/足够检测到时该角点才会被输出。若某个角点的环绕标记有缺失,通常意味着该区域存在遮挡或图像质量不佳——此时宁可丢弃该角点,也要保证输出的插值角点"高度可信"。这正是 CharucoParameters 中 minMarkers 字段(默认 2,表示至少需要检测到多少相邻标记才返回该角点)与 checkMarkers 字段(默认 true,校验标记确属同一板)的作用,见 charuco_detector.hpp。
角点插值完成后,算法还会自动执行一次亚像素细化以收敛到棋盘角点的最优位置。
绘制检测结果
插值完成后,用 cv::aruco::drawDetectedCornersCharuco() 即可把角点叠加画回图像上(默认颜色为 Scalar(255, 0, 0) 即蓝色,可自定义):
aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, cv::Scalar(255, 0, 0));
参数说明(与 charuco_detector.hpp 一致):
image:输入/输出图像(需为 1 或 3 通道),通常是检测用的原图副本;charucoCorners/charucoIds:detectBoard()的输出;cornerColor:可选角点/序号绘制颜色。
对下面这张原始输入图:
检测并绘制后的结果大致为:
而当场景存在遮挡时(仓库还提供了一张带遮挡的样张 chocclusion.jpg),尽管部分角点清晰可见,只要其环绕标记因遮挡未被检出,这些角点便不会被插值输出——这正是 ChArUco 插值"宁可少、不可错"策略的直观体现。教程还提供了完整示例视频 Nj44m_N_9FY 可对照观察。
检测程序的命令行用法
detect_board_charuco.cpp 除 -w/-h/-sl/-ml/-d/-c/-cd 外还支持以下开关:
"{c | | Output file with calibrated camera parameters }" // 严格说此处为输入标定文件
"{v | | Input from video or image file, if ommited, input comes from camera }"
"{ci | 0 | Camera id if input doesnt come from video (-v) }"
"{dp | | File of marker detector parameters }"
"{rs | | Apply refind strategy }"
官方教程给出的无标定角点检测示例(注意此时 sl/ml 以米为单位,配合示例图片):
-w=5 -h=7 -sl=0.04 -ml=0.02 -d=10 -v=doc/tutorials/objdetect/charuco_detection/images/choriginal.jpg
程序运行时会周期性打印每帧检测耗时(毫秒)与均值,便于在视频/实况场景中评估性能。仓库目录 doc/tutorials/objdetect/charuco_detection/images/ 中可找到 choriginal.jpg、chocclusion_original.jpg 等测试原图直接用于复现。
ChArUco 板的位姿估计(Pose Estimation)
坐标系约定
ChArUco 板做位姿估计的最终目标是高精度标定或位姿解算。与 cv::aruco::GridBoard 一致,CharucoBoard 的坐标系定义在板平面内,Z 轴指向板平面内侧(朝向纸张内部),原点位于板的左下角。
重要兼容性提示(OpenCV 4.6.0 起的破坏性变更): 4.6.0 之后板的坐标系发生了不兼容改动——Z 轴从"指向平面外侧"改为"指向平面内侧"。判断方法:objPoints 按**顺时针(CW)排列时对应 Z 轴向内,按逆时针(CCW)**排列时对应 Z 轴向外。因此,若你在 4.6.0 前后混用标定数据或手写对象点,务必核对绕序,否则位姿符号会翻转。
使用 matchImagePoints + solvePnP 估计位姿
位姿估计的推荐姿势是 cv::aruco::CharucoBoard::matchImagePoints()(自 Board 父类继承)配合 cv::solvePnP()。示例程序中的调用位于 detect_board_charuco.cpp:
bool validPose = false;
if (camMatrix.total() != 0 && distCoeffs.total() != 0 && charucoIds.size() >= 4) {
Mat objPoints, imgPoints;
charucoBoard.matchImagePoints(charucoCorners, charucoIds, objPoints, imgPoints);
validPose = solvePnP(objPoints, imgPoints, camMatrix, distCoeffs, rvec, tvec);
}
要点:
matchImagePoints()负责把"检出的 2D 角点 + id"换算成对应的 3D 对象点(objPoints)与 2D 图像点(imgPoints),是 ChArUco 位姿估计的核心桥梁,定义见 aruco_board.hpp;cameraMatrix、distCoeffs来自readCameraParameters读入的标定文件,位姿估计必须携带标定参数(无标定模式只能输出 2D 角点);rvec/tvec为输出的旋转向量与平移向量;- 示例程序还设了
charucoIds.size() >= 4的门槛:角点太少不足以稳定解算; cv::solvePnP()返回布尔值表示是否成功;失败的主因通常是角点数不足,或所有角点共线(几何退化,无法唯一确定姿态)。
绘制坐标轴验证结果
位姿是否正确,可调用 cv::drawFrameAxes() 把坐标轴画到图像上目视验证。示例中坐标轴长度取棋盘短边的一半(0.5 * min(squaresX, squaresY) * squareLength):
if (validPose)
cv::drawFrameAxes(imageCopy, camMatrix, distCoeffs, rvec, tvec, axisLength);
渲染规则为 X 轴红色、Y 轴绿色、Z 轴蓝色,效果如下:
带标定参数的完整命令行示例
仓库在 samples/cpp/tutorial_code/objectDetection/ 目录提供了配套的标定结果文件 tutorial_camera_charuco.yml(samples/data/aruco/ 下亦有一份),内含 camera_matrix 与 distortion_coefficients 节点,格式与 readCameraParameters 的读取键完全对应。官方示例的完整调用为:
-w=5 -h=7 -sl=0.04 -ml=0.02 -d=10 \
-v=doc/tutorials/objdetect/charuco_detection/images/choriginal.jpg \
-c=samples/cpp/tutorial_code/objectDetection/tutorial_camera_charuco.yml
与标定流程的衔接及延伸
本教程完整覆盖了 ChArUco 的"生成 → 检测 → 位姿"闭环,在仓库 samples/cpp/tutorial_code/objectDetection/ 中还提供了一脉相承的进阶示例:
- calibrate_camera_charuco.cpp:基于 ChArUco 板的多帧相机标定——正好把本文检测到的高精度角点批量喂给标定函数,是 ChArUco 最典型的落地场景;
- detect_board.cpp 与 create_board.cpp:标准 ArUco 板的对照实现(对应官方教程序列中的前一/后一篇,前者介绍 ArUco 板检测,后者介绍 ChArUco Diamond 检测,位于
doc/tutorials/objdetect/教程目录)。
一句话总结适用决策:追求亚像素级角点精度并愿意接受"只返回被充分环绕标记支撑的角点"这一约束时,选 ChArUco;反之若需要更多标记同时入镜、对绝对精度要求不高,普通 ArUco 板仍是更轻量的选择。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00




