OpenCV 4.11.0
开源计算机视觉库
正在加载...
正在搜索...
未找到匹配项
ChArUco棋盘的检测

上一篇教程: ArUco 棋盘的检测
下一篇教程: 菱形标记的检测
ArUco 标记和棋盘由于其快速的检测速度和多功能性而非常有用。然而,ArUco 标记的一个问题是,即使应用了亚像素细化,其角点的精度也不是很高。

相反,棋盘图案的角点可以更精确地细化,因为每个角点都被两个黑色方块包围。然而,查找棋盘图案不如查找 ArUco 棋盘那样通用:它必须完全可见,并且不允许遮挡。

ChArUco 棋盘试图结合这两种方法的优点。

Charuco 定义

ArUco 部分用于插值棋盘角点的位置,使其具有标记棋盘的多功能性,因为它允许遮挡或部分视图。此外,由于插值角点属于棋盘,因此在亚像素精度方面非常准确。

当需要高精度时,例如在相机标定中,Charuco 棋盘比标准 ArUco 棋盘更好。

目标

在本教程中,您将学习

  • 如何创建一个 Charuco 棋盘?
  • 如何在不进行相机标定的情况下检测 Charuco 角点?
  • 如何在进行相机标定和位姿估计的情况下检测 Charuco 角点?

源代码

您可以在 samples/cpp/tutorial_code/objectDetection/detect_board_charuco.cpp 中找到此代码。

这是一个关于如何实现目标列表中所有内容的示例代码。

int squaresX = parser.get<int>("w");
int squaresY = parser.get<int>("h");
float squareLength = parser.get<float>("sl");
float markerLength = parser.get<float>("ml");
bool refine = parser.has("rs");
int camId = parser.get<int>("ci");
string video;
if(parser.has("v")) {
video = parser.get<string>("v");
}
Mat camMatrix, distCoeffs;
readCameraParamsFromCommandLine(parser, camMatrix, distCoeffs);
aruco::DetectorParameters detectorParams = readDetectorParamsFromCommandLine(parser);
aruco::Dictionary dictionary = readDictionatyFromCommandLine(parser);
if(!parser.check()) {
parser.printErrors();
return -1; 0;
}
VideoCapture inputVideo;
int waitTime = 0;
if(!video.empty()) {
inputVideo.open(video);
} else {
inputVideo.open(camId);
waitTime = 10;
}
float axisLength = 0.5f * ((float)min(squaresX, squaresY) * (squareLength));
// 创建 Charuco 棋盘对象
aruco::CharucoBoard charucoBoard(Size(squaresX, squaresY), squareLength, markerLength, dictionary);
// 创建 Charuco 检测器
aruco::CharucoParameters charucoParams;
charucoParams.tryRefineMarkers = refine; // 如果 tryRefineMarkers,则在 detectBoard() 中将使用 refineDetectedMarkers()
charucoParams.cameraMatrix = camMatrix; // cameraMatrix 可用于 detectBoard()
charucoParams.distCoeffs = distCoeffs; // distCoeffs 可用于 detectBoard()
aruco::CharucoDetector charucoDetector(charucoBoard, charucoParams, detectorParams);
double totalTime = 0;
int totalIterations = 0;
while(inputVideo.grab()) {
Mat image, imageCopy;
inputVideo.retrieve(image);
double tick = (double)getTickCount();
vector<int> markerIds, charucoIds;
vector<vector<Point2f> > markerCorners;
vector<Point2f> charucoCorners;
Vec3d rvec, tvec;
// 检测标记和 Charuco 角点
charucoDetector.detectBoard(image, charucoCorners, charucoIds, markerCorners, markerIds);
// 估计 Charuco 棋盘位姿
bool validPose = false;
if(camMatrix.total() != 0 && distCoeffs.total() != 0 && charucoIds.size() >= 4) {
Mat objPoints, imgPoints;
charucoBoard.matchImagePoints(charucoCorners, charucoIds, objPoints, imgPoints);
validPose = solvePnP(objPoints, imgPoints, camMatrix, distCoeffs, rvec, tvec);
}
double currentTime = ((double)getTickCount() - tick) / getTickFrequency();
totalTime += currentTime;
totalIterations++;
if(totalIterations % 30 == 0) {
cout << "检测时间 = " << currentTime * 1000 << " ms "
<< "(平均 = " << 1000 * totalTime / double(totalIterations) << " ms)" << endl;
}
// 绘制结果
image.copyTo(imageCopy);
if(markerIds.size() > 0) {
aruco::drawDetectedMarkers(imageCopy, markerCorners);
}
if(charucoIds.size() > 0) {
aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, cv::Scalar(255, 0, 0));
}
if(validPose)
cv::drawFrameAxes(imageCopy, camMatrix, distCoeffs, rvec, tvec, axisLength);
imshow("out", imageCopy);
if(waitKey(waitTime) == 27) break;
}

ChArUco 棋盘创建

