OpenCV  4.10.0
开放源代码计算机视觉
正在加载……
正在搜索……
没有匹配项
ArUco 标记物的检测

下一篇教程: ArUco 棋盘的检测

作者Sergio Garrido、Alexander Panov
兼容性OpenCV >= 4.7.0

位姿估计在许多计算机视觉应用中至关重要:机器人导航、增强现实等等。此过程基于环境中点的对应点与其 2D 图像投影之间的对应关系。这通常是一个困难的步骤,因此通常使用合成或标记物标记以便于进行此步骤。

最流行的方法之一是使用二进制正方形标记物标记。这些标记物的主要好处是,一个标记物就提供足够的对应关系(其四个角)以获得相机位姿。此外,内部二进制编码使它们特别稳健,从而可以使用错误检测和更正技术。

aruco 模块基于 ArUco 库,这是一个由 Rafael Muñoz 和 Sergio Garrido 开发的用于检测正方形标记物标记的流行库 [99]

aruco 功能包括在

标记物和字典

ArUco 标记物是一个合成正方形标记物,由宽黑色边框和一个内部二进制矩阵组成,该矩阵确定其标识符 (id)。黑色边框便于在其图像中快速检测,二进制编码允许将其识别并使用错误检测和更正技术。标记物大小决定内部矩阵的大小。例如,4x4 的标记物大小由 16 位组成。

一些 ArUco 标记物示例

标记物图像示例

必须注意,可以在环境中找到旋转的标记物,但是检测过程需要能够确定其原始旋转,以便明确地识别每个角。这也是基于二进制编码来完成的。

一个标记物字典是一组在一个特定应用中要考虑的标记物。它只是每个标记物的二进制编码的列表。

字典的主要属性是字典大小和标记物大小。

  • 字典大小是组成字典的标记物数量。
  • 标记物大小是这些标记物的大小(位/模块数量)。

aruco 模块包括一些预定义的字典,涵盖各种不同的字典大小和标记物大小。

人们可能会认为标记 id 是将二进制编码转换为十进制的数字。然而,这并不可能,因为对于高标记大小,比特数太高,管理如此大的数字不切实际。取而代之的是,标记 id 只是它所属字典中标记的索引。例如,字典中的前 5 个标记具有 id:0、1、2、3 和 4。

有关字典的详细信息,请参见“字典选取”部分。

标记创建

在检测到标记之前,需要打印标记以便将其放置在环境中。可以使用 generateImageMarker() 函数生成标记图像。

例如,分析以下调用

cv::Mat markerImage;
cv::aruco::generateImageMarker(dictionary, 23, 200, markerImage, 1);
cv::imwrite("marker23.png", markerImage);
n 维密集阵列类
定义 mat.hpp:812
字典是一组大小相同的独特 ArUco 标记。
定义 aruco_dictionary.hpp:29
CV_EXPORTS_W bool imwrite(const String &filename, InputArray img, const std::vector< int > &params=std::vector< int >())
将图像保存到指定的文件中。
void generateImageMarker(const Dictionary &dictionary, int id, int sidePixels, OutputArray img, int borderBits=1)
生成规范标记图像。
Dictionary getPredefinedDictionary(PredefinedDictionaryType name)
返回 PredefinedDictionaryType 中定义的一个预定义字典。
@ DICT_6X6_250
6x6 比特,任意两个码之间的最小汉明距离 = 11,250 个码
定义 aruco_dictionary.hpp:110

首先,通过选择 aruco 模块中预定义的字典之一来创建 cv::aruco::Dictionary 对象。具体来说,该字典由 250 个标记和 6x6 比特标记大小组成(cv::aruco::DICT_6X6_250)。

cv::aruco::generateImageMarker() 的参数为

  • 第一个参数是之前创建的 cv::aruco::Dictionary 对象。
  • 第二个参数是标记 ID,这里是词典 cv::aruco::DICT_6X6_250 中的标记 23。请注意,每个词典都由不同数量的标记组成。对于此例,有效的 ID 范围为 0 至 249。任何超出有效范围的 ID 都会生成异常。
  • 第三个参数为 200,即输出标记图像大小。对于此例,输出图像大小为 200x200 像素。请注意,此参数应足够大才能存储特定词典的位数。因此,例如,您不能为 6x6 位的标记大小生成 5x5 像素的图像(而且这是不考虑标记边框的情况)。此外,为避免变形,此参数应与位数 + 边框大小成比例,或至少远大于标记大小(例如示例中的 200),从而使变形显得微不足道。
  • 第四个参数是输出图像。
  • 最后,最后一个参数是一个可选参数,用于指定标记黑色边框的宽度。大小根据位数成比例指定。例如,值 2 表示边框宽度将相当于两个内部位的大小。默认值为 1。

