上一教程: ArUco标记检测
下一教程: ChArUco板检测
原作者 Sergio Garrido, Alexander Panov
兼容性 OpenCV >= 4.7.0
ArUco板是一组标记,其作用类似于单个标记,因为它为相机提供单个姿态。
最常用的板是所有标记都在同一平面上的板,因为它易于打印。
但是,板不限于此排列,可以表示任何二维或三维布局。
板与一组独立标记的区别在于,板中标记之间的相对位置是先验已知的。这允许使用所有标记的角点来估计相机相对于整个板的姿态。
当使用一组独立标记时,可以分别估计每个标记的姿态,因为您不知道环境中标记的相对位置。
使用板的主要好处是:
姿态估计更加灵活。只需要一些标记就可以进行姿态估计。因此,即使存在遮挡或部分视图,也可以计算姿态。
获得的姿态通常更准确,因为使用了更多点对应关系(标记角点)。
板检测
板检测类似于标准标记检测。唯一的区别在于姿态估计步骤。事实上,要使用标记板,应该先进行标准标记检测,然后再估计板的姿态。
要对板进行姿态估计,应使用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; 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 << "检测时间 = " << currentTime * 1000 << " ms "
<< "(平均 = " << 1000 * totalTime / double(totalIterations) << " ms)" << endl;
}
if (!ids.empty())
aruco::drawDetectedMarkers(imageCopy, corners, ids);
如果(showRejected && !rejected.empty())
aruco::drawDetectedMarkers(imageCopy, rejected,
noArray (),
Scalar (100, 0, 255));
如果(markersOfBoardDetected > 0)
char key = (char)
waitKey (waitTime);
如果(key == 27) break;
参数为
drawFrameAxes() 函数可用于检查获得的姿态。例如
带有坐标轴的棋盘
这是另一个棋盘部分遮挡的示例
带有遮挡的棋盘
可以看出,即使一些标记未被检测到,仍然可以根据其余标记估计棋盘姿态。
示例视频
VIDEO
完整的可运行示例包含在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
构造函数根据这些参数轻松创建此对象
第一个和第二个参数分别是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 个标记),则缺失标记的投影质量可能会很差,从而导致错误的对应关系。
有关更详细的实现,请参阅模块示例。