首页
/ OpenCV ChArUco 标定板实战:从板图生成、角点检测到位姿估计的完整指南

OpenCV ChArUco 标定板实战:从板图生成、角点检测到位姿估计的完整指南

2026-09-07 13:58:09作者:何举烈Damon

ChArUco(Charuco)板把棋盘格的角点精度与 ArUco 标记的检测灵活性合二为一,是 OpenCV aruco 模块中面向高精度相机标定与位姿估计的核心标定物。本文以 charuco_detection.markdown 官方教程为主线,结合本仓库中 detect_board_charuco.cppcreate_board_charuco.cppcharuco_detector.hpp 源码,系统讲解 ChArUco 板的创建、角点插值检测原理、两种位姿估计调用链,并提供可直接运行的命令行示例与关键 API 说明。读完本文,你将能独立完成 ChArUco 板图生成、无标定/带标定两种模式下的角点检测,以及基于 solvePnP 的高精度位姿估计。

ChArUco 板概念图解:棋盘格(左)、ArUco 标记(中)与二者结合的 ChArUco 板(右)

为什么要用 ChArUco 板:两种传统方案的取舍

ArUco 标记与 ArUco 板检测快、容错强(允许部分遮挡、部分视野),但它的一个短板是:即使做了亚像素细化,角点位置的精度依然不够高

与之相对,棋盘格的每个角点都被两个黑色方块包围,角点定位可以被细化得非常精确;但棋盘格的检测非常"娇气"——必须完整可见,不允许任何遮挡,灵活性远不如 ArUco 板。

ChArUco 板的设计目标正是融合二者的优点:

  1. ArUco 部分负责插值棋盘格角点的位置,因此它继承了标记板的灵活性——允许遮挡与部分视野;
  2. 插值出的角点从属于棋盘格结构,因此天然具备很高的亚像素精度。

当精度成为首要诉求时(典型场景就是相机标定),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 构造行为一致;若需自定义,可直接访问父类 Boardboard.ids 向量修改。构造函数的 ids 形参默认为空(noArray()),传入自定义 id 数组亦可,详见 aruco_board.hpp

将板渲染为可打印图片

拿到 CharucoBoard 对象后,有两种方式生成打印用的板图:

  1. 使用脚本 apps/pattern-tools/generate_pattern.py(同时可输出 DICT JSON 与 SVG,仓库 apps/pattern-tools/ 目录下已预置各尺寸的 DICT_4X4/5X5/6X6/7X7、ARUCO_ORIGINAL、APRILTAG、MIP 等字典压缩包,适合大批量/离线生成);
  2. 调用 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

生成出的板图大致如下(此处为仓库教程目录内的样例图):

由 generateImage() 生成的可打印 ChArUco 板

命令行生成示例

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 且不传 -cdaruco_samples_utility.hpp 中的辅助函数会默认选用 DICT_4X4_50

ChArUco 板角点检测(Detection)

检测的本质与总体流程

检测 ChArUco 板,实际检测的是板上每一个棋盘格内角点(chessboard corners)。板上每个角点都有唯一 id,从 0 递增到板的总角点数减一。

官方教程把整条流水线拆成如下几个阶段,以下逐一展开:

① 读取输入图像。 detect_board_charuco.cpp 中从 VideoCapture 取帧。这张原始图像必须保留,因为后续要对 ChArUco 角点做亚像素细化,细化过程需要原始灰度纹理信息。

② 读取相机标定参数(仅在带标定的检测流程中需要)。 核心是 aruco_samples_utility.hpp 中的 readCameraParamsFromCommandLinereadCameraParameters

bool readOk = readCameraParameters(parser.get<std::string>("c"), camMatrix, distCoeffs);

readCameraParameters 通过 cv::FileStorage 从 yml 中读取键 camera_matrixdistortion_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 在构造函数阶段就一次性绑定板与全部参数(板、CharucoParametersDetectorParameters,以及可选的 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 角点被若干方块围住。只有当其周围标记被全部/足够检测到时该角点才会被输出。若某个角点的环绕标记有缺失,通常意味着该区域存在遮挡或图像质量不佳——此时宁可丢弃该角点,也要保证输出的插值角点"高度可信"。这正是 CharucoParametersminMarkers 字段(默认 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 / charucoIdsdetectBoard() 的输出;
  • cornerColor:可选角点/序号绘制颜色。

对下面这张原始输入图:

待检测的 ChArUco 板原始图像

检测并绘制后的结果大致为:

ChArUco 角点检测结果(含角点 id 标注)

而当场景存在遮挡时(仓库还提供了一张带遮挡的样张 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.jpgchocclusion_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
  • cameraMatrixdistCoeffs 来自 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 轴蓝色,效果如下:

带坐标轴渲染的 ChArUco 位姿估计结果(X 红、Y 绿、Z 蓝)

带标定参数的完整命令行示例

仓库在 samples/cpp/tutorial_code/objectDetection/ 目录提供了配套的标定结果文件 tutorial_camera_charuco.ymlsamples/data/aruco/ 下亦有一份),内含 camera_matrixdistortion_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.cppcreate_board.cpp:标准 ArUco 板的对照实现(对应官方教程序列中的前一/后一篇,前者介绍 ArUco 板检测,后者介绍 ChArUco Diamond 检测,位于 doc/tutorials/objdetect/ 教程目录)。

一句话总结适用决策:追求亚像素级角点精度并愿意接受"只返回被充分环绕标记支撑的角点"这一约束时,选 ChArUco;反之若需要更多标记同时入镜、对绝对精度要求不高,普通 ArUco 板仍是更轻量的选择。

登录后查看全文
热门项目推荐
相关项目推荐