生成的图像为

生成的标记

完整的工作示例包含在 samples/cpp/tutorial_code/objectDetection/ 中的 create_marker.cpp 内。

现在,示例通过使用 cv::CommandLineParser 从命令行获取输入。对于此文件,示例参数将类似于

"marker23.png" -d=10 -id=23

create_marker.cpp 参数

const char* keys =
"{@outfile |res.png| 输出图像 }"
"{d | 0 | 词典:DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2,"
"DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, "
"DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12,"
"DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}"
"{cd | | 带有自定义词典的输入文件 }"
"{id | 0 | 词典中的标记 ID }"
"{ms | 200 | 标记大小(像素) }"
"{bb | 1 | 标记边框中的位数 }"
"{si | false | 显示生成的图像 }";
}

标记检测

给定包含 ArUco 标记的图像,检测过程必须返回列表 [已检测标记]。每个检测标记包括

  • 其四个角在图像中的位置(按其原始顺序)。
  • 标记 ID。

标记检测过程包含两个主要步骤

  1. 检测标记候选者。此步骤中将分析图像,以查找可能是标记的方形。它从自适应阈值开始,分割标记,然后从阈值图像中提取轮廓,并丢弃那些不凸或不近似于方形的轮廓。同时还应用一些额外的过滤(移除过小或过大的轮廓,移除彼此太近的轮廓等)。
  2. 候选者检测之后,通过分析标记内部编码,有必要确定标记是否实际存在。此步骤首先从每个标记中提取标记位。要做到这一点,首先应用透视变换以获得标记规范形式。然后,使用大津阈值化规范图像,以分离白色和黑色位。根据标记大小和边框大小,将图像分成不同的单元格。然后计数每个单元格中黑色或白色像素的数量,确定是白色位还是黑色位。最后,分析这些位,以确定标记是否属于特定字典。必要时采用纠错技术。

考虑以下图像

标记组合图像

图像打印件照片中的标记

带标记的原始图像

以下检测出标记(以绿色显示)。请注意,一些标记已旋转。小红方框表示标记的左上角

带检测标记的图像

以下识别步骤中已拒绝的标记候选者(以粉色显示)

带已拒绝候选者的图像

在 aruco 模块中,会在 cv::aruco::ArucoDetector::detectMarkers() 函数中执行检测。此函数是该模块中最重要的函数,因为其他所有功能都基于由 cv::aruco::ArucoDetector::detectMarkers() 返回的检测标记。

标记检测示例