aruco 模块提供了 cv::aruco::CharucoBoard 类,该类表示 Charuco 棋盘,并继承自 cv::aruco::Board 类。

此类以及其余 ChArUco 功能定义在

要定义一个cv::aruco::CharucoBoard,需要以下参数:

  • X 和 Y 方向上的棋盘方格数量。
  • 方格边长。
  • 标记边长。
  • 标记的字典。
  • 所有标记的 ID。

对于cv::aruco::GridBoard 对象,aruco 模块提供了方便创建cv::aruco::CharucoBoard 的方法。可以使用cv::aruco::CharucoBoard 构造函数通过这些参数轻松创建此对象。

aruco::Dictionary dictionary = readDictionatyFromCommandLine(parser);
cv::aruco::CharucoBoard board(Size(squaresX, squaresY), (float)squareLength, (float)markerLength, dictionary);
  • 第一个参数分别是 X 和 Y 方向上的方格数量。
  • 第二个和第三个参数分别是方格和标记的长度。它们可以使用任何单位,记住此棋盘的估计姿态将使用相同的单位进行测量(通常使用米)。
  • 最后,提供标记的字典。

每个标记的 ID 默认情况下按升序排列,从 0 开始,就像cv::aruco::GridBoard 构造函数一样。可以通过访问board.ids 向量(类似于cv::aruco::Board 父类)轻松自定义。

获得cv::aruco::CharucoBoard 对象后,可以创建一个图像来打印它。有两种方法可以做到这一点:

  1. 使用脚本doc/patter_tools/gen_pattern.py,参见创建校准图案
  2. 使用函数cv::aruco::CharucoBoard::generateImage()

函数cv::aruco::CharucoBoard::generateImage()cv::aruco::CharucoBoard 类中提供,可以使用以下代码调用:

Mat boardImage;
Size imageSize;
imageSize.width = squaresX * squareLength + 2 * margins;
imageSize.height = squaresY * squareLength + 2 * margins;
board.generateImage(imageSize, boardImage, margins, borderBits);
  • 第一个参数是输出图像的像素大小。如果这不与棋盘尺寸成比例,它将居中于图像。
  • 第二个参数是包含 Charuco 棋盘的输出图像。
  • 第三个参数是(可选)边距(以像素为单位),因此没有标记接触图像边界。
  • 最后,标记边框的大小,类似于cv::aruco::generateImageMarker() 函数。默认值为 1。

输出图像将如下所示:

完整的示例包含在samples/cpp/tutorial_code/objectDetection/ 中的create_board_charuco.cpp 文件中。

示例create_board_charuco.cpp 现在通过cv::CommandLineParser 从命令行接收输入。对于此文件,示例参数将如下所示:

"_output_path_/chboard.png" -w=5 -h=7 -sl=100 -ml=60 -d=10

ChArUco 棋盘格检测

检测 ChArUco 棋盘格时,实际上检测的是棋盘的每个角点。

ChArUco 棋盘格上的每个角点都有一个唯一的标识符 (ID) 分配。这些 ID 从 0 到棋盘上的角点总数。ChArUco 棋盘格检测步骤可以分解为以下步骤:

  • 输入图像
Mat image, imageCopy;
inputVideo.retrieve(image);

包含要检测的标记的原始图像。该图像对于在 ChArUco 角点执行亚像素细化是必要的。

  • 读取相机校准参数(仅限于使用相机校准进行检测)
if(parser.has("c")) {
bool readOk = readCameraParameters(parser.get<std::string>("c"), camMatrix, distCoeffs);
if(!readOk) {
throw std::runtime_error("Invalid camera file\n");
}
}

readCameraParameters 的参数是:

  • 第一个参数是相机内参矩阵和畸变系数的路径。
  • 第二个和第三个参数是 cameraMatrix 和 distCoeffs。

此函数将这些参数作为输入,并返回一个布尔值,指示相机校准参数是否有效。对于无需校准的 Charuco 角点检测,不需要此步骤。

  • 检测标记并从标记插值 Charuco 角点

ChArUco 角点的检测基于先前检测到的标记。因此,首先检测标记,然后从标记插值 ChArUco 角点。检测 ChArUco 角点的方法是cv::aruco::CharucoDetector::detectBoard()

// 检测标记和 Charuco 角点
charucoDetector.detectBoard(image, charucoCorners, charucoIds, markerCorners, markerIds);

detectBoard 的参数是:

  • image - 输入图像。
  • charucoCorners - 检测到的角点的图像位置的输出列表。
  • charucoIds - charucoCorners 中每个检测到的角点的输出 ID。
  • markerCorners - 检测到的标记角点的输入/输出向量。
  • markerIds - 检测到的标记的标识符的输入/输出向量。

如果 markerCorners 和 markerIds 为空,则该函数将检测 aruco 标记和 ID。

如果提供校准参数,则 ChArUco 角点通过首先根据 ArUco 标记估计粗略姿态,然后将 ChArUco 角点重新投影回图像来进行插值。

