本教程的目标是学习如何创建标定板。

1.方法(一)利用第三方在线生成

https://calib.io/pages/camera-calibration-pattern-generator
可以根据所需定制标定板,并下载一个可打印的PDF文件。
**注意:**在标准喷墨打印机或激光打印机上打印时,请确保您的软件或打印机不应用任何缩放模式。还要确保在打印机驱动程序中没有执行光栅化。最好是在打印后手动测量最终图案。

2.方法(二)OpenCV实现标定板生成

2.1使用OpenCV提供的标定板

OpenCV提供的棋盘标定板: https://github.com/opencv/opencv/blob/master/doc/pattern.png
OpenCV提供的圆形标定板: https://github.com/opencv/opencv/blob/master/doc/acircles_pattern.png

2.2创建自己的标定板

现在,如果你想创建你自己的标定板,你将需要python来使用https://github.com/opencv/opencv/blob/master/doc/pattern_tools/gen_pattern.py
例子:
(1)在文件chess .svg中创建一个96列大小为20mm的棋盘标定板:

python gen_pattern.py -o chessboard.svg --rows 9 --columns 6 --type checkerboard --square_size 20

(2)在circleboard.svg文件中创建一个75列,半径为15mm的圆形标定板:

python gen_pattern.py -o circleboard.svg --rows 7 --columns 5 --type circles --square_size 15

(3)在文件acircleboard.svg中创建一个75列的非对称圆形标定板,正方形大小为10mm,圆之间的间距更小:

python gen_pattern.py -o acircleboard.svg --rows 7 --columns 5 --type acircles --square_size 10 --radius_rate 2

(4)在(7 4),(7 5),(8 5)单元格中为findChessboardCornersSB()创建一个氡棋盘:

python gen_pattern.py -o radon_checkerboard.svg --rows 10 --columns 15 --type radon_checkerboard -s 12.1 -m 7 4 7 5 8 5


这个没用过,不了解。

如果你想改变单位使用-u选项(mm、inches、px、m)
如果你想改变页面大小,使用-w-h选项
**注意:**在标准喷墨打印机或激光打印机上打印时,请确保您的软件或打印机不应用任何缩放模式。还要确保在打印机驱动程序中没有执行光栅化。最好是在打印后手动测量最终图案。

2.3创建一个ChArUco标定板

ArUco 标定板因其快速检测和多功能性而非常有用。然而,ArUco 标记的问题之一是,即使在应用亚像素细化之后,它们的角位置精度也不会太高。

相反,棋盘标定板的角可以更精确地细化,因为每个角都被两个黑色方块包围。然而,找到一个棋盘图案并不像找到一个ArUco板那样多用途:它必须是完全可见的,遮挡是不允许的。

ChArUco将这两种方法的好处结合起来:

当高精度是必要的时候,如在相机标定,Charuco板是一个比标准Aruco板更好的选择。

2.3.1源代码

你可以在opencv_contrib/modules/aruco/samples/tutorial_charuco_create_detect.cpp中找到这段代码。