cv::Mat inputImage;
// ... 读取 inputImage ...
std::vector<int> markerIds;
std::vector<std::vector<cv::Point2f>> markerCorners, rejectedCandidates;
cv::aruco::ArucoDetector detector(dictionary, detectorParams);
detector.detectMarkers(inputImage, markerCorners, markerIds, rejectedCandidates);
ArucoDetector 类的主要功能是通过 detectMarkers(...
定义 aruco_detector.hpp:276
ArucoDetector 使用 DetectorParameters 结构体
定义 aruco_detector.hpp:25

当创建cv::aruco::ArucoDetector对象时,需要向构造函数传递以下参数

cv::aruco::ArucoDetector::detectMarkers() 的参数为

  • 第一个参数是包含要检测的标记的图像。
  • 检测到的标记存储在 markerCornersmarkerIds 结构中
    • markerCorners 是检测到的标记的角点列表。对于每个标记,其四个角点按其原始顺序返回(从左上角开始顺时针)。因此,第一个角点是左上角,然后是右上角、右下角和左下角。
    • markerIdsmarkerCorners 中每个检测到的标记的 ID 列表。请注意,返回的 markerCornersmarkerIds 向量具有相同的大小。
  • 最终的可选参数 rejectedCandidates 是返回的标记候选列表,即已找到并考虑但未包含有效标记的形状。每个候选也由其四个角点定义,其格式与 markerCorners 参数相同。可以省略此参数,它仅在出于调试目的和用于“优化”策略(参见 cv::aruco::ArucoDetector::refineDetectedMarkers())时才有用。

执行 cv::aruco::ArucoDetector::detectMarkers() 之后,您可能想要做的下一件事是检查标记是否已正确检测。幸运的是,aruco 模块提供了一个在输入图像中绘制检测到的标记的函数,此函数为 drawDetectedMarkers()。例如

cv::Mat outputImage = inputImage.clone();
cv::aruco::drawDetectedMarkers(outputImage, markerCorners, markerIds);
CV_NODISCARD_STD Mat clone() const
创建整个数据的完整副本,包括数组和底层数据。
void drawDetectedMarkers(InputOutputArray image, InputArrayOfArrays corners, InputArray ids=noArray(), Scalar borderColor=Scalar(0, 255, 0))
在图像中绘制检测到的标记。
  • outputImage 是输入/输出图像,其中绘制标记(通常会与检测标记的图像相同)。
  • markerCornersmarkerIdscv::aruco::ArucoDetector::detectMarkers() 函数返回的已检测到标记的结构。
带检测标记的图像

注意这个函数只提供用于可视化,可以省略其使用。

使用这两个函数,我们可以创建一个基本的标记检测循环,用来从相机检测标记

cv::aruco::ArucoDetector detector(dictionary, detectorParams);
cv::VideoCapture inputVideo;
int waitTime;
if(!video.empty()) {
inputVideo.open(video);
waitTime = 0;
} else {
inputVideo.open(camId);
waitTime = 10;
}
double totalTime = 0;
int totalIterations = 0;
// 设置坐标系
cv::Mat objPoints(4, 1, CV_32FC3);
objPoints.ptr<Vec3f>(0)[0] = Vec3f(-markerLength/2.f, markerLength/2.f, 0);
objPoints.ptr<Vec3f>(0)[1] = Vec3f(markerLength/2.f, markerLength/2.f, 0);
objPoints.ptr<Vec3f>(0)[2] = Vec3f(markerLength/2.f, -markerLength/2.f, 0);
objPoints.ptr<Vec3f>(0)[3] = Vec3f(-markerLength/2.f, -markerLength/2.f, 0);
while(inputVideo.grab()) {
cv::Mat image, imageCopy;
inputVideo.retrieve(image);
double tick = (double)getTickCount();
vector<int> ids;
vector<vector<Point2f> > corners, rejected;
// 检测标记和估计姿态
detector.detectMarkers(image, corners, ids, rejected);
size_t nMarkers = corners.size();
vector<Vec3d> rvecs(nMarkers), tvecs(nMarkers);
if(estimatePose && !ids.empty()) {
// 计算每个标记的姿态
for (size_t i = 0; i < nMarkers; i++) {
solvePnP(objPoints, corners.at(i), camMatrix, distCoeffs, rvecs.at(i), tvecs.at(i));
}
}
double currentTime = ((double)getTickCount() - tick) / getTickFrequency();
totalTime += currentTime;
totalIterations++;
if(totalIterations % 30 == 0) {
cout << "Detection Time = " << currentTime * 1000 << " ms "
<< "(Mean = " << 1000 * totalTime / double(totalIterations) << " ms)" << endl;
}
// 绘制结果
image.copyTo(imageCopy);
if(!ids.empty()) {
cv::aruco::drawDetectedMarkers(imageCopy, corners, ids);
if(estimatePose) {
for(unsigned int i = 0; i < ids.size(); i++)
cv::drawFrameAxes(imageCopy, camMatrix, distCoeffs, rvecs[i], tvecs[i], markerLength * 1.5f, 2);
}
}
if(showRejected && !rejected.empty())
cv::aruco::drawDetectedMarkers(imageCopy, rejected, noArray(), Scalar(100, 0, 255));
imshow("out", imageCopy);
char key = (char)waitKey(waitTime);
if(key == 27) break;
}
void copyTo(OutputArray m) const
将矩阵复制到另一个矩阵中。
从视频文件、图像序列或摄像头进行视频捕捉的类。
Definition videoio.hpp:731
virtual bool open(const String &filename, int apiPreference=CAP_ANY)
打开视频文件、捕获设备或 IP 视频流以进行视频捕获。
virtual bool retrieve(OutputArray image, int flag=0)
解码并返回抓取的视频帧。
virtual bool grab()
从视频文件或捕捉设备抓取下一帧。
void drawFrameAxes(InputOutputArray image, InputArray cameraMatrix, InputArray distCoeffs, InputArray rvec, InputArray tvec, float length, int thickness=3)
根据姿态估计绘制世界/物体坐标系的坐标轴。
Scalar_< double > Scalar
Definition types.hpp:702
InputOutputArray noArray()
#define CV_32FC3
Definition interface.h:120
double getTickFrequency()
返回每秒的滴答数。
int64 getTickCount()
返回值的 tick 数。
void imshow(const String &winname, InputArray mat)
在指定窗口中显示图像。
int waitKey(int delay=0)
等待按下按键。

注意,已省略某些可选参数,如检测参数对象和拒绝候选的输出向量。

detect_markers.cpp 中包含完整的操作示例,位于 samples/cpp/tutorial_code/objectDetection/ 中。

现在,示例通过使用 cv::CommandLineParser 从命令行获取输入。对于此文件,示例参数将类似于

-v=/path_to_opencv/opencv/doc/tutorials/objdetect/aruco_detection/images/singlemarkersoriginal.jpg -d=10

detect_markers.cpp 的参数

const char* keys =
"{d | 0 | 词典:DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2,"
"DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, "
"DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12,"
"DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16,"
"DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20}"
"{cd | | 带有自定义词典的输入文件 }"
"{v | | 视频或图像文件的输入,如果省略,则输入来自摄像机 }"
"{ci | 0 | 如果输入不来自视频(-v),则为摄像头 ID }"
"{c | | 摄像机内参。对于摄像机位姿是必需的 }"
"{l | 0.1 | 标记边长(以米为单位)。对于摄像机位姿中的正确比例是必需的 }"
"{dp | | 标记检测参数文件 }"
"{r | | 也显示拒绝候选 }"
"{refine | | 角点优化:CORNER_REFINE_NONE=0, CORNER_REFINE_SUBPIX=1,"
"CORNER_REFINE_CONTOUR=2, CORNER_REFINE_APRILTAG=3}";

位姿估计

在检测到标记后,你可能会想做的下一步是使用它们来获取摄像机位姿。

若要执行摄像机位姿估计,你需要知道摄像机的校准参数。这些是相机矩阵和畸变系数。如果你不知道如何校准你的相机,你可以查看 OpenCV 的 calibrateCamera() 函数和校准教程。你还可以按照通过 ArUco 和 ChArUco 校准教程中的说明,使用 aruco 模块来校准你的相机。请注意,除非相机光学元件发生更改(例如改变其焦点),否则只需执行此操作一次。

经过校准后,会得到一个相机矩阵:一个包含焦距和相机中心坐标(又称内参)的 3x3 元素矩阵,以及畸变系数:一个由 5 个或更多元素组成的向量,用于对你的相机产生的畸变进行建模。

当使用 ArUco 标记估算位姿时,你可以估算每个标记的位姿。如果想要从一组标记中估算一个位姿,请使用 ArUco 板(参见检测 ArUco 板教程)。使用 ArUco 板而不是单个标记允许遮挡某些标记。

相对于标记的摄像机位姿是从标记坐标系到摄像机坐标系的一个 3D 变换。它由旋转向量和平移向量指定。OpenCV 提供cv::solvePnP()函数来执行此操作。

Mat camMatrix, distCoeffs;
if(estimatePose) {
// 您可以从教程 camera_params.yml 中读取摄像机参数
readCameraParamsFromCommandLine(parser, camMatrix, distCoeffs);
}
// 设置坐标系
cv::Mat objPoints(4, 1, CV_32FC3);
objPoints.ptr<Vec3f>(0)[0] = Vec3f(-markerLength/2.f, markerLength/2.f, 0);
objPoints.ptr<Vec3f>(0)[1] = Vec3f(markerLength/2.f, markerLength/2.f, 0);
objPoints.ptr<Vec3f>(0)[2] = Vec3f(markerLength/2.f, -markerLength/2.f, 0);
objPoints.ptr<Vec3f>(0)[3] = Vec3f(-markerLength/2.f, -markerLength/2.f, 0);
vector<int> ids;
vector<vector<Point2f> > corners, rejected;
// 检测标记和估计姿态
detector.detectMarkers(image, corners, ids, rejected);
size_t nMarkers = corners.size();
vector<Vec3d> rvecs(nMarkers), tvecs(nMarkers);
if(estimatePose && !ids.empty()) {
// 计算每个标记的姿态
for (size_t i = 0; i < nMarkers; i++) {
solvePnP(objPoints, corners.at(i), camMatrix, distCoeffs, rvecs.at(i), tvecs.at(i));
}
}
  • corners 参数是由 cv::aruco::ArucoDetector::detectMarkers() 函数返回的角标记向量。
  • 第二个参数是以米或任何其他单位表示的标记边大小。请注意,估计位姿的平移向量将采用相同的单位。
  • camMatrixdistCoeffs 是在相机校准过程中创建的相机校准参数。
  • 输出参数 rvecstvecs 分别是检测到的 corners 中每个标记的旋转和平移向量。

此函数假定的标记坐标系位于标记的中心(默认情况下)或左上角,Z 轴指向外部,如下面的图像所示。轴颜色对应关系:X:红色,Y:绿色,Z:蓝色。请注意此图像中旋转标记的轴方向。

画有轴的图像

OpenCV 提供了一个函数可以绘制如上图所示的轴,以便检查位姿估计

// 绘制结果
image.copyTo(imageCopy);
if(!ids.empty()) {
cv::aruco::drawDetectedMarkers(imageCopy, corners, ids);
if(estimatePose) {
for(unsigned int i = 0; i < ids.size(); i++)
cv::drawFrameAxes(imageCopy, camMatrix, distCoeffs, rvecs[i], tvecs[i], markerLength * 1.5f, 2);
}
}
  • imageCopy 是将展示检测到的标记的输入/输出图像。
  • camMatrixdistCoeffs 是相机校准参数。
  • rvecs[i]tvecs[i] 分别是检测到的每个标记的旋转和平移向量。
  • 最后一个参数是轴的长度,以与 tvec 相同的单位表示(通常以米为单位)。

示例视频

detect_markers.cpp 中包含完整的操作示例,位于 samples/cpp/tutorial_code/objectDetection/ 中。

现在,示例通过使用 cv::CommandLineParser 从命令行获取输入。对于此文件,示例参数将类似于

-v=/path_to_opencv/opencv/doc/tutorials/objdetect/aruco_detection/images/singlemarkersoriginal.jpg -d=10
-c=/path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_camera_params.yml

detect_markers.cpp 的参数

const char* keys =
"{d | 0 | 词典:DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2,"
"DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, "
"DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12,"
"DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16,"
"DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20}"
"{cd | | 带有自定义词典的输入文件 }"
"{v | | 视频或图像文件的输入,如果省略,则输入来自摄像机 }"
"{ci | 0 | 如果输入不来自视频(-v),则为摄像头 ID }"
"{c | | 摄像机内参。对于摄像机位姿是必需的 }"
"{l | 0.1 | 标记边长(以米为单位)。对于摄像机位姿中的正确比例是必需的 }"
"{dp | | 标记检测参数文件 }"
"{r | | 也显示拒绝候选 }"
"{refine | | 角点优化:CORNER_REFINE_NONE=0, CORNER_REFINE_SUBPIX=1,"
"CORNER_REFINE_CONTOUR=2, CORNER_REFINE_APRILTAG=3}";

选择词典

aruco 模块提供 Dictionary 类来表示标记词典。

除了词典中的标记大小和标记数量之外,还有词典的另一个重要参数 - 标记间距离。标记间距离是词典标记之间最小的汉明距离,它决定了词典检测和纠正错误的能力。

一般来说,较小的词典大小和较大的标记尺寸会增加标记间距离,反之亦然。但是,由于需要从图像中提取的位数更多,检测尺寸较大的标记会更困难。

例如,如果您只需在应用程序中使用 10 个标记,最好使用仅由这 10 个标记组成的词典,而不是使用由 1000 个标记组成的词典。原因是仅由 10 个标记组成的词典会有较高的标记间距离,因此它对错误更鲁棒。

因此,aruco 模块包含多种选择标记词典的方式,以便提高系统的可靠性

预定义词典

这是选择词典的最简单方式。aruco 模块包含一组预定义词典,其中包含各种标记大小和标记数量。例如

cv::aruco::DICT_6X6_250 是预定义标记词典的一个示例,其中包含 6x6 位,共有 250 个标记。

在所有提供的词典中,建议选择最适合您应用程序的词典。例如,如果您需要 200 个 6x6 位标记,最好使用 cv::aruco::DICT_6X6_250 而不是 cv::aruco::DICT_6X6_1000。词典越小,标记间距越大。

可以在 PredefinedDictionaryType 枚举的文档中找到可用的预定义词典的列表。

自动生成词典

可以自动生成一个词典来调整所需的标记和位数,以便优化标记间距

Dictionary extendDictionary(int nMarkers, int markerSize, const Dictionary &baseDictionary=Dictionary(), int randomSeed=0)
通过新 nMarkers 扩展基础词典。

这将生成一个定制词典,其中包含 36 个 5x5 位标记。根据参数的不同,这个过程可能需要几秒钟(对于较大的词典和较高的位数,速度较慢)。

您还可以在 opencv/samples/cpp 中使用 aruco_dict_utils.cpp 样本。此样本计算生成词典的最小汉明距离,还允许您创建可抵御反射的标记。

手动定义词典

最后,可以手动配置词典,以便可以使用任何编码。为此,需要手动分配 cv::aruco::Dictionary 对象参数。必须注意,除非您有特殊原因需要手动执行此操作,否则最好使用前面的某个替代方案。

cv::aruco::Dictionary 参数是

Dictionary {
public:
cv::Mat bytesList; // 标记代码信息
int markerSize; // 每个维度的位数
int maxCorrectionBits; // 可纠正的最大位数
...
}

bytesList 是包含标记代码的所有信息的数组。markerSize 是每个标记维度的尺寸(例如,对于 5x5 位的标记,尺寸为 5)。最后,maxCorrectionBits 是在标记检测期间可纠正的错误位数的最大值。如果此值过大,则会导致大量误报。

bytesList 中的每一行都表示一个字典标记。但是,标记不是以二进制形式存储的,而是以特殊格式存储来简化检测过程。

幸运的是,可以使用静态方法 Dictionary::getByteListFromBits() 轻松地将标记转换为此格式。

例如

// 6x6 位的标记
dictionary.markerSize = 6;
// 最大位数校正
dictionary.maxCorrectionBits = 3;
// 我们创建一个包含 100 个标记的字典
for(int i = 0; i < 100; i++)
{
// 假设 generateMarkerBits() 以二进制格式生成一个新标记,这样
// markerBits 是一个 CV_8UC1 类型的 6x6 矩阵,其中只包含 0 和 1
cv::Mat markerBits = generateMarkerBits();
cv::Mat markerCompressed = cv::aruco::Dictionary::getByteListFromBits(markerBits);
// 添加标记作为新行
dictionary.bytesList.push_back(markerCompressed);
}
void push_back(const _Tp &elem)
将元素添加到矩阵底部。
int maxCorrectionBits
可以更正的最大比特数
定义 aruco_dictionary.hpp:34
int markerSize
每个尺寸的比特数
定义 aruco_dictionary.hpp:33
Mat bytesList
标记代码信息。有关更多详细信息,请参阅类说明
定义 aruco_dictionary.hpp:32
static Mat getByteListFromBits(const Mat &bits)
位图变换矩阵到包含 4 个标记旋转的字节列表。

检测器参数

cv::aruco::ArucoDetector 参数之一是 cv::aruco::DetectorParameters 对象。此对象包含标记检测过程中可自定义的所有选项。

此部分介绍每个检测器参数。根据涉及的参数所属的流程,对其进行分类。

阈值处理

标记检测过程中的第一步是对输入图像进行自适应阈值处理。

例如,上面使用的样本图像的阈值图像为

阈值图像

可以使用以下参数自定义此阈值处理

adaptiveThreshWinSizeMin、adaptiveThreshWinSizeMax 和 adaptiveThreshWinSizeStep

adaptiveThreshWinSizeMinadaptiveThreshWinSizeMax 参数表示在其中为自适应阈值处理选择阈值窗口大小(以像素为单位)的间隔(有关更多详细信息,请参阅 OpenCV 的 threshold()adaptiveThreshold() 函数)。

参数 adaptiveThreshWinSizeStep 表示从 adaptiveThreshWinSizeMinadaptiveThreshWinSizeMax 的窗口大小增量。

例如,对于值 adaptiveThreshWinSizeMin = 5 且 adaptiveThreshWinSizeMax = 21 且 adaptiveThreshWinSizeStep = 4,将有 5 个阈值处理步骤,窗口大小分别为 5、9、13、17 和 21。在每个阈值图像上,将提取标记候选。

如果标记大小过大,窗口大小过小可能会“破坏”标记边框,导致无法检测到该标记,如下面的图像所示

破损标记图像

另一方面,如果标记过小,过大的值也会产生相同的效果,而且还会降低性能。此外,此过程将趋向于全局阈值处理,从而导致自适应优势丢失。

最简单的情况是对 adaptiveThreshWinSizeMinadaptiveThreshWinSizeMax 使用相同的值,这会生成一个阈值处理步骤。但是,通常最好对窗口大小使用一个值范围,尽管许多阈值处理步骤也会显著降低性能。

请参阅
cv::aruco::DetectorParameters::adaptiveThreshWinSizeMincv::aruco::DetectorParameters::adaptiveThreshWinSizeMaxcv::aruco::DetectorParameters::adaptiveThreshWinSizeStep

adaptiveThreshConstant

adaptiveThreshConstant 参数表示在阈值处理操作中添加的常数值(有关更多详细信息,请参见 OpenCV threshold()adaptiveThreshold() 函数)。大多数情况下,其默认值是一个不错的选择。

请参阅
cv::aruco::DetectorParameters::adaptiveThreshConstant

轮廓滤波

在进行阈值处理后,将检测轮廓。但是,并非所有轮廓都被视为标记候选。它们在不同的步骤中被滤出,以便丢弃不太可能是标记的轮廓。此部分中的参数自定义此滤波过程。

必须注意,在大多数情况下,这是检测能力和性能之间平衡的问题。所有考虑的轮廓都将在以下阶段进行处理,这些阶段通常具有更高的计算成本。因此,最好在此阶段而非在后面的阶段丢弃无效候选。

另一方面,如果滤波条件过于严格,实际标记轮廓可能会被丢弃,从而无法检测到。

minMarkerPerimeterRate 和 maxMarkerPerimeterRate

这些参数确定标记的最小尺寸和最大尺寸,具体来说是标记的最小周长和最大周长。它们不是用绝对像素值指定的,而是相对于输入图像的最大维度指定的。

例如,大小为 640x480 的图像和最小相对标记周长为 0.05 的图像将导致最小标记周长为 640x0.05 = 32 个像素,因为 640 是图像的最大维度。maxMarkerPerimeterRate 参数也是如此。

如果 minMarkerPerimeterRate 太低,检测性能可能会显着下降,因为未来阶段将考虑更多轮廓。maxMarkerPerimeterRate 参数没有这么明显的惩罚,因为通常大轮廓远多于小轮廓。minMarkerPerimeterRate 值为 0,maxMarkerPerimeterRate 值为 4(或更大)将等同于考虑图像中的所有轮廓,但出于性能原因不建议这样做。

请参阅
cv::aruco::DetectorParameters::minMarkerPerimeterRatecv::aruco::DetectorParameters::maxMarkerPerimeterRate

polygonalApproxAccuracyRate

对每个候选应用多边形近似,仅接受近似为方形的候选。此值确定多边形近似可能产生的最大误差(有关详细信息,请参见 approxPolyDP() 函数)。

此参数与候选长度相关(以像素为单位)。因此,如果候选者的周长为 100 个像素,而 polygonalApproxAccuracyRate 的值为 0.04,则最大误差为 100x0.04=5.4 个像素。

在大多数情况下,默认值有效,但对于高度失真的图像,可能需要更高的误差值。

请参阅
cv::aruco::DetectorParameters::polygonalApproxAccuracyRate

minCornerDistanceRate

同个标记中任意两个角之间的最小距离。它相对于标记周长表示。以像素为单位的最小距离为周长 * minCornerDistanceRate。

请参阅
cv::aruco::DetectorParameters::minCornerDistanceRate

minMarkerDistanceRate

来自两个不同标记的任意两个角之间的最小距离。它相对于两个标记的最小标记周长表示。如果两个候选值太接近,较小的候选项将被忽略。

请参阅
cv::aruco::DetectorParameters::minMarkerDistanceRate

minDistanceToBorder

标记角到图像边界的最小距离(以像素为单位)。如果遮挡较小,则图像边界部分遮挡的标记可以被正确检测。但是,如果其中一个角被遮挡,则返回的角通常会错误地放置在图像边界附近的错误位置。

如果标记角的位置很重要,例如你想进行位姿估计,则最好丢弃其角距离图像边界很近的所有标记。在其他情况下,就没必要。

请参阅
cv::aruco::DetectorParameters::minDistanceToBorder

位提取

在候选者检测之后,会对每个候选者的位进行分析,以确定它们是不是标记。

在分析二进制代码本身之前,需要先摘取这些位。为此,纠正了透视畸变,并使用大津阈值分割所得图像,以区分黑色和白色像素。

以下是在去除标记透视畸变后获得的图像示例

消除透视

然后,将图像分成与标记中位数相同的方块数的网格。在每个方块中,统计黑色和白色像素的数量,以确定分配给方块的位值(取自大多数值)

标记方块

有几个参数可以自定义此流程

markerBorderBits

此参数指示标记边框的宽度。它相对于每个位的尺寸。因此,值 2 指示边框的宽度为两个内部位的宽度。

该参数需要与你使用的标记的边框大小一致。边框大小可以在标记绘制函数(例如 generateImageMarker())中进行配置。

请参阅
cv::aruco::DetectorParameters::markerBorderBits

minOtsuStdDev

该值确定执行 Otsu 阈值处理的像素值的最小标准差。如果偏差低,可能意味着所有正方形都是黑色(或白色),并且应用 Otsu 没有意义。如果是这种情况,则根据平均值高于或低于 128,所有位都将设置为 0(或 1)。

请参阅
cv::aruco::DetectorParameters::minOtsuStdDev

perspectiveRemovePixelPerCell

此参数用于确定纠正透视失真(包括边框)后所获得图像中每个格子(像素)的数量。这是上图中红色方块的大小。

例如,假设我们处理的是 5x5 位且边框大小为 1 位(参见 markerBorderBits)的标记。然后,每个维度的总格子/位数为 5 + 2*1 = 7(边框要计算两次)。总格子数为 7x7。

如果 perspectiveRemovePixelPerCell 的值为 10,则所获得图像的大小将为 10*7 = 70 -> 70x70 像素。

此参数值越高,可以提高一定程度的位提取过程,但会降低性能。

请参阅
cv::aruco::DetectorParameters::perspectiveRemovePixelPerCell

perspectiveRemoveIgnoredMarginPerCell

在提取每个格子的位时,会对黑色和白色像素的数量进行计数。通常,不建议考虑所有格子像素。而最好忽略掉格子边距中的一些像素。

原因在于,在消除透视失真后,格子的颜色通常不会完全分离,并且白色格子可能会侵入黑色格子的某些像素(反之亦然)。因此,最好忽略一些像素,以避免统计错误的像素。

例如,在以下图像中

标记格子边距

只考虑绿色方框内的像素。在右侧的图像中可以看到,结果像素包含较少来自相邻格子的噪声。perspectiveRemoveIgnoredMarginPerCell 参数表示红色方块和绿色方块之间的差异。

此参数相对于格子的总大小。例如,如果格子大小为 40 个像素,并且此参数的值为 0.1,则在格子中会忽略 40*0.1=4 个像素的边距。这意味着每个格子中实际分析的像素总数将为 32x32,而不是 40x40。

请参阅
cv::aruco::DetectorParameters::perspectiveRemoveIgnoredMarginPerCell

标记识别

提取位后,下一步是检查提取的代码是否属于标记词典,并且在有需要时可以执行错误校正。

maxErroneousBitsInBorderRate

标记边框的位应为黑色。此参数指定边框中允许的白位最大数,例如边框中允许的白位最大数。它是相对于标记中位总数来表示的。

请参阅
cv::aruco::DetectorParameters::maxErroneousBitsInBorderRate

errorCorrectionRate

每个标记字典都具有理论上可校正位数的上限(Dictionary.maxCorrectionBits)。不过,此值可以通过 errorCorrectionRate 参数进行修改。

例如,如果可校正的位数(用于字典的位数)允许值为 6,并且 errorCorrectionRate 的值为 0.5,则可校正位的实际最大数为 6*0.5=3 位。

此值有助于降低错误校正能力,以免出现误报。

请参阅
cv::aruco::DetectorParameters::errorCorrectionRate

Corner Refinement

在检测并识别出标记后,最后一步执行的是对角点位置进行亚像素级精化(请参见 OpenCV cornerSubPix()cv::aruco::CornerRefineMethod)。

请注意,此步骤是可选的,它仅在需要角点位置精确时才有意义,例如姿势估计。它通常是一个耗时的步骤,因此默认情况下会禁用它。

cornerRefinementMethod

此参数决定是否执行角点亚像素处理,以及在执行角点亚像素处理时使用哪种方法。如果不需要精确的角点,则可以禁用它。可能的值有 CORNER_REFINE_NONECORNER_REFINE_SUBPIXCORNER_REFINE_CONTOURCORNER_REFINE_APRILTAG

请参阅
cv::aruco::DetectorParameters::cornerRefinementMethod

cornerRefinementWinSize

此参数决定角点精化处理的最大窗口大小。

高值可能导致图像中的靠近角点被包含在窗口区域中,这样在处理过程中标记的角点会移动到不同的、不正确的位置。它还可能影响性能。如果 ArUco 标记过小,窗口大小可能会减小,请查看 cv::aruco::DetectorParameters::relativeCornerRefinmentWinSize。最终窗口大小计算为:min(cornerRefinementWinSize, averageArucoModuleSize*relativeCornerRefinmentWinSize),其中 averageArucoModuleSize 是 ArUco 标记在像素中的平均模块大小。

请参阅
cv::aruco::DetectorParameters::cornerRefinementWinSize

relativeCornerRefinmentWinSize

角点精化相对于 ArUco 模块大小的动态窗口大小(默认值 0.3)。

最终窗口大小计算为:min(cornerRefinementWinSize, averageArucoModuleSize*relativeCornerRefinmentWinSize), 其中 averageArucoModuleSize 是以像素为单位的 ArUco 标记的平均模块大小。如果标记彼此距离较大,建议将该参数的值上调至 0.4-0.5。如果标记彼此距离较近,则建议将参数值下调至 0.1-0.2。

请参阅
cv::aruco::DetectorParameters::relativeCornerRefinementWinSize

cornerRefinementMaxIterations 和 cornerRefinementMinAccuracy

这两个参数决定了次像素级细化过程的停止准则。cornerRefinementMaxIterations 表示最大迭代次数,cornerRefinementMinAccuracy 表示停止此过程之前的最小误差值。

如果迭代次数太大,可能会影响性能。另一方面,如果次数太少,可能会导致次像素级细化效果不佳。

请参阅
cv::aruco::DetectorParameters::cornerRefinementMaxIterationscv::aruco::DetectorParameters::cornerRefinementMinAccuracy