OpenCV 4.12.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 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 为真,refineDetectedMarkers() 将在 detectBoard() 中使用
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 << "Detection Time = " << currentTime * 1000 << " ms "
<< "(Mean = " << 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