#include <opencv2/aruco/charuco.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>
#include <string>
namespace {const char* about = "A tutorial code on charuco board creation and detection of charuco board with and without camera caliberation";
const char* keys = "{c        |       | Put value of c=1 to create charuco board;\nc=2 to detect charuco board without camera calibration;\nc=3 to detect charuco board with camera calibration and Pose Estimation}";
}
void createBoard();
void detectCharucoBoardWithCalibrationPose();
void detectCharucoBoardWithoutCalibration();
static bool readCameraParameters(std::string filename, cv::Mat& camMatrix, cv::Mat& distCoeffs)
{cv::FileStorage fs(filename, cv::FileStorage::READ);if (!fs.isOpened())return false;fs["camera_matrix"] >> camMatrix;fs["distortion_coefficients"] >> distCoeffs;return (camMatrix.size() == cv::Size(3,3)) ;
}
void createBoard()
{cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(5, 7, 0.04f, 0.02f, dictionary);cv::Mat boardImage;board->draw(cv::Size(600, 500), boardImage, 10, 1);cv::imwrite("BoardImage.jpg", boardImage);
}
void detectCharucoBoardWithCalibrationPose()
{cv::VideoCapture inputVideo;inputVideo.open(0);cv::Mat cameraMatrix, distCoeffs;std::string filename = "calib.txt";bool readOk = readCameraParameters(filename, cameraMatrix, distCoeffs);if (!readOk) {std::cerr << "Invalid camera file" << std::endl;} else {cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(5, 7, 0.04f, 0.02f, dictionary);cv::Ptr<cv::aruco::DetectorParameters> params = cv::aruco::DetectorParameters::create();while (inputVideo.grab()) {cv::Mat image;cv::Mat imageCopy;inputVideo.retrieve(image);image.copyTo(imageCopy);std::vector<int> markerIds;std::vector<std::vector<cv::Point2f> > markerCorners;cv::aruco::detectMarkers(image, board->dictionary, markerCorners, markerIds, params);// if at least one marker detectedif (markerIds.size() > 0) {cv::aruco::drawDetectedMarkers(imageCopy, markerCorners, markerIds);std::vector<cv::Point2f> charucoCorners;std::vector<int> charucoIds;cv::aruco::interpolateCornersCharuco(markerCorners, markerIds, image, board, charucoCorners, charucoIds, cameraMatrix, distCoeffs);// if at least one charuco corner detectedif (charucoIds.size() > 0) {cv::Scalar color = cv::Scalar(255, 0, 0);cv::aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, color);cv::Vec3d rvec, tvec;// cv::aruco::estimatePoseCharucoBoard(charucoCorners, charucoIds, board, cameraMatrix, distCoeffs, rvec, tvec);bool valid = cv::aruco::estimatePoseCharucoBoard(charucoCorners, charucoIds, board, cameraMatrix, distCoeffs, rvec, tvec);// if charuco pose is validif (valid)cv::aruco::drawAxis(imageCopy, cameraMatrix, distCoeffs, rvec, tvec, 0.1f);}}cv::imshow("out", imageCopy);char key = (char)cv::waitKey(30);if (key == 27)break;}}
}
void detectCharucoBoardWithoutCalibration()
{cv::VideoCapture inputVideo;inputVideo.open(0);cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(5, 7, 0.04f, 0.02f, dictionary);cv::Ptr<cv::aruco::DetectorParameters> params = cv::aruco::DetectorParameters::create();params->cornerRefinementMethod = cv::aruco::CORNER_REFINE_NONE;while (inputVideo.grab()) {cv::Mat image, imageCopy;inputVideo.retrieve(image);image.copyTo(imageCopy);std::vector<int> markerIds;std::vector<std::vector<cv::Point2f> > markerCorners;cv::aruco::detectMarkers(image, board->dictionary, markerCorners, markerIds, params);//or//cv::aruco::detectMarkers(image, dictionary, markerCorners, markerIds, params);// if at least one marker detectedif (markerIds.size() > 0) {cv::aruco::drawDetectedMarkers(imageCopy, markerCorners, markerIds);std::vector<cv::Point2f> charucoCorners;std::vector<int> charucoIds;cv::aruco::interpolateCornersCharuco(markerCorners, markerIds, image, board, charucoCorners, charucoIds);// if at least one charuco corner detectedif (charucoIds.size() > 0)cv::aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, cv::Scalar(255, 0, 0));}cv::imshow("out", imageCopy);char key = (char)cv::waitKey(30);if (key == 27)break;}
}
int main(int argc, char* argv[])
{cv::CommandLineParser parser(argc, argv, keys);parser.about(about);if (argc < 2) {parser.printMessage();return 0;}int choose = parser.get<int>("c");switch (choose) {case 1:createBoard();std::cout << "An image named BoardImg.jpg is generated in folder containing this file" << std::endl;break;case 2:detectCharucoBoardWithoutCalibration();break;case 3:detectCharucoBoardWithCalibrationPose();break;default:break;}return 0;
}

2.3.2创建ChArUco标定板

aruco模块提供cv::aruco::CharucoBoard类,它代表一个CharucoBoard,并继承自Board类。
这个类,和ChArUco的其他函数一样,定义在:#include <opencv2/aruco/charuco.hpp>
要定义CharucoBoard,需要:

  • X方向的棋盘方格数。
  • Y方向的棋盘方格数。
  • 方格边长。
  • 标记边长。
  • 标记字典
  • 所有标记的id。

对于GridBoard对象,aruco模块提供了一个轻松创建charucoboard的函数。这个函数是静态函数cv::aruco::CharucoBoard::create()。 cv::aruco::CharucoBoard board = cv::aruco::CharucoBoard::create(5, 7, 0.04, 0.02, dictionary);

  • 第一个和第二个参数分别是X和Y方向上的棋盘方格数。
  • 第三和第四个参数分别是方格和标记的长度。它们可以以任何单位提供,记住这块标定板的估计姿态将以相同的单位测量(通常使用米)。
  • 最后,给出了标记的字典。

默认情况下,每个标记的id按升序分配,从0开始,就像在GridBoard::create()中一样。这可以很容易地通过访问board.ids来定制,就像在Board的父类中一样。

一旦我们有了CharucoBoard对象,我们可以创建一个图像来打印它。这可以通过CharucoBoard::draw()方法完成:

cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(5, 7, 0.04f, 0.02f, dictionary);cv::Mat boardImage;board->draw(cv::Size(600, 500), boardImage, 10, 1);
  • 第一个参数是输出图像的像素大小。在本例中是600x500像素。如果这不是成比例的标定板尺寸,它将在图像的中心。
  • boardImage:标定板输出图像
  • 第三个参数是以像素为单位的(可选的)边缘,因此没有任何标记触及图像边界。在本例中,边距为10。
  • 最后,标记边框的大小,类似于drawMarker()函数。缺省值为1。

输出图像将像这样:

一个完整的工作示例包含在modules/aruco/samples/create_board_charuco.cpp中。

// create_board_charuco.cpp
#include <opencv2/highgui.hpp>
#include <opencv2/aruco/charuco.hpp>using namespace cv;namespace {const char* about = "Create a ChArUco board image";
const char* keys  ="{@outfile |<none> | Output image }""{w        |       | Number of squares in X direction }""{h        |       | Number of squares in Y direction }""{sl       |       | Square side length (in pixels) }""{ml       |       | Marker side length (in pixels) }""{d        |       | dictionary: 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}""{m        |       | Margins size (in pixels). Default is (squareLength-markerLength) }""{bb       | 1     | Number of bits in marker borders }""{si       | false | show generated image }";
}int main(int argc, char *argv[]) {CommandLineParser parser(argc, argv, keys);parser.about(about);if(argc < 7) {parser.printMessage();return 0;}int squaresX = parser.get<int>("w");int squaresY = parser.get<int>("h");int squareLength = parser.get<int>("sl");int markerLength = parser.get<int>("ml");int dictionaryId = parser.get<int>("d");int margins = squareLength - markerLength;if(parser.has("m")) {margins = parser.get<int>("m");}int borderBits = parser.get<int>("bb");bool showImage = parser.get<bool>("si");String out = parser.get<String>(0);if(!parser.check()) {parser.printErrors();return 0;}Ptr<aruco::Dictionary> dictionary =aruco::getPredefinedDictionary(aruco::PREDEFINED_DICTIONARY_NAME(dictionaryId));Size imageSize;imageSize.width = squaresX * squareLength + 2 * margins;imageSize.height = squaresY * squareLength + 2 * margins;Ptr<aruco::CharucoBoard> board = aruco::CharucoBoard::create(squaresX, squaresY, (float)squareLength,(float)markerLength, dictionary);// show created boardMat boardImage;board->draw(imageSize, boardImage, margins, borderBits);if(showImage) {imshow("board", boardImage);waitKey(0);}imwrite(out, boardImage);return 0;
}

注意:create_board_charuco.cpp现在通过OpenCV Commandline Parser接收输入。对于该文件,示例参数如下所示"output_path/chboard.png" -w=5 -h=7 -sl=200 -ml=120 -d=10

// 纸张
page_sizes = {"A0": [840, 1188], "A1": [594, 840], "A2": [420, 594], "A3": [297, 420], "A4": [210, 297], "A5": [148, 210]}

2.3.3ChArUco板检测

当你检测一个ChArUco标定板时,你实际上检测的是标定板上的每个角点。

ChArUco标定板的每个角点都分配了一个唯一的标识符(id)。这些id从0到标定上角点的总数。charuco标定板检测的步骤可以分解为以下步骤:

  • 输入图像
cv::Mat image;

用于检测标记的原始图像。图像在执行亚像素细化ChArUco角点的时候是必要的。

  • 读取摄像机标定参数(仅用于带有摄像机标定的ChArUco标定板检测)
cv::Mat cameraMatrix, distCoeffs;
std::string filename = "calib.txt";
bool readOk = readCameraParameters(filename, cameraMatrix, distCoeffs);

readCameraParameters的参数有:
(1)filename - 这是caliberation.txt 文件的路径,它是calibrate_camera_charuco.cpp生成的输出文件
(2)cameraMatrixdistCoeffs-可选的相机标定参数
该函数将这些参数作为输入,并返回一个布尔值,表示摄像机标定参数是否有效。对于没有标定的角点检测,不需要此步骤。

  • 检测标记
cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);
cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(5, 7, 0.04f, 0.02f, dictionary);
cv::Ptr<cv::aruco::DetectorParameters> params = cv::aruco::DetectorParameters::create();
std::vector<int> markerIds;
std::vector<std::vector<cv::Point2f> > markerCorners;
cv::aruco::detectMarkers(image, board->dictionary, markerCorners, markerIds, params);

detectmarker的参数为:
(1)image :输入图像
(2)dictionary: 指向将要搜索的字典/标记集的指针。
(3)markerCorners: 检测到的标记角vector。
(4)markerIds :检测到的标记的标识vector
(5)params :标记检测参数 ChArUco角的检测是基于先前检测到的标记。这样,首先检测到标记,然后从标记中插入ChArUco角。

有标定的检测:

std::vector<cv::Point2f> charucoCorners;
std::vector<int> charucoIds;
cv::aruco::interpolateCornersCharuco(markerCorners, markerIds, image, board, charucoCorners, charucoIds, cameraMatrix, distCoeffs);

没有标定的检测

std::vector<cv::Point2f> charucoCorners;
std::vector<int> charucoIds;
cv::aruco::interpolateCornersCharuco(markerCorners, markerIds, image, board, charucoCorners, charucoIds);

检测ChArUco角的函数是cv::aruco::interpolateCornersCharuco()。这个函数返回插值后Charuco角的数量。
(1)std::vector<cv::Point2f> charucoCorners:检测到的角坐标列表
(2)std::vector<int> charucoIds:与charucoCorners相对应的ids

如果提供了标定参数,则通过以下方法对ChArUco角进行插值:首先,从ArUco标记中估计一个粗略的姿态,然后,将ChArUco角重新投影到图像中。

另一方面,在不提供标定参数的情况下,通过计算ChArUco平面与ChArUco图像投影之间对应的单应性来插值ChArUco角。

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

当检测ChArUco板的标记,特别是当使用单应性插值时,建议禁用标记的角亚像素细化。究其原因,由于棋盘方格的邻近性,亚像素过程会在边角位置产生重要的偏差,而这些偏差会传播到ChArUco边角插值中,产生较差的结果。

此外,只会返回那些周围有两个标记的角。如果没有检测到周围两个标记中的角,这通常意味着有一些遮挡或图像质量不好的区域。在任何情况下,最好不要考虑这个角,因为我们想要的是确保插值ChArUco角是非常精确的。

在ChArUco角被插值后,亚像素细化被执行。

一旦我们插入了ChArUco角,我们可能会想把它们画出来,看看它们的检测是否正确。使用drawDetectedCornersCharuco()函数实现:

cv::aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, color);

