上一个教程: 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);
if(parser.has("v")) {
video = parser.get<
String>(
"v");
}
if(!parser.check()) {
parser.printErrors();
return 0;
}
int waitTime;
if(!video.empty()) {
waitTime = 0;
} else {
waitTime = 10;
}
float axisLength = 0.5f * ((float)min(markersX, markersY) * (markerLength + markerSeparation) +
markerSeparation);
double totalTime = 0;
int totalIterations = 0;
while(inputVideo.
grab()) {
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++;
if(totalIterations % 30 == 0) {
cout << "Detection Time = " << currentTime * 1000 << " ms "
<< "(Mean = " << 1000 * totalTime / double(totalIterations) << " ms)" << endl;
}
if(!ids.empty())
aruco::drawDetectedMarkers(imageCopy, corners, ids);
if(showRejected && !rejected.empty())
aruco::drawDetectedMarkers(imageCopy, rejected,
noArray(),
Scalar(100, 0, 255));
if(markersOfBoardDetected > 0)
char key = (char)
waitKey(waitTime);
if(key == 27) break;
参数说明:
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 | | 也显示被拒绝的候选对象 }";
}
网格板(Grid Board)
创建 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 构造函数,通过这些参数轻松创建此对象。
- 第一个和第二个参数分别是 X 和 Y 方向上的标记数量。
- 第三个和第四个参数分别是标记长度和标记间隔。它们可以用任何单位提供,请记住该板的估计姿态将以相同的单位测量(通常使用米)。
- 最后,提供了标记的字典。
因此,这个板将由 5x7=35 个标记组成。每个标记的 ID 默认按升序分配,从 0 开始,所以它们将是 0, 1, 2, ..., 34。
创建网格板后,我们可能希望打印并使用它。有两种方法可以做到这一点:
- 使用脚本
doc/patter_tools/gen_pattern.py,请参阅创建校准模式。
- 使用函数
cv::aruco::GridBoard::generateImage()。
函数 cv::aruco::GridBoard::generateImage() 在 cv::aruco::GridBoard 类中提供,可以使用以下代码调用:
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 个标记),缺失标记的投影质量可能会很差,从而产生错误的对应关系。
有关更详细的实现,请参阅模块示例。