另一方面,如果未提供校准参数,则通过计算ChArUco平面与ChArUco图像投影之间的对应单应性来插值ChArUco角点。

使用单应性的主要问题是插值对图像畸变更敏感。实际上,仅使用每个ChArUco角点最近的标记进行单应性变换,以减少畸变的影响。

在检测ChArUco棋盘的标记时,尤其是在使用单应性时,建议禁用标记的角点细化。原因是由于棋盘方格的邻近性,亚像素过程可能会导致角点位置产生较大的偏差,而这些偏差会传播到ChArUco角点插值,从而产生较差的结果。

注意
为避免偏差,棋盘方格与Aruco标记之间的边距应大于一个标记模块的70%。

此外,只返回其两个周围标记都已找到的角点。如果两个周围标记中的任何一个未被检测到,这通常意味着该区域存在遮挡或图像质量不好。无论如何,最好不考虑该角点,因为我们希望确保插值的ChArUco角点非常精确。

插值ChArUco角点后,进行亚像素细化。

一旦我们插值了ChArUco角点,我们可能希望绘制它们以查看它们的检测是否正确。这可以使用cv::aruco::drawDetectedCornersCharuco()函数轻松完成

aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, cv::Scalar(255, 0, 0));
  • imageCopy是将绘制角点的图像(通常与检测角点的图像相同)。
  • outputImage将是带有已绘制角点的inputImage的克隆。
  • charucoCornerscharucoIds是从cv::aruco::CharucoDetector::detectBoard()函数检测到的ChArUco角点。
  • 最后,最后一个参数是我们想要绘制角点的(可选)颜色,类型为cv::Scalar

对于此图像

带有ChArUco棋盘的图像

结果将是

检测到的ChArUco棋盘

在存在遮挡的情况下,如下面的图像所示,尽管某些角点清晰可见,但由于遮挡,并非所有周围标记都被检测到,因此它们没有被插值。

带有遮挡的ChArUco检测

示例视频

完整的运行示例包含在samples/cpp/tutorial_code/objectDetection/内的detect_board_charuco.cpp中。

示例detect_board_charuco.cpp现在通过cv::CommandLineParser通过命令行接收输入。对于此文件,示例参数将如下所示:

-w=5 -h=7 -sl=0.04 -ml=0.02 -d=10 -v=/path_to_opencv/opencv/doc/tutorials/objdetect/charuco_detection/images/choriginal.jpg

ChArUco姿态估计

ChArUco棋盘的最终目标是非常精确地找到角点,以进行高精度校准或姿态估计。

aruco模块提供一个函数,可以轻松地执行ChArUco姿态估计。与cv::aruco::GridBoard一样,cv::aruco::CharucoBoard的坐标系放置在棋盘平面内,Z轴指向内部,并位于棋盘的左下角。

注意
OpenCV 4.6.0之后,棋盘的坐标系发生不兼容的更改,现在坐标系放置在棋盘平面内,Z轴指向平面内(以前轴指向平面外)。顺时针方向的objPoints对应于指向平面内的Z轴。逆时针方向的objPoints对应于指向平面外的Z轴。参见PR https://github.com/opencv/opencv_contrib/pull/3174

要对charuco棋盘执行姿态估计,应使用cv::aruco::CharucoBoard::matchImagePoints()cv::solvePnP()

// 估计 Charuco 棋盘位姿
bool validPose = false;
if(camMatrix.total() != 0 && distCoeffs.total() != 0 && charucoIds.size() >= 4) {
Mat objPoints, imgPoints;
charucoBoard.matchImagePoints(charucoCorners, charucoIds, objPoints, imgPoints);
validPose = solvePnP(objPoints, imgPoints, camMatrix, distCoeffs, rvec, tvec);
}
  • charucoCornerscharucoIds参数是从cv::aruco::CharucoDetector::detectBoard()函数检测到的charuco角点。
  • cameraMatrixdistCoeffs是姿态估计所需的相机校准参数。
  • 最后,rvectvec参数是ChArUco棋盘的输出姿态。
  • cv::solvePnP()如果姿态估计正确则返回true,否则返回false。失败的主要原因是姿态估计的角点不足或在同一条线上。

可以使用cv::drawFrameAxes()绘制轴线,以检查姿态是否正确估计。结果将是:(X:红色,Y:绿色,Z:蓝色)

ChArUco棋盘轴线

完整的运行示例包含在samples/cpp/tutorial_code/objectDetection/内的detect_board_charuco.cpp中。

示例detect_board_charuco.cpp现在通过cv::CommandLineParser通过命令行接收输入。对于此文件,示例参数将如下所示:

-w=5 -h=7 -sl=0.04 -ml=0.02 -d=10
-v=/path_to_opencv/opencv/doc/tutorials/objdetect/charuco_detection/images/choriginal.jpg
-c=/path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_camera_charuco.yml