(1)imageCopy将绘制角的图像(它通常与检测角的图像相同)。
(2)outputImage将是绘制角的inputImage的副本。
(3)charucoCornerscharucoIds是通过interpolateCornersCharuco()函数检测到的Charuco角。
(4)最后一个参数是我们想要绘制角的(可选的)颜色,类型为cv::Scalar


在遮挡的情况下。如下图所示,虽然一些角落清晰可见,但由于遮挡,并不是所有它们周围的标记都被检测到,因此,它们没有被插值:

一个完整的工作示例包含在modules/aruco/samples/detect_board_charuco.cpp中。

// detect_board_charuco.cpp
#include <opencv2/highgui.hpp>
#include <opencv2/aruco/charuco.hpp>
#include <vector>
#include <iostream>using namespace std;
using namespace cv;namespace {const char* about = "Pose estimation using a ChArUco board";
const char* keys  ="{w        |       | Number of squares in X direction }""{h        |       | Number of squares in Y direction }""{sl       |       | Square side length (in meters) }""{ml       |       | Marker side length (in meters) }""{d        |       | dictionary: 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}""{c        |       | Output file with calibrated camera parameters }""{v        |       | Input from video file, if ommited, input comes from camera }""{ci       | 0     | Camera id if input doesnt come from video (-v) }""{dp       |       | File of marker detector parameters }""{rs       |       | Apply refind strategy }""{r        |       | show rejected candidates too }";
}/***/
static bool readCameraParameters(string filename, Mat &camMatrix, Mat &distCoeffs) {FileStorage fs(filename, FileStorage::READ);if(!fs.isOpened())return false;fs["camera_matrix"] >> camMatrix;fs["distortion_coefficients"] >> distCoeffs;return true;
}/***/
static bool readDetectorParameters(string filename, Ptr<aruco::DetectorParameters> &params) {FileStorage fs(filename, FileStorage::READ);if(!fs.isOpened())return false;fs["adaptiveThreshWinSizeMin"] >> params->adaptiveThreshWinSizeMin;fs["adaptiveThreshWinSizeMax"] >> params->adaptiveThreshWinSizeMax;fs["adaptiveThreshWinSizeStep"] >> params->adaptiveThreshWinSizeStep;fs["adaptiveThreshConstant"] >> params->adaptiveThreshConstant;fs["minMarkerPerimeterRate"] >> params->minMarkerPerimeterRate;fs["maxMarkerPerimeterRate"] >> params->maxMarkerPerimeterRate;fs["polygonalApproxAccuracyRate"] >> params->polygonalApproxAccuracyRate;fs["minCornerDistanceRate"] >> params->minCornerDistanceRate;fs["minDistanceToBorder"] >> params->minDistanceToBorder;fs["minMarkerDistanceRate"] >> params->minMarkerDistanceRate;fs["cornerRefinementMethod"] >> params->cornerRefinementMethod;fs["cornerRefinementWinSize"] >> params->cornerRefinementWinSize;fs["cornerRefinementMaxIterations"] >> params->cornerRefinementMaxIterations;fs["cornerRefinementMinAccuracy"] >> params->cornerRefinementMinAccuracy;fs["markerBorderBits"] >> params->markerBorderBits;fs["perspectiveRemovePixelPerCell"] >> params->perspectiveRemovePixelPerCell;fs["perspectiveRemoveIgnoredMarginPerCell"] >> params->perspectiveRemoveIgnoredMarginPerCell;fs["maxErroneousBitsInBorderRate"] >> params->maxErroneousBitsInBorderRate;fs["minOtsuStdDev"] >> params->minOtsuStdDev;fs["errorCorrectionRate"] >> params->errorCorrectionRate;return true;
}/***/
int main(int argc, char *argv[]) {CommandLineParser parser(argc, argv, keys);parser.about(about);if(argc < 6) {parser.printMessage();return 0;}int squaresX = parser.get<int>("w");int squaresY = parser.get<int>("h");float squareLength = parser.get<float>("sl");float markerLength = parser.get<float>("ml");int dictionaryId = parser.get<int>("d");bool showRejected = parser.has("r");bool refindStrategy = parser.has("rs");int camId = parser.get<int>("ci");String video;if(parser.has("v")) {video = parser.get<String>("v");}Mat camMatrix, distCoeffs;if(parser.has("c")) {bool readOk = readCameraParameters(parser.get<string>("c"), camMatrix, distCoeffs);if(!readOk) {cerr << "Invalid camera file" << endl;return 0;}}Ptr<aruco::DetectorParameters> detectorParams = aruco::DetectorParameters::create();if(parser.has("dp")) {bool readOk = readDetectorParameters(parser.get<string>("dp"), detectorParams);if(!readOk) {cerr << "Invalid detector parameters file" << endl;return 0;}}if(!parser.check()) {parser.printErrors();return 0;}Ptr<aruco::Dictionary> dictionary =aruco::getPredefinedDictionary(aruco::PREDEFINED_DICTIONARY_NAME(dictionaryId));VideoCapture inputVideo;int waitTime;if(!video.empty()) {inputVideo.open(video);waitTime = 0;} else {inputVideo.open(camId);waitTime = 10;}float axisLength = 0.5f * ((float)min(squaresX, squaresY) * (squareLength));// create charuco board objectPtr<aruco::CharucoBoard> charucoboard =aruco::CharucoBoard::create(squaresX, squaresY, squareLength, markerLength, dictionary);Ptr<aruco::Board> board = charucoboard.staticCast<aruco::Board>();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, rejectedMarkers;vector< Point2f > charucoCorners;Vec3d rvec, tvec;// detect markersaruco::detectMarkers(image, dictionary, markerCorners, markerIds, detectorParams,rejectedMarkers);// refind strategy to detect more markersif(refindStrategy)aruco::refineDetectedMarkers(image, board, markerCorners, markerIds, rejectedMarkers,camMatrix, distCoeffs);// interpolate charuco cornersint interpolatedCorners = 0;if(markerIds.size() > 0)interpolatedCorners =aruco::interpolateCornersCharuco(markerCorners, markerIds, image, charucoboard,charucoCorners, charucoIds, camMatrix, distCoeffs);// estimate charuco board posebool validPose = false;if(camMatrix.total() != 0)validPose = aruco::estimatePoseCharucoBoard(charucoCorners, charucoIds, charucoboard,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;}// draw resultsimage.copyTo(imageCopy);if(markerIds.size() > 0) {aruco::drawDetectedMarkers(imageCopy, markerCorners);}if(showRejected && rejectedMarkers.size() > 0)aruco::drawDetectedMarkers(imageCopy, rejectedMarkers, noArray(), Scalar(100, 0, 255));if(interpolatedCorners > 0) {Scalar color;color = Scalar(255, 0, 0);aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, color);}if(validPose)aruco::drawAxis(imageCopy, camMatrix, distCoeffs, rvec, tvec, axisLength);imshow("out", imageCopy);char key = (char)waitKey(waitTime);if(key == 27) break;}return 0;
}

