上一教程: ArUco 标记的检测
下一教程: ChArUco 板的检测
| |
原始作者 | Sergio Garrido, Alexander Panov |
兼容性 | OpenCV >= 4.7.0 |
ArUco 板是一组标记的集合,可以充当单个标记,因为它为相机提供了一个单一的姿态。
最流行的板是在同一平面上具有所有标记的那个,因为它很容易打印
但是,电路板并不局限于此排列,并且可以表示任何 2d 或 3d 布局。
电路板和一组独立标记之间的区别在于,事先已知电路板中标记之间的相对位置。这允许使用所有标记的角来估计相机相对于整个电路板的姿态。
当您使用一组独立标记时,您可以为每个标记单独估计姿态,因为您不知道环境中标记的相对位置。
使用电路板的主要好处是
- 姿态估计更加通用。只需一些标记即可执行姿态估计。因此,即使存在遮挡或部分视图,也可以计算出姿态。
- 由于采用了更多数量的点对应关系(标记角),因此获得的姿态通常更加准确。
电路板检测
电路板检测类似于标准标记检测。唯一的区别在于姿态估计步骤。事实上,要使用标记板,应该在估计电路板姿态之前执行标准标记检测。
要对电路板执行姿态估计,您应该使用 solvePnP()
函数,如下所示 samples/cpp/tutorial_code/objectDetection/detect_board.cpp
中所示。
int markersX = parser.get<int>("w");
int markersY = parser.get<int>("h");
float markerLength = parser.get<float>("l");
float markerSeparation = parser.get<float>("s");
bool showRejected = parser.has("r");
bool refindStrategy = parser.has("rs");
int camId = parser.get<int>("ci");
Mat camMatrix, distCoeffs;
readCameraParamsFromCommandLine(parser, camMatrix, distCoeffs);
aruco::Dictionary dictionary = readDictionatyFromCommandLine(parser);
aruco::DetectorParameters detectorParams = readDetectorParamsFromCommandLine(parser);
String video;
if(parser.has("v")) {
video = parser.get<String>("v");
}
if(!parser.check()) {
parser.printErrors();
return 0;
}
aruco::ArucoDetector detector(dictionary, detectorParams);
VideoCapture inputVideo;
int waitTime;
if(!video.empty()) {
inputVideo.open(video);
waitTime = 0;
} else {
inputVideo.open(camId);
waitTime = 10;
}
float axisLength = 0.5f * ((float)
min(markersX, markersY) * (markerLength + markerSeparation) +
markerSeparation);
aruco::GridBoard board(
Size(markersX, markersY), markerLength, markerSeparation, dictionary);
double totalTime = 0;
int totalIterations = 0;
while(inputVideo.grab()) {
Mat image, imageCopy;
inputVideo.retrieve(image);
vector<int> ids;
vector<vector<Point2f>> corners, rejected;
detector.detectMarkers(image, corners, ids, rejected);
if(refindStrategy)
detector.refineDetectedMarkers(image, board, corners, ids, rejected, camMatrix,
distCoeffs);
int markersOfBoardDetected = 0;
if(!ids.empty()) {
board.matchImagePoints(corners, ids, objPoints, imgPoints);
cv::solvePnP(objPoints, imgPoints, camMatrix, distCoeffs, rvec, tvec);
markersOfBoardDetected = (int)objPoints.
total() / 4;
}
totalTime += currentTime;
totalIterations++;
如果(totalIterations % 30 == 0) {
cout << "检测时间 = " << currentTime * 1000 << " 毫秒 "
<< "(平均 = " << 1000 * totalTime / double(totalIterations) << " 毫秒)" << endl;
}
image.copyTo(imageCopy);
如果(!ids.empty())
aruco::drawDetectedMarkers(imageCopy, corners, ids);
如果(showRejected && !rejected.empty())
aruco::drawDetectedMarkers(imageCopy, rejected,
noArray(),
Scalar(100, 0, 255));
如果(markersOfBoardDetected > 0)
如果(key == 27) 退出;
size_t total() const
返回数组元素总数。
bool solvePnP(InputArray objectPoints, InputArray imagePoints, InputArray cameraMatrix, InputArray distCoeffs, OutputArray rvec, OutputArray tvec, bool useExtrinsicGuess=false, int flags=SOLVEPNP_ITERATIVE)
从 3D-2D 点对应关系查找对象姿态。
void drawFrameAxes(InputOutputArray image, InputArray cameraMatrix, InputArray distCoeffs, InputArray rvec, InputArray tvec, float length, int thickness=3)
根据姿态估计绘制世界/物体坐标系轴。
void min(InputArray src1, InputArray src2, OutputArray dst)
计算两个数组或一个数组和一个标量的逐元素最小值。
Size2i Size
定义 types.hpp:370
Vec< double, 3 > Vec3d
定义 matx.hpp:464
Scalar_< double > Scalar
定义 types.hpp:702
InputOutputArray noArray()
double getTickFrequency()
返回每秒滴答声数。
int64 getTickCount()
返回滴答声数。
void imshow(const String &winname, InputArray mat)
在指定窗口中显示图像。
int waitKey(int delay=0)
等待按下的键。
参数是
可以使用 `drawFrameAxes()` 函数来检查获得的位姿。例如
带轴的电路板
这是另一个电路板部分遮挡的示例。
带遮挡的电路板
正如所观察到的,尽管某些标记尚未检测到,但仍然可以从其他标记中估计电路板位姿。
示例视频
完整的实际示例包含在 samples/cpp/tutorial_code/objectDetection/ 中的 detect_board.cpp 内。
示例现在通过 cv::CommandLineParser 接受命令行输入。对于此文件,示例参数将如下所示:
-w=5 -h=7 -l=100 -s=10
-v=/path_to_opencv/opencv/doc/tutorials/objdetect/aruco_board_detection/gboriginal.jpg
-c=/path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_camera_params.yml
-cd=/path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_dict.yml
detect_board.cpp 的参数
const char* keys =
"{w | | X 方向的方块数 }"
"{h | | Y 方向的方块数 }"
"{l | | 标记边长(以像素为单位) }"
"{s | | 网格中两个连续标记之间的间隔(以像素为单位)}"
"{d | | 词典: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 | | 带有自定义词典的输入文件 }"
"{c | | 带有已校准相机参数的输出文件 }"
"{v | | 来自视频或图像文件的输入,如果省略,则输入来自相机 }"
"{ci | 0 | 如果输入不来自视频 (-v),则为相机 ID }"
"{dp | | 标记检测器参数文件 }"
"{rs | | 应用重新查找策略 }"
"{r | | 显示已拒绝的候选项}";
}
网格板
要创建cv::aruco::Board
对象,需要指定环境中每个标记的角点位置。但是,在许多情况下,标记板只是位于同一平面中且处于网格布局中的一组标记,因此,可以轻松地打印并使用它。
幸运的是,aruco 模块提供了以下基本功能,用于轻松创建和打印这些类型的标记。
cv::aruco::GridBoard
类是一个专门类,它继承自cv::aruco::Board
类,并且表示具有处于同一平面中且处于网格布局中(如下面的图片)中的所有标记的标记板。
带有 aruco 标记板的图片
具体来说,网格标记板中的坐标系统位于标记板的平面中,居中位于标记板的左下角,Z 指向外,如下面的图片所示(X:红色,Y:绿色,Z:蓝色)
带轴的电路板
可以使用以下参数定义一个cv::aruco::GridBoard
对象
- X 方向中标记的数量。
- Y 方向中标记的数量。
- 标记长度。
- 标记间距。
- 标记的字典。
- 所有标记的 ID(X*Y 标记)。
可以使用以下参数轻松地通过cv::aruco::GridBoard
构造函数创建该对象
aruco::GridBoard board(Size(markersX, markersY), markerLength, markerSeparation, dictionary);
- 第一个和第二个参数分别是 X 和 Y 方向中标记的数量。
- 第三个和第四个参数分别是标记长度和标记间距。可以提供任意单位,但需要注意,这个标记板的估计位姿将以相同单位(一般使用米)度量。
- 最后,提供标记的字典。
因此,这个标记板将由 5x7=35 个标记组成。默认情况下,会按从 0 开始的升序顺序分配每个标记的 id,因此它们将为 0、1、2、...、34。
创建网格标记板后,我们可能想要打印并使用它。有两种方法可以做到这一点
- 使用脚本
doc/patter_tools/gen_pattern.py
,请参阅创建校准图案。
- 使用函数
cv::aruco::GridBoard::generateImage()
。
函数cv::aruco::GridBoard::generateImage()
在cv::aruco::GridBoard类中提供,可以通过使用以下代码进行调用
Mat boardImage;
board.generateImage(imageSize, boardImage, margins, borderBits);
- 第一个参数是输出图像的像素大小。在此示例中为 600x500 像素。如果不与棋盘尺寸成比例,则它将居中显示在图像上。
boardImage
:带有棋盘的输出图像。
- 第三个参数是(可选)像素边距,因此任何标记都不会触及图像边框。在此示例中,边距为 10.
- 最后,标记边框的大小,类似于
generateImageMarker()
函数。默认值为 1.
在samples/cpp/tutorial_code/objectDetection/create_board.cpp
中附带了一个完整的棋盘创建工作示例。
输出图像将如下所示
现在,示例通过cv::CommandLineParser
通过命令行获取输入。对于此文件,示例参数将如下所示
"_output_path_/aboard.png" -w=5 -h=7 -l=100 -s=10 -d=10
精炼标记检测
ArUco 棋盘还可以用于改进标记检测。如果我们检测到属于棋盘的标记子集,我们可以使用这些标记和棋盘布局信息尝试找到之前未检测到的标记。
这可以通过使用cv::aruco::refineDetectedMarkers()
函数来完成,该函数应在调用cv::aruco::ArucoDetector::detectMarkers()
后调用。
该函数的主要参数是检测到标记的原始图像、棋盘对象、检测到的标记角点、检测到的标记 ID 和被拒绝的标记角点。
可以从cv::aruco::ArucoDetector::detectMarkers()
函数中获取被拒绝的角点,这些角点也被称为标记候选。这些候选是方形形状,它们已在原始图像中找到,但未通过识别步骤(即其内部编码显示了太多错误),因此未被识别为标记。
但是,这些候选有时是由于图像中噪音大、分辨率极低或影响二进制代码提取的其他相关问题导致未能正确识别的实际标记。cv::aruco::ArucoDetector::refineDetectedMarkers()
函数将这些候选与棋盘的缺失标记之间的对应项关联起来。此搜索基于两个参数
- 候选与缺失标记的投影之间的距离。若要获得这些投影,必须至少检测到棋盘的一个标记。如果已提供相机参数(相机矩阵和畸变系数),则使用相机参数获得投影。如果没有,则从局部单应性中获得投影,并且只允许平面棋盘(即所有标记角点的 Z 坐标都相同)。
refineDetectedMarkers()
中的 minRepDistance
参数决定候选角点和投影标记角点之间的最小欧氏距离(默认值 10)。
- 二进制编码。如果候选超出最小距离条件,则再次分析其内部比特,以确定其是否实际上是投影标记。但是,在这种情况下,条件并不那么严格,允许的错误比特数量可以更高。这在
errorCorrectionRate
参数中指明(默认值 3.0)。如果提供负值,则根本不分析内部比特,并且只评估角点距离。
以下是如何使用 cv::aruco::ArucoDetector::refineDetectedMarkers()
函数的示例
detector.detectMarkers(image, corners, ids, rejected);
if(refindStrategy)
detector.refineDetectedMarkers(image, board, corners, ids, rejected, camMatrix,
distCoeffs);
还必须注意,在某些情况下,如果最初检测到的标记数量太少(例如只有 1 或 2 个标记),则缺失标记的投影质量可能会较差,从而导致错误的对应项。
参见模块样本了解更多详细的实现。