opencv aruco calib
Detection of ArUco Markers
1、Aruco
ArUco 标记是一种合成方形标记,由宽黑边框和内部二进制矩阵组成,该矩阵确定其标识符 (id)。黑色边框有助于在图像中快速检测标记,二进制编码则有助于识别标记并应用错误检测和校正技术。标记大小决定了内部矩阵的大小。例如,4x4 大小的标记由 16 位组成。
需要注意的是,标记可能会在环境中发生旋转,但检测过程需要能够确定其原始旋转,以便明确识别每个角点。这也是基于二进制编码实现的。
- Aruco是一个开源的相机姿态估计库,已经嵌入到Opencv的Contribute包中。
- 简单来说,这是类似于二维码的Marker,长这样:
-
- 结构:
- 黑色的边界有利于快速检测图像,同时,黑色边界的旁边需要有白色的space(用于检测)
- 二进制编码可以验证ID,并且允许错误检测和纠正技术的应用。
- marker的大小决定了内部矩阵的大小。例如,一个4x4的marker由16bits組成
- 功能:
- 检测这些Marker
- 标定相机参数
- 相机位姿估计(Oroku用于计算相机相对于某个Marker的位姿,Haruko用于多相机的定位)
1.1 字典
markers的字典是在一个特殊应用中使用到的marker的集合
- 字典的大小就是组成字典所用到的Marker的数量
- marker的大小就是这些Marker的尺寸(bits)
2、Marker的生成
在检测标记之前,需要先打印标记以便将其放置在环境中。可以使用 generateImageMarker()
函数生成标记图像。
例如,让我们分析以下调用:
2.1 生成代码
Opencv 4.2
-
https://docs.opencv.org/4.2.0/d5/dae/tutorial_aruco_detection.html
-
#include <glog/logging.h> #include <gtest/gtest.h>#include <opencv2/opencv.hpp> #include <opencv2/aruco.hpp>TEST(TestGenerateAruco,generate_aruco_test) {cv::Mat markerImage;cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);cv::aruco::drawMarker(dictionary, 23, 200, markerImage, 1);cv::imshow("marker_23", markerImage);cv::waitKey(0); }
Opencv 4.13
-
cv::Mat markerImage; cv::aruco::Dictionary dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250); cv::aruco::generateImageMarker(dictionary, 23, 200, markerImage, 1); cv::imwrite("marker23.png", markerImage);
2.2 说明
首先,通过选择 aruco 模块中预定义的字典之一来创建 Dictionary
对象。具体来说,该字典由 250 个标记组成,标记大小为 6x6 位 ( DICT_6X6_250
)。
- 第一个参数是之前创建的
Dictionary
对象。 - 第二个参数是标记 ID,在本例中是字典
DICT_6X6_250
的标记 23。请注意,每个字典由不同数量的标记组成。在本例中,有效 ID 的范围是 0 到 249。任何超出有效范围的特定 ID 都将引发异常。 - 第三个参数 200 是输出标记图像的大小。在本例中,输出图像的大小为 200x200 像素。请注意,此参数应足够大,以存储特定字典的位数。因此,例如,您无法为 6x6 位的标记生成 5x5 像素的图像(这还没有考虑标记边框)。此外,为了避免变形,此参数应与位数 + 边框大小成比例,或者至少远高于标记大小(例如示例中的 200),以使变形不明显。
- 第四个参数是输出图像。
- 最后,最后一个参数是可选参数,用于指定标记黑色边框的宽度。该宽度与位数成比例。例如,值为 2 表示边框的宽度等于两个内部位的大小。默认值为 1。
3、Marker的检测
输入一张有Marker的图像,检测所有Marker,并返回Marker信息:
- Marker的四个角点
- Marker的ID
检测过程主要分为两步
- 检测标记候选。
- 在此步骤中,将分析图像以找到可作为标记候选的方形形状。首先,使用自适应阈值分割标记,然后从阈值图像中提取轮廓,并丢弃非凸形或不近似方形的轮廓。此外,还会进行一些额外的滤波处理(例如,移除过小或过大的轮廓,以及移除彼此过于接近的轮廓等)。
- 候选检测之后,需要通过分析其内部编码来确定它们是否确实是标记
- 此步骤首先提取每个标记的标记位。为此,首先应用透视变换以获得其规范形式的标记。
- 然后,使用大津法(Otsu)对规范图像进行阈值处理,以分离白色和黑色位。根据标记大小和边框大小,将图像划分为不同的单元。
- 然后,计算每个单元中黑色或白色像素的数量,以确定它是白色位还是黑色位。
- 最后,分析这些位以确定标记是否属于特定的字典。必要时,会采用纠错技术。
下面是需要检测Marker的图片
3.1 检测的函数
opencv 4.2
cv::Mat inputImage;
...
std::vector<int> markerIds;
std::vector<std::vector<cv::Point2f>> markerCorners, rejectedCandidates;
cv::Ptr<cv::aruco::DetectorParameters> parameters = cv::aruco::DetectorParameters::create();
cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);
cv::aruco::detectMarkers(inputImage, dictionary, markerCorners, markerIds, parameters, rejectedCandidates);
说明:
- 第一个参数是将要检测Marker的图像
- 第二个参数是字典对象,在本例中是预定义字典之一(
DICT_6X6_250
)。 - 检测到的标记存储在
markerCorners
和markerIds
结构中:markerCorners
是检测到的标记的角点列表。对于每个标记,其四个角点将按其原始顺序返回(从左上角开始顺时针方向)。因此,第一个角点是左上角,然后是右上角、右下角和左下角。markerIds
是markerCorners
中检测到的每个标记的 ID 列表。请注意,返回的markerCorners
和markerIds
向量的大小相同。
- 第四个参数是
DetectionParameters
类型的对象。 此对象包括可在检测过程中自定义的所有参数 - 最终参数
rejectedCandidates
是一个返回的Marker候选列表,即已找到的那些方格,但它们不提供有效的编码。 每个候选者也由其四个角定义,其格式与markerCorners
参数相同。 此参数可以省略
在 detectMarkers()
之后,你可能想做的下一件事是检查标记是否已被正确检测到。幸运的是,aruco 模块提供了一个函数 drawDetectedMarkers()
,可以在输入图像中绘制检测到的标记。例如:
3.2 检测代码及示意
#include <glog/logging.h>
#include <gtest/gtest.h>#include <opencv2/opencv.hpp>
#include <opencv2/aruco.hpp>std::string src_path = "aruco_2.png";TEST(TestAruco,detect_aruco_test) {cv::Mat img=cv::imread(src_path);cv::Mat img_detect = img.clone();cv::Mat img_rej_detect = img.clone();cv::Ptr<cv::aruco::Dictionary> dictionary =cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);cv::Ptr<cv::aruco::DetectorParameters> parameters = cv::aruco::DetectorParameters::create();std::vector<int> ids;std::vector<std::vector<cv::Point2f>> corners,rejectedCandidates;cv::aruco::detectMarkers(img, dictionary, corners, ids,parameters,rejectedCandidates);// cv::aruco::detectMarkers(img, dictionary, corners, ids);if (ids.size() > 0) {cv::aruco::drawDetectedMarkers(img_detect, corners, ids);cv::imshow("img_detect", img_detect);cv::imwrite("img_detect.png",img_detect);cv::waitKey(0);}if (rejectedCandidates.size() > 0) {cv::aruco::drawDetectedMarkers(img_rej_detect, rejectedCandidates);cv::imshow("img_rej_detect", img_rej_detect);cv::imwrite("img_rej_detect.png",img_rej_detect);cv::waitKey(0);}}
检测结果
这些是在识别步骤中被拒绝的标记候选(粉色):
4、位姿估计
检测到标记后,您可能想做的下一件事是从它们那里获取相机姿势。
为了进行摄像机的姿态估计,你需要知道你的摄像机的校准参数。
-
这些是相机矩阵和畸变系数。 如果您不知道如何校准您的相机,您可以查看
calibrateCamera ()
函数和 OpenCV 的校准教程。 -
使用 ArUco 和 ChArUco 进行校准教程。
-
请注意,除非相机光学系统发生改变(例如,改变焦距),否则校准操作只需执行一次。
最后,校准后您将获得相机矩阵:一个包含焦距和相机中心坐标(又称内在参数)的 3x3 元素矩阵,以及失真系数:一个包含 5 个或更多元素的向量,用于模拟相机产生的失真。
当你用 ArUco Marker估计姿势时,你可以单独估计每个Marker的姿势。 如果你想从一组Marker中估计一个姿势,使用 ArUco Boards (参见 ArUco Boards 的检测教程)。 使用 ArUco 板代替单个标记会导致某些标记被遮挡。
4.1 原理
相机相对于标记点的姿态是从标记点坐标系到相机坐标系的三维变换。它由旋转和平移向量指定(更多信息请参阅 solvePnP()
函数)。
4.2 估算Mark位姿
cv::Mat cameraMatrix, distCoeffs;
...
std::vector<cv::Vec3d> rvecs, tvecs;
cv::aruco::estimatePoseSingleMarkers(markerCorners, 0.05, cameraMatrix, distCoeffs, rvecs, tvecs);
说明:
- 第一个参数
markerCorners
参数是detectMarkers()
函数返回的 4个角点标记角向量。 - 第二个参数是标记边的尺寸,以米或其他单位表示。注意,估计姿态的平移向量将使用相同的单位。
- 第三个参数是相机的内参矩阵,
- 第四个参数是相机的畸变参数
rvecs
,tvecs
是输出的位姿估计(旋转量,平移量),需要注意的是,旋转量是轴角表示的旋转,需要使用罗德里格斯公式转换为旋转矩阵R
这个函数所假定的Marker坐标系原点是Marker的中心,z 轴指向外面,如下图所示。轴色对应为
- x: 红色
- y: 绿色
- z: 蓝色
-
模块提供了一个绘制轴的功能,如上图所示,因此可以检查位姿估计,可视化代码如下:
-
inputImage.copyTo(outputImage); for (int i = 0; i < rvecs.size(); ++i) {auto rvec = rvecs[i];auto tvec = tvecs[i];cv::aruco::drawAxis(outputImage, cameraMatrix, distCoeffs, rvec, tvec, 0.1);
drawAxis 参数说明
outputImage
是将绘制标记的输入/输出图像(通常与检测到标记的图像相同)。cameraMatrix
和distCoeffs
是相机校准参数。rvec
和tvec
是要绘制轴的标记的姿势参数。- 最后一个参数是轴的长度,单位与 tvec 相同(通常为米)。
4.3 代码
-
#include <iostream> #include <eigen3/Eigen/Core> #include <eigen3/Eigen/Dense>#include <opencv2/aruco/charuco.hpp> #include <opencv2/aruco.hpp> #include <opencv2/aruco/dictionary.hpp> #include <opencv2/core/core.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/imgproc/types_c.h> #include <opencv2/highgui/highgui.hpp> #include <opencv2/core/eigen.hpp> #include <opencv2/calib3d.hpp>using namespace std;int main() {// 内参不知道哪个老哥整出来的double fx, fy, cx, cy, k1, k2, k3, p1, p2;fx = 955.8925;fy = 955.4439;cx = 296.9006;cy = 215.9074;k1 = -0.1523;k2 = 0.7722;k3 = 0;p1 = 0;p2 = 0;// 内参矩阵cv::Mat cameraMatrix = (cv::Mat_<float>(3, 3) <<fx, 0.0, cx,0.0, fy, cy,0.0, 0.0, 1.0);// 畸变矩阵cv::Mat distCoeffs = (cv::Mat_<float>(5, 1) << k1, k2, p1, p2, k3);// 字典读取cv::Ptr<cv::aruco::Dictionary> dictionary =cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);cv::Mat image, imageCopy;image = cv::imread("../../../marker.jpg");image.copyTo(imageCopy);vector<int> ids;vector<vector<cv::Point2f>> corners;// 检测Markercv::aruco::detectMarkers(image, dictionary, corners, ids);if (ids.size() > 0) {// 绘制检测边框//cv::aruco::drawDetectedMarkers(imageCopy, corners, ids);// 估计相机位姿(相对于每一个marker)std::vector<cv::Vec3d> rvecs, tvecs;cv::aruco::estimatePoseSingleMarkers(corners, 0.055, cameraMatrix, distCoeffs, rvecs, tvecs);// draw axis for each markerfor (int i = 0; i < ids.size(); i++){if(ids[i]!=23)continue;/// 得到的位姿估计是:从Marker坐标系到相机坐标系的cv::Mat R;cv::Rodrigues(rvecs[i],R);cout << "ID :" << ids[i] << endl;cout << "R_{camera<---marker} :" << R << endl;cout << "t_{camera<---marker} :" << tvecs[i] << endl;cout << endl;cv::aruco::drawAxis(imageCopy, cameraMatrix, distCoeffs, rvecs[i], tvecs[i], 0.1);Eigen::Matrix3d R_eigen;cv::cv2eigen(R,R_eigen);Eigen::Vector3d zyx_Euler_fromR=R_eigen.eulerAngles(0,1,2);//Eigen中使用右乘的顺序,因此ZYX对应的是012,实际上这个编号跟乘法的顺序一致就可以了(从左向又右看的顺序)cout<< "zyx euler from Rotation \n[输出顺序为:x,y,z]:\n"<<(180)/(M_PI)*zyx_Euler_fromR.transpose()<<endl;}}cv::imshow("out", imageCopy);cv::waitKey(0);return 0; }
实际运行效果
5 Detector Parameters
detectMarkers()
函数的参数之一是 DetectorParameters
对象。该对象包含在标记检测过程中可以自定义的所有选项。
本节介绍每个检测器参数。这些参数可根据其所涉及的过程进行分类:
阈值
标记检测过程的第一步是对输入图像进行自适应阈值处理。
例如,上面使用的样本图像的阈值图像是:
可以使用以下参数自定义此阈值:
adaptiveThreshWinSizeMin, adaptiveThreshWinSizeMax, and adaptiveThreshWinSizeStep
adaptiveThreshWinSizeMin
和adaptiveThreshWinSizeMax
参数表示为自适应阈值选择阈值窗口大小(以像素为单位)的间隔(有关更多详细信息,请参阅 OpenCVthreshold()
函数)。- 参数
adaptiveThreshWinSizeStep
表示窗口大小从adaptiveThreshWinSizeMin
到adaptiveThreshWinSizeMax
的增量。
- 例如,对于值
adaptiveThreshWinSizeMin
= 5、adaptiveThreshWinSizeMax
= 21 和adaptiveThreshWinSizeStep
= 4,将有 5 个阈值步骤,窗口大小分别为 5、9、13、17 和 21。在每个阈值图像上,将提取标记候选。
注意:
-
如果标记尺寸太大,则窗口尺寸的低值可能会“破坏”标记边框,导致无法检测到,如下图所示:
-
-
另一方面,如果标记太小,过高的值也会产生同样的效果,并且也会降低性能。此外,该过程会趋向于全局阈值,从而失去自适应优势。
-
最简单的情况是将
adaptiveThreshWinSizeMin
和adaptiveThreshWinSizeMax
设置为相同的值,这样就只产生一个阈值步骤。然而,通常情况下,最好使用一个范围作为窗口大小的值,尽管过多的阈值步骤也会显著降低性能。
Default values: 默认值:
int adaptiveThreshWinSizeMin = 3
int adaptiveThreshWinSizeMax = 23
int adaptiveThreshWinSizeStep = 10
自适应阈值常数
adaptiveThreshConstant
参数表示阈值操作中添加的常量值(更多详情请参阅 OpenCVthreshold()
函数)。在大多数情况下,其默认值是一个不错的选择。
默认值:
double adaptiveThreshConstant = 7
轮廓过滤
- 阈值处理后,轮廓被检测到。然而,并非所有轮廓都会被视为标记候选。这些候选轮廓会通过不同的步骤进行过滤,以便丢弃那些不太可能是标记的轮廓。本节中的参数用于自定义此过滤过程。
- 必须注意的是,在大多数情况下,这是一个检测容量和性能之间的平衡问题。所有考虑的轮廓都将在接下来的阶段进行处理,这通常具有更高的计算成本。因此,最好在此阶段丢弃无效候选,而不是在后续阶段丢弃。
- 另一方面,如果过滤条件太严格,真实的标记轮廓可能会被丢弃,从而无法检测到。
minMarkerPerimeterRate 和 maxMarkerPerimeterRate
- 这些参数决定了标记的最小和最大尺寸,特别是标记周长的最小和最大。它们不是以绝对像素值指定的,而是相对于输入图像的最大尺寸指定的。
- 例如,如果图像尺寸为 640x480,且最小相对标记周长为 0.05,则最小标记周长为
maxMarkerPerimeterRate
= 32 像素,因为 640 是该图像的最大尺寸。maxMarkerPerimeterRate 参数也是如此。 - 如果
minMarkerPerimeterRate
值过低,则会显著降低检测性能,因为后续阶段需要考虑更多轮廓。- 这种影响对于
maxMarkerPerimeterRate
参数来说并不明显,因为通常小轮廓比大轮廓多得多。 - minMarkerPerimeterRate 值为 0 且
minMarkerPerimeterRate
值为 4(或更大)相当于考虑图像中的所有轮廓,但出于性能考虑maxMarkerPerimeterRate
不建议这样做。
- 这种影响对于
Default values: 默认值:
double minMarkerPerimeterRate = 0.03
double maxMarkerPerimeterRate = 4.0
多边形近似准确率
- 对每个候选区域应用多边形近似,只有近似于正方形的区域才会被接受。该值决定了多边形近似可能产生的最大误差(更多信息请参阅
approxPolyDP()
函数)。 - 此参数与候选区域的长度(以像素为单位)相关。因此,如果候选区域的周长为 100 像素,且
polygonalApproxAccuracyRate
的值为 0.04,则最大误差为 100x0.04=5.4 像素。 - 在大多数情况下,默认值就可以了,但对于高度扭曲的图像,可能需要更高的错误值。
Default value: 默认值:
double polygonalApproxAccuracyRate = 0.05
最小角距率
同一标记中任意一对角点之间的最小距离。该距离相对于标记周长。最小距离(以像素为单位)等于周长 * minCornerDistanceRate。
Default value: 默认值:
double minCornerDistanceRate = 0.05
最小标记距离率
两个不同标记的任意一对角点之间的最小距离。该距离以这两个标记的最小标记周长为基准。如果两个候选标记距离过近,则忽略较小的那个。
Default value: 默认值:
double minMarkerDistanceRate = 0.05
到边界的最小距离
- 任意标记角点到图像边界的最小距离(以像素为单位)。如果遮挡较小,则部分被图像边界遮挡的标记点可以被正确检测。但是,如果其中一个角点被遮挡,则返回的角点通常会放置在靠近图像边界的错误位置。
- 如果标记角的位置很重要,例如要进行姿态估计,最好丢弃角太靠近图像边界的标记。在其他情况下,则没有必要。
Default value: 默认值:
int minDistanceToBorder = 3
Bits Extraction 比特提取
候选检测之后,分析每个候选的位以确定它们是否是标记。
在分析二进制代码本身之前,需要提取位。为此,需要消除透视畸变,并使用 Otsu 阈值对生成的图像进行阈值处理,以分离黑色和白色像素。
这是消除标记的透视失真后获得的图像的示例:
然后,将图像划分为与标记位数相同的网格。在每个网格中,计算黑色和白色像素的数量,以确定分配给该网格的位值(根据多数值):
有几个参数可以定制这个过程:
markerBorderBits 标记边界位
- 此参数指示标记边框的宽度。它与每个位的大小有关。因此,值为 2 表示边框的宽度等于两个内部位的宽度。
- 此参数需要与您正在使用的标记的边框大小一致。边框大小可以在标记绘制函数(例如
drawMarker()
中配置。
Default value: 默认值:
int markerBorderBits = 1
minOtsuStdDev 最小Ots标准差
- 此值确定执行 Otsu 阈值处理的像素值的最小标准差。如果偏差较小,则可能意味着整个方块都是黑色(或白色),因此应用 Otsu 没有任何意义。在这种情况下,所有位都将设置为 0(或 1),具体取决于平均值是高于还是低于 128。
Default value: 默认值:
double minOtsuStdDev = 5.0
perspectiveRemovePixelPerCell
- 此参数决定了移除透视畸变(包括边框)后所得图像中每个单元格的像素数。这就是上图中红色方块的大小。
- 例如,假设我们处理的标记是 5x5 位,边框大小为 1 位(参见
markerBorderBits
)。那么,每个维度的单元格/位总数为 5 + 2*1 = 7(边框需要计算两次)。单元格总数为 7x7。 - 如果
perspectiveRemovePixelPerCell
的值为 10,那么获得的图像的大小将为 10*7 = 70 -> 70x70 像素。 - 该参数的较高值可以改善位提取过程(在一定程度上),但会降低性能。
Default value: 默认值:
int perspectiveRemovePixelPerCell = 4
perspectiveRemoveIgnoredMarginPerCell
- 在提取每个单元格的位时,会计算黑色和白色像素的数量。一般来说,不建议考虑所有单元格像素。最好忽略单元格边缘的一些像素。
- 原因在于,在消除透视畸变后,单元格的颜色通常无法完美分离,白色单元格可能会侵入黑色单元格的某些像素(反之亦然)。因此,最好忽略一些像素,以避免计算错误的像素。
例如,如下图所示:
仅考虑绿色方块内的像素。从右图可以看出,生成的像素包含的来自邻近单元的噪声较少。 perspectiveRemoveIgnoredMarginPerCell
参数表示红色方块和绿色方块之间的差异。
- 此参数与单元格的总大小相关。例如,如果单元格大小为 40 像素,且此参数的值为 0.1,则单元格中 40*0.1=4 像素的边距将被忽略。这意味着每个单元格中实际需要分析的像素总数为 32x32,而不是 40x40。
Default value: 默认值:
double perspectiveRemoveIgnoredMarginPerCell = 0.13
Marker identification 标记识别
- 提取位之后,下一步是检查提取的代码是否属于标记字典,如有必要,可以进行错误校正。
边界错误比特率
标记边框的位应为黑色。此参数指定边框中允许的错误位数,即边框中的最大白色位数。它相对于标记中的总位数表示。
Default value: 默认值:
double maxErroneousBitsInBorderRate = 0.35
errorCorrectionRate 误差校正率
- 每个标记字典都有一个理论上的最大可校正位数 (
Dictionary.maxCorrectionBits
)。但是,该值可以通过errorCorrectionRate
参数进行修改。 - 例如,如果允许纠正的位数(对于所使用的字典)为 6,并且
errorCorrectionRate
的值为 0.5,则实际可以纠正的最大位数为 6*0.5=3 位。 - 该值有助于降低纠错能力以避免误报。
Default value: 默认值:
double errorCorrectionRate = 0.6
Corner Refinement 角点细化
检测并识别标记后,最后一步是对角位置进行亚像素细化(参见 OpenCV cornerSubPix()
和 cv::aruco::CornerRefineMethod
)。
请注意,此步骤是可选的,仅在标记角位置必须精确的情况下才有意义,例如用于姿态估计。此步骤通常很耗时,因此默认情况下处于禁用状态。
cornerRefinementMethod 角点细化方法
- 此参数决定是否执行角点亚像素处理,以及如果执行则使用哪种方法。如果不需要精确的角点,可以禁用该参数。可能的值为
CORNER_REFINE_NONE
、CORNER_REFINE_SUBPIX
、CORNER_REFINE_CONTOUR
和CORNER_REFINE_APRILTAG
。
Default value: 默认值:
int cornerRefinementMethod = CORNER_REFINE_NONE
cornerRefinementWinSize
- 该参数决定了子像素细化过程的窗口大小。
- 较高的值可能会导致图像中较近的角点被包含在窗口区域内,从而导致标记角点在处理过程中移动到错误的位置。此外,它还会影响性能。
cornerRefinementMaxIterations and cornerRefinementMinAccuracy
- 这两个参数决定了亚像素细化过程的停止标准。cornerRefinementMaxIterations 表示最大迭代次数,
cornerRefinementMaxIterations
cornerRefinementMinAccuracy
停止过程前的最小误差值。 - 如果迭代次数过高,则会影响性能。另一方面,如果迭代次数过低,则会产生较差的亚像素细化效果。
Default values: 默认值:
int cornerRefinementMaxIterations = 30
double cornerRefinementMinAccuracy = 0.1
6 流程

Detection of ArUco Boards
1、Aruco Board
-
ArUco板是一组marker,在计算相机位姿时,如同对单个marker计算相机位姿一样,但是更加鲁棒。
-
最受欢迎的板是所有标记都在同一平面上的板,因为它可以轻松打印:
-
-
然而,棋盘并不局限于这种排列,可以表示任何二维或三维布局。
棋盘与一组独立标记点的区别在于,棋盘中标记点之间的相对位置是先验已知的。这使得所有标记点的角点都可以用来估计相机相对于整个棋盘的姿态。
使用ArUco板的好处是:
- 姿态估计的通用性更强。只需要一些标记即可进行姿态估计。因此,即使在存在遮挡或部分视图的情况下,也可以计算姿态。
- 由于采用了更多的点对应(标记角),因此获得的姿势通常更准确。
1.1 主要的类
aruco 模块允许使用 Boards。其主要类是 cv::aruco::Board
类,它定义了 Board 的布局:
class Board {
public:std::vector<std::vector<cv::Point3f> > objPoints;cv::Ptr<cv::aruco::Dictionary> dictionary;std::vector<int> ids;
};
- 第一个参数:
objPoints
结构体是 3d Board 参考系中角点位置的列表,即其布局。对于每个标记,其四个角点按标准顺序存储,即按顺时针方向,从左上角开始。 - 第二个参数: 指示棋盘Marker属于哪个marker字典
- 第三个参数:
ids
结构指示objPoints
中每个标记相对于指定dictionary
标识符。
2、Aruco Board 标定相机
2.1. 使用函数calibrateCameraAruco()
3、Aruco Board 的生成
创建 Board
对象需要指定环境中每个标记的角位置。然而,在很多情况下,棋盘只是一组位于同一平面且呈网格布局的标记,因此可以轻松打印和使用。
幸运的是,aruco 模块提供了轻松创建和打印这些类型标记的基本功能。
GridBoard
类是一个从 Board
类继承的专门类,它表示一个 Board,其中所有标记都位于同一平面并采用网格布局,如下图所示:
具体来说,网格板中的坐标系位于板平面中,以板的左下角为中心,Z 指向外,如下图所示(X:红色,Y:绿色,Z:蓝色):
GridBoard
GridBoard
类两个函数说明:
-
static Ptr<GridBoard> cv::aruco::GridBoard::create( int markersX, // X方向marker数量,即下图NumberX int markersY, // Y方向marker数量,即下图NumberY float markerLength, // marker长度,即下图MarkerLength float markerSeparation, // marker之间的间隔,即下图MarkerSeperation const Ptr< Dictionary > & dictionary, // 字典 int firstMarker = 0 //grid board上第一个marker的ID )
-
可以使用 cv::aruco::GridBoard::create()
静态函数根据这些参数轻松创建此对象:
-
cv::aruco::GridBoard board = cv::aruco::GridBoard::create(5, 7, 0.04, 0.01, dictionary);
-
第一个和第二个参数分别是 X 和 Y 方向的标记数量。
-
第三和第四个参数分别是标记长度和标记间距。它们可以采用任何单位,但请记住,该板的估计姿态将以相同的单位进行测量(通常使用米)。
-
最后,提供了标记的词典。
因此,该棋盘将由 5x7=35 个标记组成。每个标记的 ID 默认按从 0 开始的升序分配,因此它们将是 0、1、2、…、34。您可以通过 board.ids
访问 ids 向量轻松自定义,就像在 Board
父类中一样。
-
创建网格板后,我们可能希望将其打印出来并使用它。
cv::aruco::GridBoard::draw()
提供了一个生成GridBoard
图像的函数。例如: -
cv::Ptr<cv::aruco::GridBoard> board = cv::aruco::GridBoard::create(5, 7, 0.04, 0.01, dictionary); board->draw( cv::Size(600, 500), boardImage, 10, 1 );
-
第一个参数是输出图像的尺寸(以像素为单位)。本例中为 600x500 像素。如果该尺寸与棋盘尺寸不成比例,则图像将居中显示。
-
boardImage
:带有棋盘的输出图像。 -
第三个参数是可选的边距(以像素为单位),因此所有标记都不会接触图像边框。在本例中,边距为 10。
-
最后,标记边框的大小,类似于
drawMarker()
函数。默认值为 1。
实现
#include <glog/logging.h>
#include <gtest/gtest.h>#include <opencv2/opencv.hpp>
#include <opencv2/aruco.hpp>TEST(TestBoard,generate_test) {int m_x = 5; //X轴上标记的数量int m_y = 7; //Y轴上标记的数量 本例生成5x5的棋盘int m_len = 100; //标记的长度,单位是像素int m_sep = 20; //每个标记之间的间隔,单位像素int dict_id = cv::aruco::DICT_6X6_250;//生成标记的字典IDint margins = m_sep;//标记与边界之间的间隔int borderBits = 1; //标记的边界所占的bit位数int firstMarker= 0; //板的第一个Marker id (后面的直接按顺序生成)bool showImage = true;bool imwriteImage = true;// 计算输出图像大小cv::Size imageSize;imageSize.width = m_x * (m_len + m_sep) - m_sep + 2 * margins;imageSize.height = m_y * (m_len + m_sep) - m_sep + 2 * margins;// 定义字典const auto dict_name = cv::aruco::PREDEFINED_DICTIONARY_NAME(dict_id);cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(dict_name);cv::Ptr<cv::aruco::GridBoard> board = cv::aruco::GridBoard::create(m_x, m_y,float(m_len),float(m_sep),dictionary,firstMarker);// show created boardcv::Mat boardImage;board->draw(imageSize, boardImage, margins, borderBits);/// 是否显示if (showImage) {cv::imshow("board", boardImage);cv::waitKey(0);}// Writeif(imwriteImage) {cv::imwrite("board.png", boardImage);cv::waitKey(30);}
}
效果
5、Refine marker detection
ArUco 板也可以用来改进标记的检测。如果我们检测到了属于该板的标记子集,我们可以利用这些标记和板的布局信息来尝试找到之前未被检测到的标记。
这可以使用 refineDetectedMarkers()
函数来完成,该函数应在调用 detectMarkers()
之后调用。
该函数的主要参数是检测到标记的原始图像、Board 对象、检测到的标记角、检测到的标记 ID 和被拒绝的标记角。
4、Aruco Board进行相机位姿估计
在进行相机位姿估计之前,同样需要先进行marker的检测detectMarkers()
3.1 estimatePoseBoard()
函数
cv::aruco::estimatePoseBoard(markerCorners, markerIds, board, cameraMatrix, distCoeffs, rvec, tvec);
说明
- 第一、二个参数
markerCorners
,markerIds
: marker检测得到的角点和id - 第三个参数: 上面提到的
Board
类对象 - 第四、五个参数: 相机参数
cameraMatrix
和distCoeffs
- 最后两个参数: 平移和旋转(相对于整个Aruco板的),如果作为数据传入,数据不为空的时候,作为初始位姿估计,然后计算,再作为输出
3.2 视频流输入
/// Video 输入
cv::VideoCapture inputVideo;
inputVideo.open(0);// camera parameters are read from somewhere
cv::Mat cameraMatrix, distCoeffs;
readCameraParameters(cameraMatrix, distCoeffs);// create GridBoard
cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);
cv::Ptr<cv::aruco::GridBoard> board = cv::aruco::GridBoard::create(5, 7, 0.04, 0.01, dictionary);while (inputVideo.grab()) {// 转换成图片cv::Mat image, imageCopy;inputVideo.retrieve(image);image.copyTo(imageCopy);// 检测 角点 + IDstd::vector<int> ids;std::vector<std::vector<cv::Point2f> > corners;cv::aruco::detectMarkers(image, dictionary, corners, ids);// if at least one marker detectedif (ids.size() > 0) {// 绘制角点 + Idscv::aruco::drawDetectedMarkers(imageCopy, corners, ids);// 估算相机矩阵cv::Vec3d rvec, tvec;int valid = estimatePoseBoard(corners, ids, board, cameraMatrix, distCoeffs, rvec, tvec);// if at least one board marker detectedif(valid > 0)cv::aruco::drawAxis(imageCopy, cameraMatrix, distCoeffs, rvec, tvec,0.1);}cv::imshow("out", imageCopy);char key = (char) cv::waitKey(waitTime);if (key == 27)break;
}
3.2 代码
#include <glog/logging.h>
#include <gtest/gtest.h>#include <opencv2/opencv.hpp>
#include <opencv2/aruco.hpp>#include <Eigen/Eigen>
#include <opencv2/core/eigen.hpp>
std::string GetAructoPng() {std::string file_path = std::string(__FILE__);int index = file_path.find_last_of("/");file_path = file_path.substr(0,index);return file_path + "/aruco_101.png";
}TEST(TestBoard,estimator_test) {// 内参不知道哪个老哥整出来的double fx, fy, cx, cy, k1, k2, k3, p1, p2;fx = 955.8925; fy = 955.4439;cx = 296.9006; cy = 215.9074;k1 = -0.1523; k2 = 0.7722; k3 = 0;p1 = 0; p2 = 0;// 内参矩阵cv::Mat cameraMatrix = (cv::Mat_<float>(3, 3) <<fx, 0.0, cx,0.0, fy, cy,0.0, 0.0, 1.0);// 畸变矩阵cv::Mat distCoeffs = (cv::Mat_<float>(5, 1) << k1, k2, p1, p2, k3);// 字典读取cv::Ptr<cv::aruco::Dictionary> dictionary =cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);// 读取图片cv::Mat image, imageCopy;image = cv::imread(GetAructoPng());image.copyTo(imageCopy);// 下面这些参数需要用来计算相机位姿cv::Ptr<cv::aruco::GridBoard> board =cv::aruco::GridBoard::create(5, 7, 0.04, 0.01, dictionary);// 检测Markerstd::vector<int> ids;std::vector<std::vector<cv::Point2f> > corners;std::vector<std::vector<cv::Point2f> > corners_out;cv::aruco::detectMarkers(image, dictionary, corners, ids,cv::aruco::DetectorParameters::create(),corners_out);LOG(INFO)<<"ids: "<<ids.size();// 显示检测到的但是由于字典对不上被拒绝的Markerif (corners_out.size()>0){LOG(INFO)<<"一共有 "<<corners_out.size()<<" 个被拒绝的 Marker";for (int idx_rej=0;idx_rej<corners_out.size();idx_rej++) {for (int i=0;i<4;i++) {cv::circle(imageCopy,cv::Point(corners_out[idx_rej][i].x,corners_out[idx_rej][i].y),6,cv::Scalar(0,0,255));}}}// 显示正确的Markerif (ids.size() > 0) {// 绘制检测边框cv::aruco::drawDetectedMarkers(imageCopy, corners, ids);// 估计相机位姿(相对于 aruco 板)cv::Vec3d rvec, tvec;int valid = cv::aruco::estimatePoseBoard(corners, ids, board,cameraMatrix, distCoeffs, rvec, tvec);LOG(INFO)<<"valid: "<<valid;if(valid) {/// 得到的位姿估计是:从board坐标系到相机坐标系的cv::Mat R;cv::Rodrigues(rvec,R);LOG(INFO) << "R_{camera_marker} :" << R;LOG(INFO) << "t_{camera_marker} :" << tvec;cv::aruco::drawAxis(imageCopy, cameraMatrix, distCoeffs, rvec, tvec, 0.1);Eigen::Matrix3d R_eigen;cv::cv2eigen(R,R_eigen);Eigen::Vector3d zyx_Euler_fromR=R_eigen.eulerAngles(0,1,2);//Eigen中使用右乘的顺序,因此ZYX对应的是012,实际上这个编号跟乘法的顺序一致就可以了(从左向又右看的顺序)LOG(INFO)<< "zyx euler from Rotation \n[输出顺序为:x,y,z]:\n"<<(180)/(M_PI)*zyx_Euler_fromR.transpose();}}cv::imshow("out", imageCopy);cv::imwrite("out_ss.png", imageCopy);cv::waitKey(0);
}
这是官方自己给出的运行效果
5、优化标记检测
-
ArUco
板也可以用来改进标记的检测。如果我们检测到了属于该板的标记子集,我们可以利用这些标记和板的布局信息来尝试找到之前未被检测到的标记。 -
这可以使用
refineDetectedMarkers()
函数来完成,该函数应在调用detectMarkers()
之后调用。 -
该函数的主要参数是检测到标记的原始图像、Board 对象、检测到的标记角、检测到的标记 ID 和被拒绝的标记角。
-
被丢弃的角点可以通过
detectMarkers()
函数获取,它们也被称为标记候选。这些候选角点是在原始图像中发现的方形,但未能通过识别步骤(即其内部编码存在太多错误),因此未被识别为标记。 -
然而,这些候选标记有时是实际的标记,但由于图像噪声高、分辨率极低或其他影响二进制代码提取的相关问题,无法正确识别。refineDetectedMarkers
refineDetectedMarkers()
函数会查找这些候选标记与棋盘上缺失标记之间的对应关系。此搜索基于两个参数:- 候选标记与缺失标记投影之间的距离。要获得这些投影,必须至少检测到棋盘上的一个标记。如果提供了相机参数(相机矩阵和畸变系数),则使用它们获取投影。如果没有,则通过局部单应性矩阵获取投影,并且只允许使用平面棋盘(即所有标记角的 Z 坐标应相同)。refineDetectedMarkers
refineDetectedMarkers()
中的minRepDistance
参数确定候选角与投影标记角之间的最小欧氏距离(默认值为 10)。 - 二进制编码。如果候选标记超出最小距离条件,则再次分析其内部位,以确定其是否为实际投影标记。然而,在这种情况下,条件不那么严格,允许的错误位数可能会更高。这在
errorCorrectionRate
参数中指示(默认值为 3.0)。如果提供负值,则根本不分析内部位,仅评估角点距离。
- 候选标记与缺失标记投影之间的距离。要获得这些投影,必须至少检测到棋盘上的一个标记。如果提供了相机参数(相机矩阵和畸变系数),则使用它们获取投影。如果没有,则通过局部单应性矩阵获取投影,并且只允许使用平面棋盘(即所有标记角的 Z 坐标应相同)。refineDetectedMarkers
-
这是使用
refineDetectedMarkers()
函数的示例:-
cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250); cv::Ptr<cv::aruco::GridBoard> board = cv::aruco::GridBoard::create(5, 7, 0.04, 0.01, dictionary);std::vector<int> markerIds; std::vector<std::vector<cv::Point2f>> markerCorners, rejectedCandidates; cv::aruco::detectMarkers(inputImage, dictionary, markerCorners, markerIds, cv::aruco::DetectorParameters(), rejectedCandidates);cv::aruco::refineDetectedMarkersinputImage, board, markerCorners, markerIds, rejectedCandidates); // 调用此函数后,如果检测到任何新标记,它将从rejectedCandidates中删除,并包含在markerCorners和markerIds的末尾
-
还必须注意,在某些情况下,如果首先检测到的标记数量太少(例如只有 1 或 2 个标记),则缺失标记的投影质量可能很差,从而产生错误的对应关系。
请参阅模块示例以了解更详细的实现。
Detection of ChArUco Corners
ArUco 标记和板非常有用,因为它们具有快速检测和多功能性。然而,ArUco 标记的问题之一是,即使在应用亚像素优化之后,其角位置的精度也不是太高。
相反,棋盘图案的角可以更准确地细化,因为每个角都被两个黑色方块包围。然而,找到棋盘图案并不像找到 ArUco 棋盘那样通用:它必须完全可见并且不允许遮挡。
ChArUco 董事会试图结合这两种方法的优势:
ArUco 部分用于插入棋盘角的位置,使其具有标记板的多功能性,因为它允许遮挡或部分视图。此外,由于插值角属于棋盘,因此它们在亚像素精度方面非常准确。
当需要高精度时,例如在相机校准中,Charuco 板是比标准 Aruco 板更好的选择。
ChArUco Board Creation
-
aruco 模块提供了
cv::aruco::CharucoBoard
类,该类表示 Charuco Board,该类继承自Board
类。 -
该类与 ChArUco 的其余功能一样,定义于:
-
#include <opencv2/aruco/charuco.hpp>static Ptr<CharucoBoard> cv::aruco::CharucoBoard::create(int squaresX,int squaresY,float squareLength,float markerLength, const Ptr< Dictionary > & dictionary)
-
-
要定义
CharucoBoard
,必须:- X 方向的棋盘方格数。
- Y 方向的棋盘方格数量。
- 方边的长度。
- 标记侧的长度。
- 标记的字典。
- 所有标记的 ID。
-
对于
GridBoard
对象,aruco 模块提供了一个功能,可以轻松创建CharucoBoard
。这个函数是 static 函数cv::aruco::CharucoBoard::create()
:-
cv::aruco::CharucoBoard board = cv::aruco::CharucoBoard::create(5, 7, 0.04, 0.02, dictionary);
-
第一个和第二个参数分别是 X 和 Y 方向的正方形个数。
-
第三个和第四个参数分别是正方形和标记的长度。它们可以以任何单位提供,请记住,此板的估计姿势将以相同的单位测量(通常使用米)。
-
最后,提供了标记的字典。
-
-
默认情况下,每个标记的 id 都是按升序分配的,从 0 开始,就像在
GridBoard::create()
中一样。这可以通过board.ids
访问 ids 向量来轻松自定义,就像在Board
父类中一样。 -
一旦我们有了
CharucoBoard
对象,我们就可以创建一个图像来打印它。这可以通过CharucoBoard::d raw()
方法完成:-
#include <glog/logging.h> #include <gtest/gtest.h>#include <opencv2/opencv.hpp> #include <opencv2/aruco.hpp> #include <opencv2/aruco/charuco.hpp>TEST(TestChAruco,generate_aruco_test) {cv::Ptr<cv::aruco::Dictionary> dictionary =cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);cv::Ptr<cv::aruco::CharucoBoard> board =cv::aruco::CharucoBoard::create(5, 7, 0.04, 0.02, dictionary);cv::Mat boardImage;board->draw( cv::Size(600, 500), boardImage, 10, 1 );cv::imshow("boardImage.png",boardImage);cv::imwrite("boardImage.png",boardImage);cv::waitKey(0); }
-
第一个参数是输出图像的大小(以像素为单位)。在本例中为 600x500 像素。如果这与板尺寸不成比例,它将在图像上居中。
-
boardImage
:带有 board 的输出图像。 -
第三个参数是(可选)边距(以像素为单位),因此没有任何标记接触图像边框。在本例中,边距为 10。
-
最后,标记边框的大小,类似于
drawMarker()
函数。默认值为 1。
-
-
输出图像所示:
-
ChArUco Board Detection
当您检测到 ChArUco 棋盘时,您实际检测到的是棋盘的每个棋盘角。
ChArUco 板上的每个角都分配有一个唯一的标识符 (id)。这些 ID 从 0 到棋盘中的角总数。
因此,检测到的 ChArUco 板包括:
std::vector<cv::Point2f> charucoCorners
:检测到的角落的图像位置列表。std::vector<int> charucoIds
:charucoCorners
中检测到的每个角的 ID。
ChArUco 拐角的检测基于先前检测到的标记。这样,首先检测标记,然后从标记中插入 ChArUco 角。
检测 ChArUco 角的函数是 cv::aruco::interpolateCornersCharuco()
。此示例显示了整个过程。首先,检测标记,然后从这些标记中插入 ChArUco 角。
-
int cv::aruco::interpolateCornersCharuco(InputArrayOfArrays markerCorners,InputArray markerIds,InputArray image,const Ptr< CharucoBoard > & board,OutputArray charucoCorners,OutputArray charucoIds,InputArray cameraMatrix = noArray(),InputArray distCoeffs = noArray(),int minMarkers = 2 )
-
cameraMatrix: A=[fx0cx0fycy001]A=\begin{bmatrix} f_x&0&c_x\\0&f_y&c_y\\0&0&1 \end{bmatrix}A=fx000fy0cxcy1
-
distCoeffs: o可选的失真系数向量(k1、k2、p1、p2[、k3[、k4、k5、k6]、[s1、s2、s3、s4]]),包含 4、5、8 或 12 个元素
带相机外参
-
cv::Mat inputImage; cv::Mat cameraMatrix, distCoeffs; // camera parameters are read from somewhere readCameraParameters(cameraMatrix, distCoeffs); cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250); cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(5, 7, 0.04, 0.02, dictionary); ... std::vector<int> markerIds; std::vector<std::vector<cv::Point2f>> markerCorners; cv::aruco::detectMarkers(inputImage, board->dictionary, markerCorners, markerIds); // if at least one marker detected if(markerIds.size() > 0) {std::vector<cv::Point2f> charucoCorners;std::vector<int> charucoIds;cv::aruco::interpolateCornersCharuco(markerCorners, markerIds, inputImage, board, charucoCorners, charucoIds, cameraMatrix, distCoeffs); }
不带相机外参
在本例中,我们调用了 interpolateCornersCharuco()
来提供相机校准参数。但是,这些参数是可选的。没有这些参数的类似示例是:
-
cv::Mat inputImage; cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250); cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(5, 7, 0.04, 0.02, dictionary); ... std::vector<int> markerIds; std::vector<std::vector<cv::Point2f>> markerCorners; cv::Ptr<cv::aruco::DetectorParameters> params; params->cornerRefinementMethod = cv::aruco::CORNER_REFINE_NONE; cv::aruco::detectMarkers(inputImage, board->dictionary, markerCorners, markerIds, params); // if at least one marker detected if(markerIds.size() > 0) {std::vector<cv::Point2f> charucoCorners;std::vector<int> charucoIds;cv::aruco::interpolateCornersCharuco(markerCorners, markerIds, inputImage, board, charucoCorners, charucoIds); }
说明
如果提供了校准参数,则首先从 ArUco 标记估计粗略姿势,然后将 ChArUco 角重新投影回图像,从而对 ChArUco 角进行插值。
另一方面,如果未提供校准参数,则通过计算 ChArUco 平面和 ChArUco 图像投影之间的相应单应性来插值 ChArUco 角。
使用单向性的主要问题是插值对图像失真更敏感。实际上,单调仅使用每个 ChArUco 角最近的标记进行,以减少失真的影响。
当检测 ChArUco 板的标记时,特别是在使用单向性时,建议禁用标记的边角细化。这样做的原因是,由于棋盘方块很接近,亚像素过程可以在角位置产生重要的偏差,并且这些偏差会传播到 ChArUco 角插值,从而产生糟糕的结果。
此外,仅返回已找到两个周围标记的角。如果未检测到两个周围的标记中的任何一个,这通常意味着该区域存在一些遮挡或图像质量不佳。无论如何,最好不要考虑那个角,因为我们想要的是确保插值的 ChArUco 角非常准确。
对 ChArUco 角进行插值后,将执行子像素优化。
一旦我们插值了 ChArUco 角,我们可能想要绘制它们以查看它们的检测是否正确。这可以使用 drawDetectedCornersCharuco()
函数轻松完成:
image
是将绘制角的图像(它通常是检测到角的同一图像)。outputImage
将是inputImage
的克隆,并绘制了角。charucoCorners
和charucoIds
是从interpolateCornersCharuco()
函数中检测到的 Charuco 角。- 最后,最后一个参数是我们想要绘制角的(可选)颜色,
类型为 cv::Scalar
。
#include <glog/logging.h>
#include <gtest/gtest.h>#include <opencv2/opencv.hpp>
#include <opencv2/aruco.hpp>
#include <opencv2/aruco/charuco.hpp>TEST(TestChAruco,generate_aruco_test) {cv::Ptr<cv::aruco::Dictionary> dictionary =cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);cv::Ptr<cv::aruco::CharucoBoard> board =cv::aruco::CharucoBoard::create(5, 7, 0.04, 0.02, dictionary);cv::Mat boardImage;board->draw( cv::Size(600, 500), boardImage, 10, 1 );cv::imshow("boardImage.png",boardImage);cv::imwrite("boardImage.png",boardImage);cv::waitKey(0);
}TEST(TestChAruco,interpolateCorners_aruco_test) {cv::Mat inputImage;inputImage = cv::imread("aruco_202.png");ASSERT_FALSE(inputImage.empty());cv::Mat imageCopy = inputImage.clone();LOG(INFO)<<"read image ";double fx, fy, cx, cy, k1, k2, k3, p1, p2;fx = 955.8925; fy = 955.4439;cx = 296.9006; cy = 215.9074;k1 = -0.1523; k2 = 0.7722; k3 = 0;p1 = 0; p2 = 0;// 内参矩阵cv::Mat cameraMatrix = (cv::Mat_<float>(3, 3) <<fx, 0.0, cx,0.0, fy, cy,0.0, 0.0, 1.0);cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(5, 7, 0.04, 0.02, dictionary);std::vector<int> markerIds;std::vector<std::vector<cv::Point2f>> markerCorners;cv::Ptr<cv::aruco::DetectorParameters> params = cv::aruco::DetectorParameters::create();params->cornerRefinementMethod = cv::aruco::CORNER_REFINE_NONE;cv::aruco::detectMarkers(inputImage, board->dictionary, markerCorners, markerIds,params);LOG(INFO)<<"detectMarkers: "<<markerIds.size();// if at least one marker detectedif(markerIds.size() > 0) {std::vector<cv::Point2f> charucoCorners;std::vector<int> charucoIds;cv::aruco::interpolateCornersCharuco(markerCorners, markerIds, inputImage, board, charucoCorners, charucoIds);if(charucoIds.size() > 0)cv::aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, cv::Scalar(0,0,255));}cv::imshow("inputImage.png",imageCopy);cv::imwrite("inputImage.png",imageCopy);cv::waitKey(0);
}
显示:
官网遮挡
cv::VideoCapture inputVideo;
inputVideo.open(0);
cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);
cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(5, 7, 0.04, 0.02, dictionary);
cv::Ptr<cv::aruco::DetectorParameters> params;
params->cornerRefinementMethod = cv::aruco::CORNER_REFINE_NONE;
while (inputVideo.grab()) {cv::Mat image, imageCopy;inputVideo.retrieve(image);image.copyTo(imageCopy);std::vector<int> ids;std::vector<std::vector<cv::Point2f>> corners;cv::aruco::detectMarkers(image, dictionary, corners, ids, params);// if at least one marker detectedif (ids.size() > 0) {cv::aruco::drawDetectedMarkers(imageCopy, corners, ids);std::vector<cv::Point2f> charucoCorners;std::vector<int> charucoIds;cv::aruco::interpolateCornersCharuco(corners, ids, image, board, charucoCorners, charucoIds);// if at least one charuco corner detectedif(charucoIds.size() > 0)cv::aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, cv::Scalar(255, 0, 0));}cv::imshow("out", imageCopy);char key = (char) cv::waitKey(waitTime);if (key == 27)break;
}
ChArUco Pose Estimation
ChArUco 板的最终目标是非常准确地找到角落,以进行高精度校准或姿态估计。
aruco 模块提供了一个功能,可以轻松执行 ChArUco 姿态估计。与 GridBoard
一样,CharucoBoard
的坐标系放置在板平面中,Z 轴指向外,并以板的左下角为中心。
姿态估计的函数是 estimatePoseCharucoBoard():
-
bool cv::aruco::estimatePoseCharucoBoard(InputArray charucoCorners,InputArray charucoIds,const Ptr< CharucoBoard > & board,InputArray cameraMatrix,InputArray distCoeffs,InputOutputArray rvec,InputOutputArray tvec,bool useExtrinsicGuess = false)
-
charucoCorners
和charucoIds
参数是从interpolateCornersCharuco()
函数中检测到的 charuco 角。 -
第三个参数是
CharucoBoard
对象。 -
cameraMatrix
和distCoeffs
是姿势估计所需的相机校准参数。 -
最后,
rvec
和tvec
参数是 Charuco Board 的输出姿势。 -
如果姿势估计正确,则函数返回 true,否则返回 false。失败的主要原因是没有足够的角进行姿态估计,或者它们在同一行中。
可以使用 drawAxis()
绘制轴,以检查姿势是否正确估计。结果将是:(X:红色、Y:绿色、Z:蓝色)
使用姿态估计进行 ChArUco 检测的完整示例:
-
cv::VideoCapture inputVideo; inputVideo.open(0); cv::Mat cameraMatrix, distCoeffs; // camera parameters are read from somewhere readCameraParameters(cameraMatrix, distCoeffs); cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250); cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(5, 7, 0.04, 0.02, dictionary); while (inputVideo.grab()) {cv::Mat image, imageCopy;inputVideo.retrieve(image);image.copyTo(imageCopy);std::vector<int> ids;std::vector<std::vector<cv::Point2f>> corners;cv::aruco::detectMarkers(image, dictionary, corners, ids);// if at least one marker detectedif (ids.size() > 0) {std::vector<cv::Point2f> charucoCorners;std::vector<int> charucoIds;cv::aruco::interpolateCornersCharuco(corners, ids, image, board, charucoCorners, charucoIds, cameraMatrix, distCoeffs);// if at least one charuco corner detectedif(charucoIds.size() > 0) {cv::aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, cv::Scalar(255, 0, 0));cv::Vec3d rvec, tvec;bool valid = cv::aruco::estimatePoseCharucoBoard(charucoCorners, charucoIds, board, cameraMatrix, distCoeffs, rvec, tvec);// if charuco pose is validif(valid)cv::aruco::drawAxis(imageCopy, cameraMatrix, distCoeffs, rvec, tvec, 0.1);}}cv::imshow("out", imageCopy);char key = (char) cv::waitKey(waitTime);if (key == 27)break; }
Detection of Diamond Markers
ChArUco 钻石标记(或简称钻石标记)是由 3x3 个方格和白色方格内的 4 个 ArUco 标记组成的棋盘。它在外观上类似于 ChArUco 板,但它们在概念上有所不同。
在 ChArUco 板和 Diamond 标记物中,它们的检测基于先前检测到的 ArUco 标记物。
- 在 ChArUco 案例中,通过直接查看其标识符来选择使用的标记。这意味着,如果在图像上找到标记(包含在 Board 中),则会自动假定该标记属于该 Board。此外,如果在图像中多次找到标记板,则会产生歧义,因为系统无法知道应该为该板使用哪一个标记板。
- 另一方面,Diamond marker 的检测不是基于标识符。相反,它们的检测基于标记的相对位置。因此,标记标识符可以在同一颗钻石中或不同钻石之间重复,并且可以同时检测而不会产生歧义。但是,由于根据标记的相对位置查找标记的复杂性,菱形标记的大小限制为 3x3 个正方形和 4 个标记。
与单个 ArUco 标记一样,每个 Diamond 标记由 4 个角和一个标识符组成。这四个角对应着标记中的 4 个棋盘角,标识符实际上是一个由 4 个数字组成的数组,它们是方块内部的四个 ArUco 标记的标识符。
菱形标记在应允许重复标记的那些情况下非常有用。例如:
- 通过使用菱形标记进行标记来增加单个标记的标识符数量。它们最多允许 N^4 个不同的 id,即所用字典中的标记数 N。
- 为四个标记中的每一个赋予概念意义。例如,四个标记 ID 中的一个可用于指示标记的比例(即正方形的大小),这样只需更改四个标记中的一个,就可以在环境中找到不同大小的同一颗钻石,用户无需手动指示每个标记的比例。此大小写包含在模块的
samples
文件夹内的 diamond_detector.cpp 文件中。 - 此外,由于它的角是棋盘角,因此可用于准确的姿态估计。
- 菱形功能包含在
<opencv2/aruco/charuco.hpp>
中
ChArUco Diamond Creation
可以使用 drawCharucoDiamond()
函数轻松创建钻石标记的图像。例如:
-
cv::Mat diamondImage; cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250); cv::aruco::drawCharucoDiamond(dictionary, cv::Vec4i(45,68,28,74), 200, 120, markerImage);
-
这将创建一个方形大小为 200 像素且标记大小为 120 像素的菱形标记图像。标记 ID 在第二个参数中作为
Vec4i
对象给出。菱形布局中标记 ID 的顺序与标准 ChArUco 板中的顺序相同,即上、左、右和下。 -
完整的工作示例包含在 module samples 文件夹内的
create_diamond.cpp
中。
ChArUco Diamond Detection
与大多数情况一样,钻石标记物的检测需要事先检测 ArUco 标记物。检测标记后,使用 detectCharucoDiamond()
函数检测钻石:
-
cv::Mat inputImage; float squareLength = 0.40; float markerLength = 0.25; ... std::vector<int> markerIds; std::vector<std::vector< cv::Point2f>> markerCorners; // detect ArUco markers cv::aruco::detectMarkers(inputImage, dictionary, markerCorners, markerIds); std::vector<cv::Vec4i> diamondIds; std::vector<std::vector<cv::Point2f>> diamondCorners; // detect diamon diamonds cv::aruco::detectCharucoDiamond(inputImage, markerCorners, markerIds, squareLength / markerLength, diamondCorners, diamondIds);
detectCharucoDiamond()
函数接收原始图像以及之前检测到的标记角和 ID。在 ChArUco 角点执行子像素优化时,必须输入图像。它还接收两者所需的方形尺寸和标记大小之间的速率,从标记的相对位置检测菱形并插入 ChArUco 角。
该函数以两个参数返回检测到的钻石。
- 第一个参数
diamondCorners
是一个数组,其中包含每个检测到的钻石的所有四个角。其格式类似于detectMarkers()
函数检测到的角,对于每个菱形,角的表示顺序与 ArUco 标记相同,即从左上角开始顺时针顺序。 - 第二个返回的参数
diamondIds
包含diamondCorners
中返回的菱形角的所有 ID。每个 id 实际上是一个由 4 个整数组成的数组,可以用Vec4i
表示。
可以使用函数 drawDetectedDiamonds()
可视化检测到的钻石,该函数仅接收图像、菱形角和 ID:
-
... std::vector<cv::Vec4i> diamondIds; std::vector<std::vector<cv::Point2f>> diamondCorners; cv::aruco::detectCharucoDiamond(inputImage, markerCorners, markerIds, squareLength / markerLength, diamondCorners, diamondIds); cv::aruco::drawDetectedDiamonds(inputImage, diamondCorners, diamondIds);
-
ChArUco Diamond Pose Estimation
由于 ChArUco 钻石由其四个角表示,因此可以采用与单个 ArUco 标记相同的方式估计其位置,即使用 estimatePoseSingleMarkers()
函数。例如:
-
... std::vector<cv::Vec4i> diamondIds; std::vector<std::vector<cv::Point2f>> diamondCorners; // detect diamon diamonds cv::aruco::detectCharucoDiamond(inputImage, markerCorners, markerIds, squareLength / markerLength, diamondCorners, diamondIds); // estimate poses std::vector<cv::Vec3d> rvecs, tvecs; cv::aruco::estimatePoseSingleMarkers(diamondCorners, squareLength, camMatrix, distCoeffs, rvecs, tvecs); // draw axis for(unsigned int i=0; i<rvecs.size(); i++)cv::aruco::drawAxis(inputImage, camMatrix, distCoeffs, rvecs[i], tvecs[i], axisLength);
该函数将获取每个菱形标记的旋转和平移向量,并将它们存储在 rvec
和 tvec
中。请注意,菱形角是棋盘方角,因此,必须提供方格长度用于姿势估计,而不是标记长度。还需要相机校准参数。
Finally, an axis can be drawn to check the estimated pose is correct using drawAxis()
:
最后,可以使用 drawAxis()
绘制一个轴来检查估计的姿势是否正确:
Calibration with ArUco and ChArUco
ArUco 模块还可用于校准相机。相机校准包括获取相机固有参数和畸变系数。除非修改相机光学元件,否则此参数将保持固定,因此相机校准只需执行一次。
相机校准通常使用 OpenCV calibrateCamera()
函数执行。此功能要求环境点与其从不同视点在相机图像中的投影之间存在一些对应关系。通常,这些对应关系是从棋盘图案的角落获得的。有关更多详细信息,请参阅 calibrateCamera()
函数文档或 OpenCV 校准教程。
-
double cv::calibrateCamera ( InputArrayOfArrays objectPoints, InputArrayOfArrays imagePoints, Size imageSize, InputOutputArray cameraMatrix, InputOutputArray distCoeffs, OutputArrayOfArrays rvecs, OutputArrayOfArrays tvecs, OutputArray stdDeviationsIntrinsics, OutputArray stdDeviationsExtrinsics, OutputArray perViewErrors, int flags = 0, TermCriteria criteria = TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, DBL_EPSILON) )
使用 ArUco 模块,可以根据 ArUco 标记角或 ChArUco 角进行校准。使用 ArUco 进行校准比使用传统的棋盘模式要通用得多,因为它允许遮挡或部分视图。
可以说,可以使用标记角或 ChArUco 角进行校准。但是,强烈建议使用 ChArUco 角方法,因为与标记角相比,提供的角要准确得多。使用标准板进行校准时,应仅在由于任何限制而无法使用 ChArUco 板的情况下使用。
Calibration with ChArUco Boards
要使用 ChArUco 棋盘进行校准,必须从不同的视点检测棋盘,就像标准校准对传统棋盘模式所做的那样。但是,由于使用 ChArUco 的好处,允许遮挡和部分视图,并且并非所有角都需要在所有视点中都可见。
要校准的函数是 calibrateCameraCharuco()。
例:
-
cv::Ptr<aruco::CharucoBoard> board = ... // create charuco board cv::Size imgSize = ... // camera image size std::vector<std::vector<cv::Point2f>> allCharucoCorners; std::vector<std::vector<int>> allCharucoIds; // Detect charuco board from several viewpoints and fill allCharucoCorners and allCharucoIds ... ... // After capturing in several viewpoints, start calibration cv::Mat cameraMatrix, distCoeffs; std::vector<cv::Mat> rvecs, tvecs; int calibrationFlags = ... // Set calibration flags (same than in calibrateCamera() function) double repError = cv::aruco::calibrateCameraCharuco(allCharucoCorners, allCharucoIds, board, imgSize, cameraMatrix, distCoeffs, rvecs, tvecs, calibrationFlags);
-
在每个视点上捕获的 ChArUco 角和 ChArUco 标识符存储在向量
allCharucoCorners
和allCharucoIds
中,每个视点一个元素。 -
calibrateCameraCharuco()
函数将使用相机校准参数填充cameraMatrix
和distCoeffs
数组。它将返回从校准中获得的重投影误差。rvec
和tvec
中的元素将填充每个视点中摄像机的估计姿势(相对于 ChArUco 板)。 -
最后,
calibrationFlags
参数确定一些校准选项。其格式等效于 OpenCVcalibrateCamera() 函数中的 flags
参数。 -
完整的工作示例包含在 module samples 文件夹内的
calibrate_camera_charuco.cpp
中。
Calibration with ArUco Boards
如前所述,建议使用 ChAruco 板而不是 ArUco 板进行相机校准,因为 ChArUco 角比标记角更准确。但是,在某些特殊情况下,必须要求使用基于 ArUco 板的校准。对于这些情况,提供了 calibrateCameraAruco()
函数。与前一种情况一样,它需要从不同的角度检测 ArUco 板。
calibrateCameraAruco()
使用示例:
-
cv::Ptr<aruco::Board> board = ... // create aruco board cv::Size imgSize = ... // camera image size std::vector<std::vector<cv::Point2f>> allCornersConcatenated; std::vector<int> allIdsConcatenated; std::vector<int> markerCounterPerFrame; // Detect aruco board from several viewpoints and fill allCornersConcatenated, allIdsConcatenated and markerCounterPerFrame ... ... // After capturing in several viewpoints, start calibration cv::Mat cameraMatrix, distCoeffs; std::vector<cv::Mat> rvecs, tvecs; int calibrationFlags = ... // Set calibration flags (same than in calibrateCamera() function) double repError = cv::aruco::calibrateCameraAruco(allCornersConcatenated, allIdsConcatenated, markerCounterPerFrame, board, imgSize, cameraMatrix, distCoeffs, rvecs, tvecs, calibrationFlags);
-
在这种情况下,与
calibrateCameraCharuco()
函数相反,在每个视点上检测到的标记将连接在数组allCornersConcatenated
和allCornersConcatenated
(前两个参数)中。第三个参数(数组markerCounterPerFrame
)指示在每个视点上检测到的标记数。其余参数与calibrateCameraCharuco()
中的参数相同,除了板布局对象不需要是CharucoBoard
对象,它可以是任何Board
对象。 -
完整的工作示例包含在 module samples 文件夹内的
calibrate_camera.cpp
中。