注意:detect_board_charuco.cpp现在通过OpenCV Commandline Parser接收输入。对于该文件,示例参数如下所示-c="_path_/calib.txt" -dp="_path_/detector_params.yml" -w=5 -h=7 -sl=0.04 -ml=0.02 -d=10

detector_params.yml文件内容:

%YAML:1.0
nmarkers: 1024
adaptiveThreshWinSizeMin: 3
adaptiveThreshWinSizeMax: 23
adaptiveThreshWinSizeStep: 10
adaptiveThreshWinSize: 21
adaptiveThreshConstant: 7
minMarkerPerimeterRate: 0.03
maxMarkerPerimeterRate: 4.0
polygonalApproxAccuracyRate: 0.05
minCornerDistanceRate: 0.05
minDistanceToBorder: 3
minMarkerDistance: 10.0
minMarkerDistanceRate: 0.05
cornerRefinementMethod: 0
cornerRefinementWinSize: 5
cornerRefinementMaxIterations: 30
cornerRefinementMinAccuracy: 0.1
markerBorderBits: 1
perspectiveRemovePixelPerCell: 8
perspectiveRemoveIgnoredMarginPerCell: 0.13
maxErroneousBitsInBorderRate: 0.04
minOtsuStdDev: 5.0
errorCorrectionRate: 0.6

