OpenCV 4.13.0
开源计算机视觉库 (Open Source Computer Vision)
正在加载...
正在搜索...
未找到匹配项
ArUco 标记检测

下一个教程: ArUco 板检测

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

姿态估计在许多计算机视觉应用中至关重要:机器人导航、增强现实等等。此过程基于在真实环境中的点及其2D图像投影之间寻找对应关系。这通常是一个困难的步骤,因此通常使用合成或基准标记来简化它。

最流行的方法之一是使用二值方形基准标记。这些标记的主要优点是单个标记提供足够的对应关系(其四个角)以获取相机姿态。此外,内部的二进制编码使其特别稳健,允许应用错误检测和纠正技术。

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

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:840
Dictionary 是由相同大小的唯一 ArUco 标记组成的集合。
定义 aruco_dictionary.hpp:29
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:109

首先,通过选择 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. 在候选检测之后,需要通过分析其内部编码来确定它们是否确实是标记。此步骤首先提取每个标记的标记位。为此,首先应用透视变换以获取其规范形式的标记。然后,使用 Otsu 对规范图像进行阈值处理,以分离白色和黑色位。根据标记大小和边框大小将图像分成不同的单元格。然后计算每个单元格中黑色或白色像素的数量,以确定它是白色位还是黑色位。最后,分析这些位以确定标记是否属于特定字典。必要时采用纠错技术。

考虑以下图像

包含各种标记的图像

以及这张图片的照片打印件

带有标记的原始图像

这些是检测到的标记(绿色)。请注意,一些标记已旋转。小的红色方块表示标记的左上角

带有检测到的标记的图像

这些是在识别步骤中被拒绝的标记候选(粉红色)

带有被拒绝候选的图像

在 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
struct DetectorParameters 由 ArucoDetector 使用
定义 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 是将绘制标记的输入/输出图像(通常与检测到标记的图像相同)。
  • markerCornersmarkerIds 是由 cv::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 << "检测时间 = " << currentTime * 1000 << " 毫秒 "
<< "(平均 = " << 1000 * totalTime / double(totalIterations) << " 毫秒)" << 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;
}

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

一个完整的示例包含在 samples/cpp/tutorial_code/objectDetection/ 中的 detect_markers.cpp 中。

这些示例现在使用 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}";

姿态估计

检测到标记后,您可能想要做的下一件事是使用它们来获取相机姿态。

要执行相机姿态估计,您需要了解相机的校准参数。这些是相机矩阵和畸变系数。如果您不知道如何校准相机,可以查看 calibrateCamera() 函数和 OpenCV 的校准教程。您也可以使用 aruco 模块校准相机,如 使用 ArUco 和 ChArUco 进行校准 教程中所述。请注意,这只需在相机光学元件未修改(例如更改其焦距)的情况下执行一次。

校准的结果是得到一个相机矩阵:一个 3x3 元素的矩阵,包含焦距和相机中心坐标(即内参),以及畸变系数:一个 5 个或更多元素的向量,用于模拟相机产生的畸变。

当您使用 ArUco 标记估计姿态时,您可以单独估计每个标记的姿态。如果您想从一组标记中估计一个姿态,请使用 ArUco 板(请参阅 ArUco 板检测 教程)。使用 ArUco 板而不是单个标记允许某些标记被遮挡。

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

Mat camMatrix, distCoeffs;
if(estimatePose) {
// 您可以从 tutorial_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 单位相同(通常为米)。

示例视频

一个完整的示例包含在 samples/cpp/tutorial_code/objectDetection/ 中的 detect_markers.cpp 中。

这些示例现在使用 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 参数是

class 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 是一个 6x6 的 CV_8UC1 类型矩阵,只包含 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::adaptiveThreshWinSizeMin, cv::aruco::DetectorParameters::adaptiveThreshWinSizeMax, cv::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::minMarkerPerimeterRate, cv::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

位提取

在候选标记检测之后,分析每个候选标记的位,以确定它们是否是标记。

在分析二进制代码本身之前,需要提取位。为此,校正透视畸变,并使用 Otsu 阈值处理生成的图像以分离黑白像素。

这是移除标记透视畸变后得到的图像示例

透视移除

然后,将图像划分为一个网格,其单元格数量与标记中的位数相同。在每个单元格中,计算黑白像素的数量,以确定分配给该单元格的位值(根据多数值)

标记单元格

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

markerBorderBits

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

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

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

minOtsuStdDev

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

另请参阅
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

角点精炼

标记检测和识别后,最后一步是对角点位置进行亚像素精炼(请参阅 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::relativeCornerRefinmentWinSize

cornerRefinementMaxIterations 和 cornerRefinementMinAccuracy

这两个参数决定了亚像素精炼过程的停止标准。cornerRefinementMaxIterations 表示最大迭代次数,cornerRefinementMinAccuracy 表示停止过程前的最小误差值。

如果迭代次数过高,可能会影响性能。另一方面,如果过低,则可能导致亚像素精炼效果不佳。

另请参阅
cv::aruco::DetectorParameters::cornerRefinementMaxIterations, cv::aruco::DetectorParameters::cornerRefinementMinAccuracy