1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-29 23:33:05 +04:00

Merge pull request #22986 from AleksandrPanov:move_contrib_charuco_to_main_objdetect

merge with https://github.com/opencv/opencv_contrib/pull/3394

move Charuco API from contrib to main repo:

- add CharucoDetector:
```
CharucoDetector::detectBoard(InputArray image, InputOutputArrayOfArrays markerCorners, InputOutputArray markerIds, 
                             OutputArray charucoCorners, OutputArray charucoIds) const // detect charucoCorners and/or markerCorners
CharucoDetector::detectDiamonds(InputArray image, InputOutputArrayOfArrays _markerCorners,
                                InputOutputArrayOfArrays _markerIds, OutputArrayOfArrays _diamondCorners,
                                OutputArray _diamondIds) const
```

- add `matchImagePoints()` for `CharucoBoard`
- remove contrib aruco dependencies from interactive-calibration tool
- move almost all aruco tests to objdetect

### Pull Request Readiness Checklist

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

- [x] I agree to contribute to the project under Apache 2 License.
- [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
- [x] The PR is proposed to the proper branch
- [x] There is a reference to the original bug report and related work
- [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable
      Patch to opencv_extra has the same branch name.
- [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
Alexander Panov
2022-12-28 17:28:59 +03:00
committed by GitHub
parent 9627ab9462
commit 121034876d
21 changed files with 2721 additions and 499 deletions
+205
View File
@@ -0,0 +1,205 @@
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#include "test_precomp.hpp"
#include "test_aruco_utils.hpp"
namespace opencv_test {
vector<Point2f> getAxis(InputArray _cameraMatrix, InputArray _distCoeffs, InputArray _rvec,
InputArray _tvec, float length, const float offset) {
vector<Point3f> axis;
axis.push_back(Point3f(offset, offset, 0.f));
axis.push_back(Point3f(length+offset, offset, 0.f));
axis.push_back(Point3f(offset, length+offset, 0.f));
axis.push_back(Point3f(offset, offset, length));
vector<Point2f> axis_to_img;
projectPoints(axis, _rvec, _tvec, _cameraMatrix, _distCoeffs, axis_to_img);
return axis_to_img;
}
vector<Point2f> getMarkerById(int id, const vector<vector<Point2f> >& corners, const vector<int>& ids) {
for (size_t i = 0ull; i < ids.size(); i++)
if (ids[i] == id)
return corners[i];
return vector<Point2f>();
}
void getSyntheticRT(double yaw, double pitch, double distance, Mat& rvec, Mat& tvec) {
rvec = Mat::zeros(3, 1, CV_64FC1);
tvec = Mat::zeros(3, 1, CV_64FC1);
// rotate "scene" in pitch axis (x-axis)
Mat rotPitch(3, 1, CV_64FC1);
rotPitch.at<double>(0) = -pitch;
rotPitch.at<double>(1) = 0;
rotPitch.at<double>(2) = 0;
// rotate "scene" in yaw (y-axis)
Mat rotYaw(3, 1, CV_64FC1);
rotYaw.at<double>(0) = 0;
rotYaw.at<double>(1) = yaw;
rotYaw.at<double>(2) = 0;
// compose both rotations
composeRT(rotPitch, Mat(3, 1, CV_64FC1, Scalar::all(0)), rotYaw,
Mat(3, 1, CV_64FC1, Scalar::all(0)), rvec, tvec);
// Tvec, just move in z (camera) direction the specific distance
tvec.at<double>(0) = 0.;
tvec.at<double>(1) = 0.;
tvec.at<double>(2) = distance;
}
void projectMarker(Mat& img, const aruco::Board& board, int markerIndex, Mat cameraMatrix, Mat rvec, Mat tvec,
int markerBorder) {
// canonical image
Mat markerImg;
const int markerSizePixels = 100;
aruco::generateImageMarker(board.getDictionary(), board.getIds()[markerIndex], markerSizePixels, markerImg, markerBorder);
// projected corners
Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0));
vector<Point2f> corners;
// get max coordinate of board
Point3f maxCoord = board.getRightBottomCorner();
// copy objPoints
vector<Point3f> objPoints = board.getObjPoints()[markerIndex];
// move the marker to the origin
for (size_t i = 0; i < objPoints.size(); i++)
objPoints[i] -= (maxCoord / 2.f);
projectPoints(objPoints, rvec, tvec, cameraMatrix, distCoeffs, corners);
// get perspective transform
vector<Point2f> originalCorners;
originalCorners.push_back(Point2f(0, 0));
originalCorners.push_back(Point2f((float)markerSizePixels, 0));
originalCorners.push_back(Point2f((float)markerSizePixels, (float)markerSizePixels));
originalCorners.push_back(Point2f(0, (float)markerSizePixels));
Mat transformation = getPerspectiveTransform(originalCorners, corners);
// apply transformation
Mat aux;
const char borderValue = 127;
warpPerspective(markerImg, aux, transformation, img.size(), INTER_NEAREST, BORDER_CONSTANT,
Scalar::all(borderValue));
// copy only not-border pixels
for (int y = 0; y < aux.rows; y++) {
for (int x = 0; x < aux.cols; x++) {
if (aux.at< unsigned char >(y, x) == borderValue) continue;
img.at< unsigned char >(y, x) = aux.at< unsigned char >(y, x);
}
}
}
Mat projectBoard(const aruco::GridBoard& board, Mat cameraMatrix, double yaw, double pitch, double distance,
Size imageSize, int markerBorder) {
Mat rvec, tvec;
getSyntheticRT(yaw, pitch, distance, rvec, tvec);
Mat img = Mat(imageSize, CV_8UC1, Scalar::all(255));
for (unsigned int index = 0; index < board.getIds().size(); index++)
projectMarker(img, board, index, cameraMatrix, rvec, tvec, markerBorder);
return img;
}
/** Check if a set of 3d points are enough for calibration. Z coordinate is ignored.
* Only axis parallel lines are considered */
static bool _arePointsEnoughForPoseEstimation(const std::vector<Point3f> &points) {
if(points.size() < 4) return false;
std::vector<double> sameXValue; // different x values in points
std::vector<int> sameXCounter; // number of points with the x value in sameXValue
for(unsigned int i = 0; i < points.size(); i++) {
bool found = false;
for(unsigned int j = 0; j < sameXValue.size(); j++) {
if(sameXValue[j] == points[i].x) {
found = true;
sameXCounter[j]++;
}
}
if(!found) {
sameXValue.push_back(points[i].x);
sameXCounter.push_back(1);
}
}
// count how many x values has more than 2 points
int moreThan2 = 0;
for(unsigned int i = 0; i < sameXCounter.size(); i++) {
if(sameXCounter[i] >= 2) moreThan2++;
}
// if we have more than 1 two xvalues with more than 2 points, calibration is ok
if(moreThan2 > 1)
return true;
return false;
}
bool getCharucoBoardPose(InputArray charucoCorners, InputArray charucoIds, const aruco::CharucoBoard &board,
InputArray cameraMatrix, InputArray distCoeffs, InputOutputArray rvec, InputOutputArray tvec,
bool useExtrinsicGuess) {
CV_Assert((charucoCorners.getMat().total() == charucoIds.getMat().total()));
if(charucoIds.getMat().total() < 4) return false; // need, at least, 4 corners
std::vector<Point3f> objPoints;
objPoints.reserve(charucoIds.getMat().total());
for(unsigned int i = 0; i < charucoIds.getMat().total(); i++) {
int currId = charucoIds.getMat().at< int >(i);
CV_Assert(currId >= 0 && currId < (int)board.getChessboardCorners().size());
objPoints.push_back(board.getChessboardCorners()[currId]);
}
// points need to be in different lines, check if detected points are enough
if(!_arePointsEnoughForPoseEstimation(objPoints)) return false;
solvePnP(objPoints, charucoCorners, cameraMatrix, distCoeffs, rvec, tvec, useExtrinsicGuess);
return true;
}
/**
* @brief Return object points for the system centered in a middle (by default) or in a top left corner of single
* marker, given the marker length
*/
static Mat _getSingleMarkerObjectPoints(float markerLength, bool use_aruco_ccw_center) {
CV_Assert(markerLength > 0);
Mat objPoints(4, 1, CV_32FC3);
// set coordinate system in the top-left corner of the marker, with Z pointing out
if (use_aruco_ccw_center) {
objPoints.ptr<Vec3f>(0)[0] = Vec3f(-markerLength/2.f, markerLength/2.f, 0);
objPoints.ptr<Vec3f>(0)[1] = Vec3f(markerLength/2.f, markerLength/2.f, 0);
objPoints.ptr<Vec3f>(0)[2] = Vec3f(markerLength/2.f, -markerLength/2.f, 0);
objPoints.ptr<Vec3f>(0)[3] = Vec3f(-markerLength/2.f, -markerLength/2.f, 0);
}
else {
objPoints.ptr<Vec3f>(0)[0] = Vec3f(0.f, 0.f, 0);
objPoints.ptr<Vec3f>(0)[1] = Vec3f(markerLength, 0.f, 0);
objPoints.ptr<Vec3f>(0)[2] = Vec3f(markerLength, markerLength, 0);
objPoints.ptr<Vec3f>(0)[3] = Vec3f(0.f, markerLength, 0);
}
return objPoints;
}
void getMarkersPoses(InputArrayOfArrays corners, float markerLength, InputArray cameraMatrix, InputArray distCoeffs,
OutputArray _rvecs, OutputArray _tvecs, OutputArray objPoints,
bool use_aruco_ccw_center, SolvePnPMethod solvePnPMethod) {
CV_Assert(markerLength > 0);
Mat markerObjPoints = _getSingleMarkerObjectPoints(markerLength, use_aruco_ccw_center);
int nMarkers = (int)corners.total();
_rvecs.create(nMarkers, 1, CV_64FC3);
_tvecs.create(nMarkers, 1, CV_64FC3);
Mat rvecs = _rvecs.getMat(), tvecs = _tvecs.getMat();
for (int i = 0; i < nMarkers; i++)
solvePnP(markerObjPoints, corners.getMat(i), cameraMatrix, distCoeffs, rvecs.at<Vec3d>(i), tvecs.at<Vec3d>(i),
false, solvePnPMethod);
if(objPoints.needed())
markerObjPoints.convertTo(objPoints, -1);
}
}
@@ -0,0 +1,42 @@
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#include "test_precomp.hpp"
#include "opencv2/calib3d.hpp"
namespace opencv_test {
static inline double deg2rad(double deg) { return deg * CV_PI / 180.; }
vector<Point2f> getAxis(InputArray _cameraMatrix, InputArray _distCoeffs, InputArray _rvec, InputArray _tvec,
float length, const float offset = 0.f);
vector<Point2f> getMarkerById(int id, const vector<vector<Point2f> >& corners, const vector<int>& ids);
/**
* @brief Get rvec and tvec from yaw, pitch and distance
*/
void getSyntheticRT(double yaw, double pitch, double distance, Mat& rvec, Mat& tvec);
/**
* @brief Project a synthetic marker
*/
void projectMarker(Mat& img, const aruco::Board& board, int markerIndex, Mat cameraMatrix, Mat rvec, Mat tvec,
int markerBorder);
/**
* @brief Get a synthetic image of GridBoard in perspective
*/
Mat projectBoard(const aruco::GridBoard& board, Mat cameraMatrix, double yaw, double pitch, double distance,
Size imageSize, int markerBorder);
bool getCharucoBoardPose(InputArray charucoCorners, InputArray charucoIds, const aruco::CharucoBoard &board,
InputArray cameraMatrix, InputArray distCoeffs, InputOutputArray rvec,
InputOutputArray tvec, bool useExtrinsicGuess = false);
void getMarkersPoses(InputArrayOfArrays corners, float markerLength, InputArray cameraMatrix, InputArray distCoeffs,
OutputArray _rvecs, OutputArray _tvecs, OutputArray objPoints = noArray(),
bool use_aruco_ccw_center = true, SolvePnPMethod solvePnPMethod = SolvePnPMethod::SOLVEPNP_ITERATIVE);
}
@@ -0,0 +1,321 @@
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#include "test_precomp.hpp"
#include "test_aruco_utils.hpp"
namespace opencv_test { namespace {
enum class ArucoAlgParams
{
USE_DEFAULT = 0,
USE_ARUCO3 = 1
};
/**
* @brief Check pose estimation of aruco board
*/
class CV_ArucoBoardPose : public cvtest::BaseTest {
public:
CV_ArucoBoardPose(ArucoAlgParams arucoAlgParams)
{
aruco::DetectorParameters params;
aruco::Dictionary dictionary = aruco::getPredefinedDictionary(aruco::DICT_6X6_250);
params.minDistanceToBorder = 3;
if (arucoAlgParams == ArucoAlgParams::USE_ARUCO3) {
params.useAruco3Detection = true;
params.cornerRefinementMethod = aruco::CORNER_REFINE_SUBPIX;
params.minSideLengthCanonicalImg = 16;
params.errorCorrectionRate = 0.8;
}
detector = aruco::ArucoDetector(dictionary, params);
}
protected:
aruco::ArucoDetector detector;
void run(int);
};
void CV_ArucoBoardPose::run(int) {
int iter = 0;
Mat cameraMatrix = Mat::eye(3, 3, CV_64FC1);
Size imgSize(500, 500);
cameraMatrix.at< double >(0, 0) = cameraMatrix.at< double >(1, 1) = 650;
cameraMatrix.at< double >(0, 2) = imgSize.width / 2;
cameraMatrix.at< double >(1, 2) = imgSize.height / 2;
Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0));
const int sizeX = 3, sizeY = 3;
aruco::DetectorParameters detectorParameters = detector.getDetectorParameters();
// for different perspectives
for(double distance = 0.2; distance <= 0.4; distance += 0.15) {
for(int yaw = -55; yaw <= 50; yaw += 25) {
for(int pitch = -55; pitch <= 50; pitch += 25) {
vector<int> tmpIds;
for(int i = 0; i < sizeX*sizeY; i++)
tmpIds.push_back((iter + int(i)) % 250);
aruco::GridBoard gridboard(Size(sizeX, sizeY), 0.02f, 0.005f, detector.getDictionary(), tmpIds);
int markerBorder = iter % 2 + 1;
iter++;
// create synthetic image
Mat img = projectBoard(gridboard, cameraMatrix, deg2rad(yaw), deg2rad(pitch), distance,
imgSize, markerBorder);
vector<vector<Point2f> > corners;
vector<int> ids;
detectorParameters.markerBorderBits = markerBorder;
detector.setDetectorParameters(detectorParameters);
detector.detectMarkers(img, corners, ids);
ASSERT_EQ(ids.size(), gridboard.getIds().size());
// estimate pose
Mat rvec, tvec;
{
Mat objPoints, imgPoints; // get object and image points for the solvePnP function
gridboard.matchImagePoints(corners, ids, objPoints, imgPoints);
solvePnP(objPoints, imgPoints, cameraMatrix, distCoeffs, rvec, tvec);
}
// check axes
vector<Point2f> axes = getAxis(cameraMatrix, distCoeffs, rvec, tvec, gridboard.getRightBottomCorner().x);
vector<Point2f> topLeft = getMarkerById(gridboard.getIds()[0], corners, ids);
ASSERT_NEAR(topLeft[0].x, axes[0].x, 2.f);
ASSERT_NEAR(topLeft[0].y, axes[0].y, 2.f);
vector<Point2f> topRight = getMarkerById(gridboard.getIds()[2], corners, ids);
ASSERT_NEAR(topRight[1].x, axes[1].x, 2.f);
ASSERT_NEAR(topRight[1].y, axes[1].y, 2.f);
vector<Point2f> bottomLeft = getMarkerById(gridboard.getIds()[6], corners, ids);
ASSERT_NEAR(bottomLeft[3].x, axes[2].x, 2.f);
ASSERT_NEAR(bottomLeft[3].y, axes[2].y, 2.f);
// check estimate result
for(unsigned int i = 0; i < ids.size(); i++) {
int foundIdx = -1;
for(unsigned int j = 0; j < gridboard.getIds().size(); j++) {
if(gridboard.getIds()[j] == ids[i]) {
foundIdx = int(j);
break;
}
}
if(foundIdx == -1) {
ts->printf(cvtest::TS::LOG, "Marker detected with wrong ID in Board test");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
vector< Point2f > projectedCorners;
projectPoints(gridboard.getObjPoints()[foundIdx], rvec, tvec, cameraMatrix, distCoeffs,
projectedCorners);
for(int c = 0; c < 4; c++) {
double repError = cv::norm(projectedCorners[c] - corners[i][c]); // TODO cvtest
if(repError > 5.) {
ts->printf(cvtest::TS::LOG, "Corner reprojection error too high");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
}
}
}
}
}
}
/**
* @brief Check refine strategy
*/
class CV_ArucoRefine : public cvtest::BaseTest {
public:
CV_ArucoRefine(ArucoAlgParams arucoAlgParams)
{
aruco::Dictionary dictionary = aruco::getPredefinedDictionary(aruco::DICT_6X6_250);
aruco::DetectorParameters params;
params.minDistanceToBorder = 3;
params.cornerRefinementMethod = aruco::CORNER_REFINE_SUBPIX;
if (arucoAlgParams == ArucoAlgParams::USE_ARUCO3)
params.useAruco3Detection = true;
aruco::RefineParameters refineParams(10.f, 3.f, true);
detector = aruco::ArucoDetector(dictionary, params, refineParams);
}
protected:
aruco::ArucoDetector detector;
void run(int);
};
void CV_ArucoRefine::run(int) {
int iter = 0;
Mat cameraMatrix = Mat::eye(3, 3, CV_64FC1);
Size imgSize(500, 500);
cameraMatrix.at< double >(0, 0) = cameraMatrix.at< double >(1, 1) = 650;
cameraMatrix.at< double >(0, 2) = imgSize.width / 2;
cameraMatrix.at< double >(1, 2) = imgSize.height / 2;
Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0));
aruco::DetectorParameters detectorParameters = detector.getDetectorParameters();
// for different perspectives
for(double distance = 0.2; distance <= 0.4; distance += 0.2) {
for(int yaw = -60; yaw < 60; yaw += 30) {
for(int pitch = -60; pitch <= 60; pitch += 30) {
aruco::GridBoard gridboard(Size(3, 3), 0.02f, 0.005f, detector.getDictionary());
int markerBorder = iter % 2 + 1;
iter++;
// create synthetic image
Mat img = projectBoard(gridboard, cameraMatrix, deg2rad(yaw), deg2rad(pitch), distance,
imgSize, markerBorder);
// detect markers
vector<vector<Point2f> > corners, rejected;
vector<int> ids;
detectorParameters.markerBorderBits = markerBorder;
detector.setDetectorParameters(detectorParameters);
detector.detectMarkers(img, corners, ids, rejected);
// remove a marker from detection
int markersBeforeDelete = (int)ids.size();
if(markersBeforeDelete < 2) continue;
rejected.push_back(corners[0]);
corners.erase(corners.begin(), corners.begin() + 1);
ids.erase(ids.begin(), ids.begin() + 1);
// try to refind the erased marker
detector.refineDetectedMarkers(img, gridboard, corners, ids, rejected, cameraMatrix,
distCoeffs, noArray());
// check result
if((int)ids.size() < markersBeforeDelete) {
ts->printf(cvtest::TS::LOG, "Error in refine detected markers");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
}
}
}
}
TEST(CV_ArucoBoardPose, accuracy) {
CV_ArucoBoardPose test(ArucoAlgParams::USE_DEFAULT);
test.safe_run();
}
typedef CV_ArucoBoardPose CV_Aruco3BoardPose;
TEST(CV_Aruco3BoardPose, accuracy) {
CV_Aruco3BoardPose test(ArucoAlgParams::USE_ARUCO3);
test.safe_run();
}
typedef CV_ArucoRefine CV_Aruco3Refine;
TEST(CV_ArucoRefine, accuracy) {
CV_ArucoRefine test(ArucoAlgParams::USE_DEFAULT);
test.safe_run();
}
TEST(CV_Aruco3Refine, accuracy) {
CV_Aruco3Refine test(ArucoAlgParams::USE_ARUCO3);
test.safe_run();
}
TEST(CV_ArucoBoardPose, CheckNegativeZ)
{
double matrixData[9] = { -3.9062571886921410e+02, 0., 4.2350000000000000e+02,
0., 3.9062571886921410e+02, 2.3950000000000000e+02,
0., 0., 1 };
cv::Mat cameraMatrix = cv::Mat(3, 3, CV_64F, matrixData);
vector<cv::Point3f> pts3d1, pts3d2;
pts3d1.push_back(cv::Point3f(0.326198f, -0.030621f, 0.303620f));
pts3d1.push_back(cv::Point3f(0.325340f, -0.100594f, 0.301862f));
pts3d1.push_back(cv::Point3f(0.255859f, -0.099530f, 0.293416f));
pts3d1.push_back(cv::Point3f(0.256717f, -0.029557f, 0.295174f));
pts3d2.push_back(cv::Point3f(-0.033144f, -0.034819f, 0.245216f));
pts3d2.push_back(cv::Point3f(-0.035507f, -0.104705f, 0.241987f));
pts3d2.push_back(cv::Point3f(-0.105289f, -0.102120f, 0.237120f));
pts3d2.push_back(cv::Point3f(-0.102926f, -0.032235f, 0.240349f));
vector<int> tmpIds = {0, 1};
vector<vector<Point3f> > tmpObjectPoints = {pts3d1, pts3d2};
aruco::Board board(tmpObjectPoints, aruco::getPredefinedDictionary(0), tmpIds);
vector<vector<Point2f> > corners;
vector<Point2f> pts2d;
pts2d.push_back(cv::Point2f(37.7f, 203.3f));
pts2d.push_back(cv::Point2f(38.5f, 120.5f));
pts2d.push_back(cv::Point2f(105.5f, 115.8f));
pts2d.push_back(cv::Point2f(104.2f, 202.7f));
corners.push_back(pts2d);
pts2d.clear();
pts2d.push_back(cv::Point2f(476.0f, 184.2f));
pts2d.push_back(cv::Point2f(479.6f, 73.8f));
pts2d.push_back(cv::Point2f(590.9f, 77.0f));
pts2d.push_back(cv::Point2f(587.5f, 188.1f));
corners.push_back(pts2d);
Vec3d rvec, tvec;
int nUsed = 0;
{
Mat objPoints, imgPoints; // get object and image points for the solvePnP function
board.matchImagePoints(corners, board.getIds(), objPoints, imgPoints);
nUsed = (int)objPoints.total()/4;
solvePnP(objPoints, imgPoints, cameraMatrix, Mat(), rvec, tvec);
}
ASSERT_EQ(nUsed, 2);
cv::Matx33d rotm; cv::Point3d out;
cv::Rodrigues(rvec, rotm);
out = cv::Point3d(tvec) + rotm*Point3d(board.getObjPoints()[0][0]);
ASSERT_GT(out.z, 0);
corners.clear(); pts2d.clear();
pts2d.push_back(cv::Point2f(38.4f, 204.5f));
pts2d.push_back(cv::Point2f(40.0f, 124.7f));
pts2d.push_back(cv::Point2f(102.0f, 119.1f));
pts2d.push_back(cv::Point2f(99.9f, 203.6f));
corners.push_back(pts2d);
pts2d.clear();
pts2d.push_back(cv::Point2f(476.0f, 184.3f));
pts2d.push_back(cv::Point2f(479.2f, 75.1f));
pts2d.push_back(cv::Point2f(588.7f, 79.2f));
pts2d.push_back(cv::Point2f(586.3f, 188.5f));
corners.push_back(pts2d);
nUsed = 0;
{
Mat objPoints, imgPoints; // get object and image points for the solvePnP function
board.matchImagePoints(corners, board.getIds(), objPoints, imgPoints);
nUsed = (int)objPoints.total()/4;
solvePnP(objPoints, imgPoints, cameraMatrix, Mat(), rvec, tvec, true);
}
ASSERT_EQ(nUsed, 2);
cv::Rodrigues(rvec, rotm);
out = cv::Point3d(tvec) + rotm*Point3d(board.getObjPoints()[0][0]);
ASSERT_GT(out.z, 0);
}
TEST(CV_ArucoGenerateBoard, regression_1226) {
int bwidth = 1600;
int bheight = 1200;
cv::aruco::Dictionary dict = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_4X4_50);
cv::aruco::CharucoBoard board(Size(7, 5), 1.0, 0.75, dict);
cv::Size sz(bwidth, bheight);
cv::Mat mat;
ASSERT_NO_THROW(
{
board.generateImage(sz, mat, 0, 1);
});
}
}} // namespace
@@ -0,0 +1,659 @@
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#include "test_precomp.hpp"
#include "test_aruco_utils.hpp"
namespace opencv_test { namespace {
/**
* @brief Get a synthetic image of Chessboard in perspective
*/
static Mat projectChessboard(int squaresX, int squaresY, float squareSize, Size imageSize,
Mat cameraMatrix, Mat rvec, Mat tvec) {
Mat img(imageSize, CV_8UC1, Scalar::all(255));
Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0));
for(int y = 0; y < squaresY; y++) {
float startY = float(y) * squareSize;
for(int x = 0; x < squaresX; x++) {
if(y % 2 != x % 2) continue;
float startX = float(x) * squareSize;
vector< Point3f > squareCorners;
squareCorners.push_back(Point3f(startX, startY, 0) - Point3f(squaresX*squareSize/2.f, squaresY*squareSize/2.f, 0.f));
squareCorners.push_back(squareCorners[0] + Point3f(squareSize, 0, 0));
squareCorners.push_back(squareCorners[0] + Point3f(squareSize, squareSize, 0));
squareCorners.push_back(squareCorners[0] + Point3f(0, squareSize, 0));
vector< vector< Point2f > > projectedCorners;
projectedCorners.push_back(vector< Point2f >());
projectPoints(squareCorners, rvec, tvec, cameraMatrix, distCoeffs, projectedCorners[0]);
vector< vector< Point > > projectedCornersInt;
projectedCornersInt.push_back(vector< Point >());
for(int k = 0; k < 4; k++)
projectedCornersInt[0]
.push_back(Point((int)projectedCorners[0][k].x, (int)projectedCorners[0][k].y));
fillPoly(img, projectedCornersInt, Scalar::all(0));
}
}
return img;
}
/**
* @brief Check pose estimation of charuco board
*/
static Mat projectCharucoBoard(aruco::CharucoBoard& board, Mat cameraMatrix, double yaw,
double pitch, double distance, Size imageSize, int markerBorder,
Mat &rvec, Mat &tvec) {
getSyntheticRT(yaw, pitch, distance, rvec, tvec);
// project markers
Mat img = Mat(imageSize, CV_8UC1, Scalar::all(255));
for(unsigned int indexMarker = 0; indexMarker < board.getIds().size(); indexMarker++) {
projectMarker(img, board, indexMarker, cameraMatrix, rvec, tvec, markerBorder);
}
// project chessboard
Mat chessboard =
projectChessboard(board.getChessboardSize().width, board.getChessboardSize().height,
board.getSquareLength(), imageSize, cameraMatrix, rvec, tvec);
for(unsigned int i = 0; i < chessboard.total(); i++) {
if(chessboard.ptr< unsigned char >()[i] == 0) {
img.ptr< unsigned char >()[i] = 0;
}
}
return img;
}
/**
* @brief Check Charuco detection
*/
class CV_CharucoDetection : public cvtest::BaseTest {
public:
CV_CharucoDetection();
protected:
void run(int);
};
CV_CharucoDetection::CV_CharucoDetection() {}
void CV_CharucoDetection::run(int) {
int iter = 0;
Mat cameraMatrix = Mat::eye(3, 3, CV_64FC1);
Size imgSize(500, 500);
aruco::DetectorParameters params;
params.minDistanceToBorder = 3;
aruco::CharucoBoard board(Size(4, 4), 0.03f, 0.015f, aruco::getPredefinedDictionary(aruco::DICT_6X6_250));
aruco::CharucoDetector detector(board, aruco::CharucoParameters(), params);
cameraMatrix.at<double>(0, 0) = cameraMatrix.at<double>(1, 1) = 600;
cameraMatrix.at<double>(0, 2) = imgSize.width / 2;
cameraMatrix.at<double>(1, 2) = imgSize.height / 2;
Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0));
// for different perspectives
for(double distance = 0.2; distance <= 0.4; distance += 0.2) {
for(int yaw = -55; yaw <= 50; yaw += 25) {
for(int pitch = -55; pitch <= 50; pitch += 25) {
int markerBorder = iter % 2 + 1;
iter++;
// create synthetic image
Mat rvec, tvec;
Mat img = projectCharucoBoard(board, cameraMatrix, deg2rad(yaw), deg2rad(pitch),
distance, imgSize, markerBorder, rvec, tvec);
// detect markers and interpolate charuco corners
vector<vector<Point2f> > corners;
vector<Point2f> charucoCorners;
vector<int> ids, charucoIds;
params.markerBorderBits = markerBorder;
detector.setDetectorParameters(params);
//detector.detectMarkers(img, corners, ids);
if(iter % 2 == 0) {
detector.detectBoard(img, charucoCorners, charucoIds, corners, ids);
} else {
aruco::CharucoParameters charucoParameters;
charucoParameters.cameraMatrix = cameraMatrix;
charucoParameters.distCoeffs = distCoeffs;
detector.setCharucoParameters(charucoParameters);
detector.detectBoard(img, charucoCorners, charucoIds, corners, ids);
}
if(ids.size() == 0) {
ts->printf(cvtest::TS::LOG, "Marker detection failed");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
// check results
vector< Point2f > projectedCharucoCorners;
// copy chessboardCorners
vector<Point3f> copyChessboardCorners = board.getChessboardCorners();
// move copyChessboardCorners points
for (size_t i = 0; i < copyChessboardCorners.size(); i++)
copyChessboardCorners[i] -= board.getRightBottomCorner() / 2.f;
projectPoints(copyChessboardCorners, rvec, tvec, cameraMatrix, distCoeffs,
projectedCharucoCorners);
for(unsigned int i = 0; i < charucoIds.size(); i++) {
int currentId = charucoIds[i];
if(currentId >= (int)board.getChessboardCorners().size()) {
ts->printf(cvtest::TS::LOG, "Invalid Charuco corner id");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
double repError = cv::norm(charucoCorners[i] - projectedCharucoCorners[currentId]); // TODO cvtest
if(repError > 5.) {
ts->printf(cvtest::TS::LOG, "Charuco corner reprojection error too high");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
}
}
}
}
}
/**
* @brief Check charuco pose estimation
*/
class CV_CharucoPoseEstimation : public cvtest::BaseTest {
public:
CV_CharucoPoseEstimation();
protected:
void run(int);
};
CV_CharucoPoseEstimation::CV_CharucoPoseEstimation() {}
void CV_CharucoPoseEstimation::run(int) {
int iter = 0;
Mat cameraMatrix = Mat::eye(3, 3, CV_64FC1);
Size imgSize(500, 500);
aruco::DetectorParameters params;
params.minDistanceToBorder = 3;
aruco::CharucoBoard board(Size(4, 4), 0.03f, 0.015f, aruco::getPredefinedDictionary(aruco::DICT_6X6_250));
aruco::CharucoDetector detector(board, aruco::CharucoParameters(), params);
cameraMatrix.at<double>(0, 0) = cameraMatrix.at< double >(1, 1) = 650;
cameraMatrix.at<double>(0, 2) = imgSize.width / 2;
cameraMatrix.at<double>(1, 2) = imgSize.height / 2;
Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0));
// for different perspectives
for(double distance = 0.2; distance <= 0.3; distance += 0.1) {
for(int yaw = -55; yaw <= 50; yaw += 25) {
for(int pitch = -55; pitch <= 50; pitch += 25) {
int markerBorder = iter % 2 + 1;
iter++;
// get synthetic image
Mat rvec, tvec;
Mat img = projectCharucoBoard(board, cameraMatrix, deg2rad(yaw), deg2rad(pitch),
distance, imgSize, markerBorder, rvec, tvec);
// detect markers
vector<vector<Point2f> > corners;
vector<int> ids;
params.markerBorderBits = markerBorder;
detector.setDetectorParameters(params);
// detect markers and interpolate charuco corners
vector<Point2f> charucoCorners;
vector<int> charucoIds;
if(iter % 2 == 0) {
detector.detectBoard(img, charucoCorners, charucoIds, corners, ids);
} else {
aruco::CharucoParameters charucoParameters;
charucoParameters.cameraMatrix = cameraMatrix;
charucoParameters.distCoeffs = distCoeffs;
detector.setCharucoParameters(charucoParameters);
detector.detectBoard(img, charucoCorners, charucoIds, corners, ids);
}
ASSERT_EQ(ids.size(), board.getIds().size());
if(charucoIds.size() == 0) continue;
// estimate charuco pose
getCharucoBoardPose(charucoCorners, charucoIds, board, cameraMatrix, distCoeffs, rvec, tvec);
// check axes
const float offset = (board.getSquareLength() - board.getMarkerLength()) / 2.f;
vector<Point2f> axes = getAxis(cameraMatrix, distCoeffs, rvec, tvec, board.getSquareLength(), offset);
vector<Point2f> topLeft = getMarkerById(board.getIds()[0], corners, ids);
ASSERT_NEAR(topLeft[0].x, axes[1].x, 3.f);
ASSERT_NEAR(topLeft[0].y, axes[1].y, 3.f);
vector<Point2f> bottomLeft = getMarkerById(board.getIds()[2], corners, ids);
ASSERT_NEAR(bottomLeft[0].x, axes[2].x, 3.f);
ASSERT_NEAR(bottomLeft[0].y, axes[2].y, 3.f);
// check estimate result
vector< Point2f > projectedCharucoCorners;
projectPoints(board.getChessboardCorners(), rvec, tvec, cameraMatrix, distCoeffs,
projectedCharucoCorners);
for(unsigned int i = 0; i < charucoIds.size(); i++) {
int currentId = charucoIds[i];
if(currentId >= (int)board.getChessboardCorners().size()) {
ts->printf(cvtest::TS::LOG, "Invalid Charuco corner id");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
double repError = cv::norm(charucoCorners[i] - projectedCharucoCorners[currentId]); // TODO cvtest
if(repError > 5.) {
ts->printf(cvtest::TS::LOG, "Charuco corner reprojection error too high");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
}
}
}
}
}
/**
* @brief Check diamond detection
*/
class CV_CharucoDiamondDetection : public cvtest::BaseTest {
public:
CV_CharucoDiamondDetection();
protected:
void run(int);
};
CV_CharucoDiamondDetection::CV_CharucoDiamondDetection() {}
void CV_CharucoDiamondDetection::run(int) {
int iter = 0;
Mat cameraMatrix = Mat::eye(3, 3, CV_64FC1);
Size imgSize(500, 500);
aruco::DetectorParameters params;
params.minDistanceToBorder = 0;
float squareLength = 0.03f;
float markerLength = 0.015f;
aruco::CharucoBoard board(Size(3, 3), squareLength, markerLength,
aruco::getPredefinedDictionary(aruco::DICT_6X6_250));
aruco::CharucoDetector detector(board);
cameraMatrix.at<double>(0, 0) = cameraMatrix.at< double >(1, 1) = 650;
cameraMatrix.at<double>(0, 2) = imgSize.width / 2;
cameraMatrix.at<double>(1, 2) = imgSize.height / 2;
Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0));
aruco::CharucoParameters charucoParameters;
charucoParameters.cameraMatrix = cameraMatrix;
charucoParameters.distCoeffs = distCoeffs;
detector.setCharucoParameters(charucoParameters);
// for different perspectives
for(double distance = 0.2; distance <= 0.3; distance += 0.1) {
for(int yaw = -50; yaw <= 50; yaw += 25) {
for(int pitch = -50; pitch <= 50; pitch += 25) {
int markerBorder = iter % 2 + 1;
vector<int> idsTmp;
for(int i = 0; i < 4; i++)
idsTmp.push_back(4 * iter + i);
board = aruco::CharucoBoard(Size(3, 3), squareLength, markerLength,
aruco::getPredefinedDictionary(aruco::DICT_6X6_250), idsTmp);
detector.setBoard(board);
iter++;
// get synthetic image
Mat rvec, tvec;
Mat img = projectCharucoBoard(board, cameraMatrix, deg2rad(yaw), deg2rad(pitch),
distance, imgSize, markerBorder, rvec, tvec);
// detect markers
vector<vector<Point2f>> corners;
vector<int> ids;
params.markerBorderBits = markerBorder;
detector.setDetectorParameters(params);
//detector.detectMarkers(img, corners, ids);
// detect diamonds
vector<vector<Point2f>> diamondCorners;
vector<Vec4i> diamondIds;
detector.detectDiamonds(img, diamondCorners, diamondIds, corners, ids);
// check detect
if(ids.size() != 4) {
ts->printf(cvtest::TS::LOG, "Not enough markers for diamond detection");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
// check results
if(diamondIds.size() != 1) {
ts->printf(cvtest::TS::LOG, "Diamond not detected correctly");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
for(int i = 0; i < 4; i++) {
if(diamondIds[0][i] != board.getIds()[i]) {
ts->printf(cvtest::TS::LOG, "Incorrect diamond ids");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
}
vector< Point2f > projectedDiamondCorners;
// copy chessboardCorners
vector<Point3f> copyChessboardCorners = board.getChessboardCorners();
// move copyChessboardCorners points
for (size_t i = 0; i < copyChessboardCorners.size(); i++)
copyChessboardCorners[i] -= board.getRightBottomCorner() / 2.f;
projectPoints(copyChessboardCorners, rvec, tvec, cameraMatrix, distCoeffs,
projectedDiamondCorners);
vector< Point2f > projectedDiamondCornersReorder(4);
projectedDiamondCornersReorder[0] = projectedDiamondCorners[0];
projectedDiamondCornersReorder[1] = projectedDiamondCorners[1];
projectedDiamondCornersReorder[2] = projectedDiamondCorners[3];
projectedDiamondCornersReorder[3] = projectedDiamondCorners[2];
for(unsigned int i = 0; i < 4; i++) {
double repError = cv::norm(diamondCorners[0][i] - projectedDiamondCornersReorder[i]); // TODO cvtest
if(repError > 5.) {
ts->printf(cvtest::TS::LOG, "Diamond corner reprojection error too high");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
}
// estimate diamond pose
vector< Vec3d > estimatedRvec, estimatedTvec;
getMarkersPoses(diamondCorners, squareLength, cameraMatrix, distCoeffs, estimatedRvec,
estimatedTvec, noArray(), false);
// check result
vector< Point2f > projectedDiamondCornersPose;
vector< Vec3f > diamondObjPoints(4);
diamondObjPoints[0] = Vec3f(0.f, 0.f, 0);
diamondObjPoints[1] = Vec3f(squareLength, 0.f, 0);
diamondObjPoints[2] = Vec3f(squareLength, squareLength, 0);
diamondObjPoints[3] = Vec3f(0.f, squareLength, 0);
projectPoints(diamondObjPoints, estimatedRvec[0], estimatedTvec[0], cameraMatrix,
distCoeffs, projectedDiamondCornersPose);
for(unsigned int i = 0; i < 4; i++) {
double repError = cv::norm(projectedDiamondCornersReorder[i] - projectedDiamondCornersPose[i]); // TODO cvtest
if(repError > 5.) {
ts->printf(cvtest::TS::LOG, "Charuco pose error too high");
ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
return;
}
}
}
}
}
}
/**
* @brief Check charuco board creation
*/
class CV_CharucoBoardCreation : public cvtest::BaseTest {
public:
CV_CharucoBoardCreation();
protected:
void run(int);
};
CV_CharucoBoardCreation::CV_CharucoBoardCreation() {}
void CV_CharucoBoardCreation::run(int)
{
aruco::Dictionary dictionary = aruco::getPredefinedDictionary(aruco::DICT_5X5_250);
int n = 6;
float markerSizeFactor = 0.5f;
for (float squareSize_mm = 5.0f; squareSize_mm < 35.0f; squareSize_mm += 0.1f)
{
aruco::CharucoBoard board_meters(Size(n, n), squareSize_mm*1e-3f,
squareSize_mm * markerSizeFactor * 1e-3f, dictionary);
aruco::CharucoBoard board_millimeters(Size(n, n), squareSize_mm,
squareSize_mm * markerSizeFactor, dictionary);
for (size_t i = 0; i < board_meters.getNearestMarkerIdx().size(); i++)
{
if (board_meters.getNearestMarkerIdx()[i].size() != board_millimeters.getNearestMarkerIdx()[i].size() ||
board_meters.getNearestMarkerIdx()[i][0] != board_millimeters.getNearestMarkerIdx()[i][0])
{
ts->printf(cvtest::TS::LOG,
cv::format("Charuco board topology is sensitive to scale with squareSize=%.1f\n",
squareSize_mm).c_str());
ts->set_failed_test_info(cvtest::TS::FAIL_INVALID_OUTPUT);
break;
}
}
}
}
TEST(CV_CharucoDetection, accuracy) {
CV_CharucoDetection test;
test.safe_run();
}
TEST(CV_CharucoPoseEstimation, accuracy) {
CV_CharucoPoseEstimation test;
test.safe_run();
}
TEST(CV_CharucoDiamondDetection, accuracy) {
CV_CharucoDiamondDetection test;
test.safe_run();
}
TEST(CV_CharucoBoardCreation, accuracy) {
CV_CharucoBoardCreation test;
test.safe_run();
}
TEST(Charuco, testCharucoCornersCollinear_true)
{
int squaresX = 13;
int squaresY = 28;
float squareLength = 300;
float markerLength = 150;
int dictionaryId = 11;
aruco::Dictionary dictionary = aruco::getPredefinedDictionary(aruco::PredefinedDictionaryType(dictionaryId));
aruco::CharucoBoard charucoBoard(Size(squaresX, squaresY), squareLength, markerLength, dictionary);
// consistency with C++98
const int arrLine[9] = {192, 204, 216, 228, 240, 252, 264, 276, 288};
vector<int> charucoIdsAxisLine(9, 0);
for (int i = 0; i < 9; i++){
charucoIdsAxisLine[i] = arrLine[i];
}
const int arrDiag[7] = {198, 209, 220, 231, 242, 253, 264};
vector<int> charucoIdsDiagonalLine(7, 0);
for (int i = 0; i < 7; i++){
charucoIdsDiagonalLine[i] = arrDiag[i];
}
bool resultAxisLine = charucoBoard.checkCharucoCornersCollinear(charucoIdsAxisLine);
EXPECT_TRUE(resultAxisLine);
bool resultDiagonalLine = charucoBoard.checkCharucoCornersCollinear(charucoIdsDiagonalLine);
EXPECT_TRUE(resultDiagonalLine);
}
TEST(Charuco, testCharucoCornersCollinear_false)
{
int squaresX = 13;
int squaresY = 28;
float squareLength = 300;
float markerLength = 150;
int dictionaryId = 11;
aruco::Dictionary dictionary = aruco::getPredefinedDictionary(aruco::PredefinedDictionaryType(dictionaryId));
aruco::CharucoBoard charucoBoard(Size(squaresX, squaresY), squareLength, markerLength, dictionary);
// consistency with C++98
const int arr[63] = {192, 193, 194, 195, 196, 197, 198, 204, 205, 206, 207, 208,
209, 210, 216, 217, 218, 219, 220, 221, 222, 228, 229, 230,
231, 232, 233, 234, 240, 241, 242, 243, 244, 245, 246, 252,
253, 254, 255, 256, 257, 258, 264, 265, 266, 267, 268, 269,
270, 276, 277, 278, 279, 280, 281, 282, 288, 289, 290, 291,
292, 293, 294};
vector<int> charucoIds(63, 0);
for (int i = 0; i < 63; i++){
charucoIds[i] = arr[i];
}
bool result = charucoBoard.checkCharucoCornersCollinear(charucoIds);
EXPECT_FALSE(result);
}
// test that ChArUco board detection is subpixel accurate
TEST(Charuco, testBoardSubpixelCoords)
{
cv::Size res{500, 500};
cv::Mat K = (cv::Mat_<double>(3,3) <<
0.5*res.width, 0, 0.5*res.width,
0, 0.5*res.height, 0.5*res.height,
0, 0, 1);
// set expected_corners values
cv::Mat expected_corners = (cv::Mat_<float>(9,2) <<
200, 200,
250, 200,
300, 200,
200, 250,
250, 250,
300, 250,
200, 300,
250, 300,
300, 300
);
cv::Mat gray;
aruco::Dictionary dict = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_APRILTAG_36h11);
aruco::CharucoBoard board(Size(4, 4), 1.f, .8f, dict);
// generate ChArUco board
board.generateImage(Size(res.width, res.height), gray, 150);
cv::GaussianBlur(gray, gray, Size(5, 5), 1.0);
aruco::DetectorParameters params;
params.cornerRefinementMethod = cv::aruco::CORNER_REFINE_APRILTAG;
aruco::CharucoParameters charucoParameters;
charucoParameters.cameraMatrix = K;
aruco::CharucoDetector detector(board, charucoParameters);
detector.setDetectorParameters(params);
std::vector<int> ids;
std::vector<std::vector<cv::Point2f>> corners;
cv::Mat c_ids, c_corners;
detector.detectBoard(gray, c_corners, c_ids, corners, ids);
ASSERT_EQ(ids.size(), size_t(8));
ASSERT_EQ(c_corners.rows, expected_corners.rows);
EXPECT_NEAR(0, cvtest::norm(expected_corners, c_corners.reshape(1), NORM_INF), 1e-1);
}
TEST(Charuco, issue_14014)
{
string imgPath = cvtest::findDataFile("aruco/recover.png");
Mat img = imread(imgPath);
aruco::DetectorParameters detectorParams;
detectorParams.cornerRefinementMethod = aruco::CORNER_REFINE_SUBPIX;
detectorParams.cornerRefinementMinAccuracy = 0.01;
aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_7X7_250), detectorParams);
aruco::CharucoBoard board(Size(8, 5), 0.03455f, 0.02164f, detector.getDictionary());
vector<Mat> corners, rejectedPoints;
vector<int> ids;
detector.detectMarkers(img, corners, ids, rejectedPoints);
ASSERT_EQ(corners.size(), 19ull);
EXPECT_EQ(Size(4, 1), corners[0].size()); // check dimension of detected corners
size_t numRejPoints = rejectedPoints.size();
ASSERT_EQ(rejectedPoints.size(), 26ull); // optional check to track regressions
EXPECT_EQ(Size(4, 1), rejectedPoints[0].size()); // check dimension of detected corners
detector.refineDetectedMarkers(img, board, corners, ids, rejectedPoints);
ASSERT_EQ(corners.size(), 20ull);
EXPECT_EQ(Size(4, 1), corners[0].size()); // check dimension of rejected corners after successfully refine
ASSERT_EQ(rejectedPoints.size() + 1, numRejPoints);
EXPECT_EQ(Size(4, 1), rejectedPoints[0].size()); // check dimension of rejected corners after successfully refine
}
}} // namespace