这里的calib.txt是由calibrate_camera_charuco.cpp生成的输出文件。

// calibrate_camera_charuco.cpp
#include <opencv2/highgui.hpp>
#include <opencv2/calib3d.hpp>
#include <opencv2/aruco/charuco.hpp>
#include <opencv2/imgproc.hpp>
#include <vector>
#include <iostream>
#include <ctime>using namespace std;
using namespace cv;namespace {const char* about ="Calibration using a ChArUco board\n""  To capture a frame for calibration, press 'c',\n""  If input comes from video, press any key for next frame\n""  To finish capturing, press 'ESC' key and calibration starts.\n";
const char* keys  ="{w        |       | Number of squares in X direction }""{h        |       | Number of squares in Y direction }""{sl       |       | Square side length (in meters) }""{ml       |       | Marker side length (in meters) }""{d        |       | dictionary: 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}""{@outfile |<none> | Output file with calibrated camera parameters }""{v        |       | Input from video file, if ommited, input comes from camera }""{ci       | 0     | Camera id if input doesnt come from video (-v) }""{dp       |       | File of marker detector parameters }""{rs       | false | Apply refind strategy }""{zt       | false | Assume zero tangential distortion }""{a        |       | Fix aspect ratio (fx/fy) to this value }""{pc       | false | Fix the principal point at the center }""{sc       | false | Show detected chessboard corners after calibration }";
}/***/
static bool readDetectorParameters(string filename, Ptr<aruco::DetectorParameters> &params) {FileStorage fs(filename, FileStorage::READ);if(!fs.isOpened())return false;fs["adaptiveThreshWinSizeMin"] >> params->adaptiveThreshWinSizeMin;fs["adaptiveThreshWinSizeMax"] >> params->adaptiveThreshWinSizeMax;fs["adaptiveThreshWinSizeStep"] >> params->adaptiveThreshWinSizeStep;fs["adaptiveThreshConstant"] >> params->adaptiveThreshConstant;fs["minMarkerPerimeterRate"] >> params->minMarkerPerimeterRate;fs["maxMarkerPerimeterRate"] >> params->maxMarkerPerimeterRate;fs["polygonalApproxAccuracyRate"] >> params->polygonalApproxAccuracyRate;fs["minCornerDistanceRate"] >> params->minCornerDistanceRate;fs["minDistanceToBorder"] >> params->minDistanceToBorder;fs["minMarkerDistanceRate"] >> params->minMarkerDistanceRate;fs["cornerRefinementMethod"] >> params->cornerRefinementMethod;fs["cornerRefinementWinSize"] >> params->cornerRefinementWinSize;fs["cornerRefinementMaxIterations"] >> params->cornerRefinementMaxIterations;fs["cornerRefinementMinAccuracy"] >> params->cornerRefinementMinAccuracy;fs["markerBorderBits"] >> params->markerBorderBits;fs["perspectiveRemovePixelPerCell"] >> params->perspectiveRemovePixelPerCell;fs["perspectiveRemoveIgnoredMarginPerCell"] >> params->perspectiveRemoveIgnoredMarginPerCell;fs["maxErroneousBitsInBorderRate"] >> params->maxErroneousBitsInBorderRate;fs["minOtsuStdDev"] >> params->minOtsuStdDev;fs["errorCorrectionRate"] >> params->errorCorrectionRate;return true;
}/***/
static bool saveCameraParams(const string &filename, Size imageSize, float aspectRatio, int flags,const Mat &cameraMatrix, const Mat &distCoeffs, double totalAvgErr) {FileStorage fs(filename, FileStorage::WRITE);if(!fs.isOpened())return false;time_t tt;time(&tt);struct tm *t2 = localtime(&tt);char buf[1024];strftime(buf, sizeof(buf) - 1, "%c", t2);fs << "calibration_time" << buf;fs << "image_width" << imageSize.width;fs << "image_height" << imageSize.height;if(flags & CALIB_FIX_ASPECT_RATIO) fs << "aspectRatio" << aspectRatio;if(flags != 0) {sprintf(buf, "flags: %s%s%s%s",flags & CALIB_USE_INTRINSIC_GUESS ? "+use_intrinsic_guess" : "",flags & CALIB_FIX_ASPECT_RATIO ? "+fix_aspectRatio" : "",flags & CALIB_FIX_PRINCIPAL_POINT ? "+fix_principal_point" : "",flags & CALIB_ZERO_TANGENT_DIST ? "+zero_tangent_dist" : "");}fs << "flags" << flags;fs << "camera_matrix" << cameraMatrix;fs << "distortion_coefficients" << distCoeffs;fs << "avg_reprojection_error" << totalAvgErr;return true;
}/***/
int main(int argc, char *argv[]) {CommandLineParser parser(argc, argv, keys);parser.about(about);if(argc < 7) {parser.printMessage();return 0;}int squaresX = parser.get<int>("w");int squaresY = parser.get<int>("h");float squareLength = parser.get<float>("sl");float markerLength = parser.get<float>("ml");int dictionaryId = parser.get<int>("d");string outputFile = parser.get<string>(0);bool showChessboardCorners = parser.get<bool>("sc");int calibrationFlags = 0;float aspectRatio = 1;if(parser.has("a")) {calibrationFlags |= CALIB_FIX_ASPECT_RATIO;aspectRatio = parser.get<float>("a");}if(parser.get<bool>("zt")) calibrationFlags |= CALIB_ZERO_TANGENT_DIST;if(parser.get<bool>("pc")) calibrationFlags |= CALIB_FIX_PRINCIPAL_POINT;Ptr<aruco::DetectorParameters> detectorParams = aruco::DetectorParameters::create();if(parser.has("dp")) {bool readOk = readDetectorParameters(parser.get<string>("dp"), detectorParams);if(!readOk) {cerr << "Invalid detector parameters file" << endl;return 0;}}bool refindStrategy = parser.get<bool>("rs");int camId = parser.get<int>("ci");String video;if(parser.has("v")) {video = parser.get<String>("v");}if(!parser.check()) {parser.printErrors();return 0;}VideoCapture inputVideo;int waitTime;if(!video.empty()) {inputVideo.open(video);waitTime = 0;} else {inputVideo.open(camId);waitTime = 10;}Ptr<aruco::Dictionary> dictionary =aruco::getPredefinedDictionary(aruco::PREDEFINED_DICTIONARY_NAME(dictionaryId));// create charuco board objectPtr<aruco::CharucoBoard> charucoboard =aruco::CharucoBoard::create(squaresX, squaresY, squareLength, markerLength, dictionary);Ptr<aruco::Board> board = charucoboard.staticCast<aruco::Board>();// collect data from each framevector< vector< vector< Point2f > > > allCorners;vector< vector< int > > allIds;vector< Mat > allImgs;Size imgSize;while(inputVideo.grab()) {Mat image, imageCopy;inputVideo.retrieve(image);vector< int > ids;vector< vector< Point2f > > corners, rejected;// detect markersaruco::detectMarkers(image, dictionary, corners, ids, detectorParams, rejected);// refind strategy to detect more markersif(refindStrategy) aruco::refineDetectedMarkers(image, board, corners, ids, rejected);// interpolate charuco cornersMat currentCharucoCorners, currentCharucoIds;if(ids.size() > 0)aruco::interpolateCornersCharuco(corners, ids, image, charucoboard, currentCharucoCorners,currentCharucoIds);// draw resultsimage.copyTo(imageCopy);if(ids.size() > 0) aruco::drawDetectedMarkers(imageCopy, corners);if(currentCharucoCorners.total() > 0)aruco::drawDetectedCornersCharuco(imageCopy, currentCharucoCorners, currentCharucoIds);putText(imageCopy, "Press 'c' to add current frame. 'ESC' to finish and calibrate",Point(10, 20), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(255, 0, 0), 2);imshow("out", imageCopy);char key = (char)waitKey(waitTime);if(key == 27) break;if(key == 'c' && ids.size() > 0) {cout << "Frame captured" << endl;allCorners.push_back(corners);allIds.push_back(ids);allImgs.push_back(image);imgSize = image.size();}}if(allIds.size() < 1) {cerr << "Not enough captures for calibration" << endl;return 0;}Mat cameraMatrix, distCoeffs;vector< Mat > rvecs, tvecs;double repError;if(calibrationFlags & CALIB_FIX_ASPECT_RATIO) {cameraMatrix = Mat::eye(3, 3, CV_64F);cameraMatrix.at< double >(0, 0) = aspectRatio;}// prepare data for calibrationvector< vector< Point2f > > allCornersConcatenated;vector< int > allIdsConcatenated;vector< int > markerCounterPerFrame;markerCounterPerFrame.reserve(allCorners.size());for(unsigned int i = 0; i < allCorners.size(); i++) {markerCounterPerFrame.push_back((int)allCorners[i].size());for(unsigned int j = 0; j < allCorners[i].size(); j++) {allCornersConcatenated.push_back(allCorners[i][j]);allIdsConcatenated.push_back(allIds[i][j]);}}// calibrate camera using aruco markersdouble arucoRepErr;arucoRepErr = aruco::calibrateCameraAruco(allCornersConcatenated, allIdsConcatenated,markerCounterPerFrame, board, imgSize, cameraMatrix,distCoeffs, noArray(), noArray(), calibrationFlags);// prepare data for charuco calibrationint nFrames = (int)allCorners.size();vector< Mat > allCharucoCorners;vector< Mat > allCharucoIds;vector< Mat > filteredImages;allCharucoCorners.reserve(nFrames);allCharucoIds.reserve(nFrames);for(int i = 0; i < nFrames; i++) {// interpolate using camera parametersMat currentCharucoCorners, currentCharucoIds;aruco::interpolateCornersCharuco(allCorners[i], allIds[i], allImgs[i], charucoboard,currentCharucoCorners, currentCharucoIds, cameraMatrix,distCoeffs);allCharucoCorners.push_back(currentCharucoCorners);allCharucoIds.push_back(currentCharucoIds);filteredImages.push_back(allImgs[i]);}if(allCharucoCorners.size() < 4) {cerr << "Not enough corners for calibration" << endl;return 0;}// calibrate camera using charucorepError =aruco::calibrateCameraCharuco(allCharucoCorners, allCharucoIds, charucoboard, imgSize,cameraMatrix, distCoeffs, rvecs, tvecs, calibrationFlags);bool saveOk =  saveCameraParams(outputFile, imgSize, aspectRatio, calibrationFlags,cameraMatrix, distCoeffs, repError);if(!saveOk) {cerr << "Cannot save output file" << endl;return 0;}cout << "Rep Error: " << repError << endl;cout << "Rep Error Aruco: " << arucoRepErr << endl;cout << "Calibration saved to " << outputFile << endl;// show interpolated charuco corners for debuggingif(showChessboardCorners) {for(unsigned int frame = 0; frame < filteredImages.size(); frame++) {Mat imageCopy = filteredImages[frame].clone();if(allIds[frame].size() > 0) {if(allCharucoCorners[frame].total() > 0) {aruco::drawDetectedCornersCharuco( imageCopy, allCharucoCorners[frame],allCharucoIds[frame]);}}imshow("out", imageCopy);char key = (char)waitKey(0);if(key == 27) break;}}return 0;
}

