OpenCV  4.10.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();
返回 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 << "检测时间 = " << currentTime * 1000 << " 毫秒 "
<< "(平均 = " << 1000 * totalTime / double(totalIterations) << " 毫秒)" << 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;
}
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
Definition types.hpp:370
Vec< double, 3 > Vec3d
Definition matx.hpp:464
double getTickFrequency()
返回每秒嘀嗒次数。
int64 getTickCount()
返回嘀嗒次数。
void imshow(const String &winname, InputArray mat)
在指定窗口显示图像。
int waitKey(int delay=0)
等待按下的键。

ChArUco 板创建

aruco 模块提供了表示 ChArUco 板的 cv::aruco::CharucoBoard 类,该类继承自 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);
ChArUco 棋盘是一个平面棋盘,其中的标记放置在国际象棋棋盘的白方格内...
定义 aruco_board.hpp:135
  • 第一个参数分别是 X 和 Y 方向中的方格数量。
  • 第二个和第三个参数分别是方格的长度和标记的长度。它们可以用任意单位提供,记住该棋盘的估计位姿将以相同的单位进行测量(通常使用米)。
  • 最后,提供标记的字典。

每个标记的 ID 默认按升序分配,并从 0 开始,如 cv::aruco::GridBoard 构造函数。可以通过通过 board.ids 访问 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);
_Tp width
宽度
定义 types.hpp:362
  • 第一个参数是输出图像以像素为单位的大小。如果这与板尺寸不成比例,则它将居中于该图像。
  • 第二个参数是带有 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("无效的相机文件\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 - 已检测标记的 ID 输入/输出矢量

如果 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