2.3.4 ChArUco姿势估计

ChArUco标定板的最终目标是找到非常准确的角以便高精度标定或姿态估计。

aruco模块提供了一个函数,可以方便地执行ChArUco姿态估计。与在GridBoard中一样,CharucoBoard的坐标系统被放置在标定板平面中,Z轴指向外,并在标定板的左下角居中。

姿态估计的函数是estimatePoseCharucoBoard():

cv::aruco::estimatePoseCharucoBoard(charucoCorners, charucoIds, board, cameraMatrix, distCoeffs, rvec, tvec);
  • charucoCorners 和charucoIds参数是通过interpolateCornersCharuco()函数检测到的charuco角。
  • 第三个参数是CharucoBoard对象。
  • cameraMatrix 和distCoeffs是相机标定参数,是姿态估计的必要参数。
  • 最后,rvec和tvec参数是Charuco板的输出位姿。
  • 如果姿势被正确估计,函数返回true,否则返回false。失败的主要原因是没有足够的角来进行姿态估计或者它们在同一条线上。

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

一个完整的ChArUco检测与姿态估计的例子:

void detectCharucoBoardWithCalibrationPose()
{cv::VideoCapture inputVideo;inputVideo.open(0);cv::Mat cameraMatrix, distCoeffs;std::string filename = "calib.txt";bool readOk = readCameraParameters(filename, cameraMatrix, distCoeffs);if (!readOk) {std::cerr << "Invalid camera file" << std::endl;} else {cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(5, 7, 0.04f, 0.02f, dictionary);cv::Ptr<cv::aruco::DetectorParameters> params = cv::aruco::DetectorParameters::create();while (inputVideo.grab()) {cv::Mat image;cv::Mat imageCopy;inputVideo.retrieve(image);image.copyTo(imageCopy);std::vector<int> markerIds;std::vector<std::vector<cv::Point2f> > markerCorners;cv::aruco::detectMarkers(image, board->dictionary, markerCorners, markerIds, params);// if at least one marker detectedif (markerIds.size() > 0) {cv::aruco::drawDetectedMarkers(imageCopy, markerCorners, markerIds);std::vector<cv::Point2f> charucoCorners;std::vector<int> charucoIds;cv::aruco::interpolateCornersCharuco(markerCorners, markerIds, image, board, charucoCorners, charucoIds, cameraMatrix, distCoeffs);// if at least one charuco corner detectedif (charucoIds.size() > 0) {cv::Scalar color = cv::Scalar(255, 0, 0);cv::aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, color);cv::Vec3d rvec, tvec;// cv::aruco::estimatePoseCharucoBoard(charucoCorners, charucoIds, board, cameraMatrix, distCoeffs, rvec, tvec);bool valid = cv::aruco::estimatePoseCharucoBoard(charucoCorners, charucoIds, board, cameraMatrix, distCoeffs, rvec, tvec);// if charuco pose is validif (valid)cv::aruco::drawAxis(imageCopy, cameraMatrix, distCoeffs, rvec, tvec, 0.1f);}}cv::imshow("out", imageCopy);char key = (char)cv::waitKey(30);if (key == 27)break;}}
}

一个完整的工作示例包含在modules/aruco/samples/detect_board_charuco.cpp中。对于该文件,示例参数如下所示"_path_/calib.txt" -dp="_path_/detector_params.yml" -w=5 -h=7 -sl=0.04 -ml=0.02 -d=10

参考目录

https://docs.opencv.org/4.x/df/d4a/tutorial_charuco_detection.html

相机标定与3D重建(1)创建标定板(上)相关推荐

  1. KinectFusion:用运动的深度相机进行实时3D重建及交互

    KinectFusion的GPU Pipeline实现, 涉及到的两篇原理和实现中的一篇,两年前翻译的,那时候多数不理解,再拿出来整理一下,也是加深理解,配图来自原论文,文字为本人翻译,如有不正,敬请 ...

  2. 相机标定与3D重建(0)标定板说明

    准确标定相机对于任何机器/计算机视觉设置的成功都很重要.但是,有不同的标定板可供选择.为了让您更轻松地进行选择,本文解释了每种方法的主要优点. 标定板尺寸 在选择标定板时,一个重要的考虑因素是它的物理 ...

  3. 十五天掌握OpenCV——摄像机标定和3D重构!—摄像机标定

    魏老师学生--Cecil:学习OpenCV-机器视觉之旅 基础 代码 设置 标定 畸变校正 反向投影误差 代码演示 Aim: 学习摄像机畸变以及摄像机的内部参数和外部参数: 对畸变图像进行修复. 基础 ...

  4. 相机校准和3D重建10-计算基本矩阵2

    根据两个图像中的对应点计算基本矩阵. Mat cv::findFundamentalMat(InputArray points1, InputArray points2, int method, do ...

  5. 双目视觉标定原理详解(张氏标定)

    一.图像坐标:我想和世界坐标谈谈(A) 玉米竭力用轻松具体的描述来讲述双目三维重建中的一些数学问题.希望这样的方式让大家以一个轻松的心态阅读玉米的<计算机视觉学习笔记>双目视觉数学架构系列 ...

  6. opencv三维重建_使用iPhone相机和OpenCV来完成3D重建(第一部分)

    正文字数:1497  阅读时长:2分钟 这个教程将带你使用自己的手机摄像头和图片实现从零开始到点云. Posted by Omar Padiernaurl : https://becominghuma ...

  7. 使用iPhone相机和OpenCV来完成3D重建(第一部分)

    正文字数:1497  阅读时长:2分钟 这个教程将带你使用自己的手机摄像头和图片实现从零开始到点云. Posted by Omar Padierna url : https://becominghum ...

  8. 基础 | 详解3D结构光如何标定

    来源丨马少爷 编辑丨AIoT工业检测 结构光视觉的优点: 非接触.信息量大.测精度高.抗干扰能力强. 结构光视觉传感器参数的标定包括:摄像机参数标定.结构光平面参数标定. 结构光视觉测量原理图 我们不 ...

  9. 双目视觉标定与 3D 坐标测量

    目录 一.研究背景与意义 二.课题内容 (一)双目视觉标定 1.原理 2. 流程 (二)双目视觉测量 1. 对第"4"对图分析 2. 对第"5"对图分析 3. ...

最新文章

  1. orm 通用方法——RunProc调用存储过程
  2. phpstudy免费安全检测服务_@你,您有一份免费安全服务已到账
  3. python第三方库——requests
  4. Mesos框架对比:Marathon 和 Aurora
  5. php中怎么设置透明背景图片,css怎样设置背景透明
  6. 微信公众号如何开通支付功能?
  7. Qt QLabel文本框的使用
  8. noip模拟测试 主仆见证了 Hobo 的离别
  9. 【沙龙干货】Swift是花拳绣腿吗?开发语言与职业生涯如何选择?
  10. 抖音恶心的整人代码~~~VBS代码
  11. 九龙证券|主力出逃大热门互联网股近13亿元!尾盘两股获加仓超亿元
  12. 【R代码 (葡萄酒)及其可视化分析 #随机森林-支持向量机】
  13. ROS SMACH示例教程(三)
  14. 一沓扑克牌中剔除等于13或者相邻之和等于13的扑克牌,剩余多少
  15. Cocos2d-x 3D模型渲染
  16. [附源码]计算机毕业设计Python+uniapp智慧校园APP的设计与实现55q4l(程序+lw+APP+远程部署)
  17. elementUI的消息弹窗组件手动关闭和遮罩层关闭问题
  18. Exadata 的诊断工具之 sundiag.sh
  19. 矩阵光学 matlab,矩阵光学.doc
  20. JMeter jp@gc - PerfMon Metrics Collector插件

热门文章

  1. Cisco AP IOS升级
  2. 20个免费和开源数据可视化工具
  3. hiho#1712 : 字符串排序
  4. 食堂计费系统服务器,食堂消费系统解决方案
  5. i386和X86_64各是什么意思
  6. java课程设计简单计算器_JAVA课程设计--简易计算器(201521123022 黄俊麟)
  7. 亲测好用!免费英语学习版ChatGPT,国内能直接用!(内测名额有限)
  8. 用Python玩百变人脸!趣味容颜
  9. 统计学基础(三)—数据的概率分布与差异检验方法
  10. 安川5系7系伺服 电路 驱动器原理图