diff --git a/modules/objdetect/include/opencv2/objdetect/aruco_board.hpp b/modules/objdetect/include/opencv2/objdetect/aruco_board.hpp index bdf2fac8dd..fbd420c90e 100644 --- a/modules/objdetect/include/opencv2/objdetect/aruco_board.hpp +++ b/modules/objdetect/include/opencv2/objdetect/aruco_board.hpp @@ -146,6 +146,18 @@ public: CV_WRAP CharucoBoard(const Size& size, float squareLength, float markerLength, const Dictionary &dictionary, InputArray ids = noArray()); + /** @brief set legacy chessboard pattern. + * + * Legacy setting creates chessboard patterns starting with a white box in the upper left corner + * if there is an even row count of chessboard boxes, otherwise it starts with a black box. + * This setting ensures compatibility to patterns created with OpenCV versions prior OpenCV 4.6.0. + * See https://github.com/opencv/opencv/issues/23152. + * + * Default value: false. + */ + CV_WRAP void setLegacyPattern(bool legacyPattern); + CV_WRAP bool getLegacyPattern() const; + CV_WRAP Size getChessboardSize() const; CV_WRAP float getSquareLength() const; CV_WRAP float getMarkerLength() const; diff --git a/modules/objdetect/src/aruco/aruco_board.cpp b/modules/objdetect/src/aruco/aruco_board.cpp index c16cd1e931..2ad6c5757b 100644 --- a/modules/objdetect/src/aruco/aruco_board.cpp +++ b/modules/objdetect/src/aruco/aruco_board.cpp @@ -229,7 +229,8 @@ struct GridBoardImpl : public Board::Impl { Board::Impl(_dictionary), size(_size), markerLength(_markerLength), - markerSeparation(_markerSeparation) + markerSeparation(_markerSeparation), + legacyPattern(false) { CV_Assert(size.width*size.height > 0 && markerLength > 0 && markerSeparation > 0); } @@ -240,6 +241,8 @@ struct GridBoardImpl : public Board::Impl { float markerLength; // separation between markers in the grid float markerSeparation; + // set pre4.6.0 chessboard pattern behavior (even row count patterns have a white box in the upper left corner) + bool legacyPattern; }; GridBoard::GridBoard() {} @@ -297,7 +300,8 @@ struct CharucoBoardImpl : Board::Impl { Board::Impl(_dictionary), size(_size), squareLength(_squareLength), - markerLength(_markerLength) + markerLength(_markerLength), + legacyPattern(false) {} // chessboard size @@ -309,6 +313,9 @@ struct CharucoBoardImpl : Board::Impl { // Physical marker side length (normally in meters) float markerLength; + // set pre4.6.0 chessboard pattern behavior (even row count patterns have a white box in the upper left corner) + bool legacyPattern; + // vector of chessboard 3D corners precalculated std::vector chessboardCorners; @@ -316,6 +323,7 @@ struct CharucoBoardImpl : Board::Impl { std::vector > nearestMarkerIdx; std::vector > nearestMarkerCorners; + void createCharucoBoard(); void calcNearestMarkerCorners(); void matchImagePoints(InputArrayOfArrays detectedCharuco, InputArray detectedIds, @@ -324,8 +332,56 @@ struct CharucoBoardImpl : Board::Impl { void generateImage(Size outSize, OutputArray img, int marginSize, int borderBits) const override; }; +void CharucoBoardImpl::createCharucoBoard() { + float diffSquareMarkerLength = (squareLength - markerLength) / 2; + int totalMarkers = (int)(ids.size()); + // calculate Board objPoints + int nextId = 0; + objPoints.clear(); + for(int y = 0; y < size.height; y++) { + for(int x = 0; x < size.width; x++) { + + if(legacyPattern && (size.height % 2 == 0)) { // legacy behavior only for even row count patterns + if((y + 1) % 2 == x % 2) continue; // black corner, no marker here + } else { + if(y % 2 == x % 2) continue; // black corner, no marker here + } + + vector corners(4); + corners[0] = Point3f(x * squareLength + diffSquareMarkerLength, + y * squareLength + diffSquareMarkerLength, 0); + corners[1] = corners[0] + Point3f(markerLength, 0, 0); + corners[2] = corners[0] + Point3f(markerLength, markerLength, 0); + corners[3] = corners[0] + Point3f(0, markerLength, 0); + objPoints.push_back(corners); + // first ids in dictionary + if (totalMarkers == 0) + ids.push_back(nextId); + nextId++; + } + } + if (totalMarkers > 0 && nextId != totalMarkers) + CV_Error(cv::Error::StsBadSize, "Size of ids must be equal to the number of markers: "+std::to_string(nextId)); + + // now fill chessboardCorners + chessboardCorners.clear(); + for(int y = 0; y < size.height - 1; y++) { + for(int x = 0; x < size.width - 1; x++) { + Point3f corner; + corner.x = (x + 1) * squareLength; + corner.y = (y + 1) * squareLength; + corner.z = 0; + chessboardCorners.push_back(corner); + } + } + rightBottomBorder = Point3f(size.width * squareLength, size.height * squareLength, 0.f); + calcNearestMarkerCorners(); +} + /** Fill nearestMarkerIdx and nearestMarkerCorners arrays */ void CharucoBoardImpl::calcNearestMarkerCorners() { + nearestMarkerIdx.clear(); + nearestMarkerCorners.clear(); nearestMarkerIdx.resize(chessboardCorners.size()); nearestMarkerCorners.resize(chessboardCorners.size()); unsigned int nMarkers = (unsigned int)objPoints.size(); @@ -459,7 +515,11 @@ void CharucoBoardImpl::generateImage(Size outSize, OutputArray img, int marginSi for(int y = 0; y < size.height; y++) { for(int x = 0; x < size.width; x++) { - if(y % 2 != x % 2) continue; // white corner, dont do anything + if(legacyPattern && (size.height % 2 == 0)) { // legacy behavior only for even row count patterns + if((y + 1) % 2 != x % 2) continue; // white corner, dont do anything + } else { + if(y % 2 != x % 2) continue; // white corner, dont do anything + } double startX, startY; startX = squareSizePixels * double(x); @@ -481,47 +541,9 @@ CharucoBoard::CharucoBoard(const Size& size, float squareLength, float markerLen CV_Assert(size.width > 1 && size.height > 1 && markerLength > 0 && squareLength > markerLength); - vector > objPoints; - float diffSquareMarkerLength = (squareLength - markerLength) / 2; - int totalMarkers = (int)(ids.total()); ids.copyTo(impl->ids); - // calculate Board objPoints - int nextId = 0; - for(int y = 0; y < size.height; y++) { - for(int x = 0; x < size.width; x++) { - if(y % 2 == x % 2) continue; // black corner, no marker here - - vector corners(4); - corners[0] = Point3f(x * squareLength + diffSquareMarkerLength, - y * squareLength + diffSquareMarkerLength, 0); - corners[1] = corners[0] + Point3f(markerLength, 0, 0); - corners[2] = corners[0] + Point3f(markerLength, markerLength, 0); - corners[3] = corners[0] + Point3f(0, markerLength, 0); - objPoints.push_back(corners); - // first ids in dictionary - if (totalMarkers == 0) - impl->ids.push_back(nextId); - nextId++; - } - } - if (totalMarkers > 0 && nextId != totalMarkers) - CV_Error(cv::Error::StsBadSize, "Size of ids must be equal to the number of markers: "+std::to_string(nextId)); - impl->objPoints = objPoints; - - // now fill chessboardCorners - std::vector & c = static_pointer_cast(impl)->chessboardCorners; - for(int y = 0; y < size.height - 1; y++) { - for(int x = 0; x < size.width - 1; x++) { - Point3f corner; - corner.x = (x + 1) * squareLength; - corner.y = (y + 1) * squareLength; - corner.z = 0; - c.push_back(corner); - } - } - impl->rightBottomBorder = Point3f(size.width * squareLength, size.height * squareLength, 0.f); - static_pointer_cast(impl)->calcNearestMarkerCorners(); + static_pointer_cast(impl)->createCharucoBoard(); } Size CharucoBoard::getChessboardSize() const { @@ -539,6 +561,20 @@ float CharucoBoard::getMarkerLength() const { return static_pointer_cast(impl)->markerLength; } +void CharucoBoard::setLegacyPattern(bool legacyPattern) { + CV_Assert(impl); + if (static_pointer_cast(impl)->legacyPattern != legacyPattern) + { + static_pointer_cast(impl)->legacyPattern = legacyPattern; + static_pointer_cast(impl)->createCharucoBoard(); + } +} + +bool CharucoBoard::getLegacyPattern() const { + CV_Assert(impl); + return static_pointer_cast(impl)->legacyPattern; +} + bool CharucoBoard::checkCharucoCornersCollinear(InputArray charucoIds) const { CV_Assert(impl); Mat charucoIdsMat = charucoIds.getMat(); diff --git a/modules/objdetect/test/test_aruco_utils.cpp b/modules/objdetect/test/test_aruco_utils.cpp index b301622b60..8600bc7c00 100644 --- a/modules/objdetect/test/test_aruco_utils.cpp +++ b/modules/objdetect/test/test_aruco_utils.cpp @@ -8,12 +8,12 @@ namespace opencv_test { vector getAxis(InputArray _cameraMatrix, InputArray _distCoeffs, InputArray _rvec, - InputArray _tvec, float length, const float offset) { + InputArray _tvec, float length, const Point2f offset) { vector 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)); + axis.push_back(Point3f(offset.x, offset.y, 0.f)); + axis.push_back(Point3f(length+offset.x, offset.y, 0.f)); + axis.push_back(Point3f(offset.x, length+offset.y, 0.f)); + axis.push_back(Point3f(offset.x, offset.y, length)); vector axis_to_img; projectPoints(axis, _rvec, _tvec, _cameraMatrix, _distCoeffs, axis_to_img); return axis_to_img; diff --git a/modules/objdetect/test/test_aruco_utils.hpp b/modules/objdetect/test/test_aruco_utils.hpp index 9b79fa59bc..a13b32fe4a 100644 --- a/modules/objdetect/test/test_aruco_utils.hpp +++ b/modules/objdetect/test/test_aruco_utils.hpp @@ -10,7 +10,7 @@ namespace opencv_test { static inline double deg2rad(double deg) { return deg * CV_PI / 180.; } vector getAxis(InputArray _cameraMatrix, InputArray _distCoeffs, InputArray _rvec, InputArray _tvec, - float length, const float offset = 0.f); + float length, const Point2f offset = Point2f(0, 0)); vector getMarkerById(int id, const vector >& corners, const vector& ids); diff --git a/modules/objdetect/test/test_charucodetection.cpp b/modules/objdetect/test/test_charucodetection.cpp index 97a6eb704d..87d2f12b02 100644 --- a/modules/objdetect/test/test_charucodetection.cpp +++ b/modules/objdetect/test/test_charucodetection.cpp @@ -12,7 +12,7 @@ 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 cameraMatrix, Mat rvec, Mat tvec, bool legacyPattern) { Mat img(imageSize, CV_8UC1, Scalar::all(255)); Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0)); @@ -20,7 +20,11 @@ static Mat projectChessboard(int squaresX, int squaresY, float squareSize, Size 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; + if(legacyPattern && (squaresY % 2 == 0)) { + if((y + 1) % 2 != x % 2) continue; + } else { + if(y % 2 != x % 2) continue; + } float startX = float(x) * squareSize; vector< Point3f > squareCorners; @@ -66,7 +70,7 @@ static Mat projectCharucoBoard(aruco::CharucoBoard& board, Mat cameraMatrix, dou // project chessboard Mat chessboard = projectChessboard(board.getChessboardSize().width, board.getChessboardSize().height, - board.getSquareLength(), imageSize, cameraMatrix, rvec, tvec); + board.getSquareLength(), imageSize, cameraMatrix, rvec, tvec, board.getLegacyPattern()); for(unsigned int i = 0; i < chessboard.total(); i++) { if(chessboard.ptr< unsigned char >()[i] == 0) { @@ -82,16 +86,15 @@ static Mat projectCharucoBoard(aruco::CharucoBoard& board, Mat cameraMatrix, dou */ class CV_CharucoDetection : public cvtest::BaseTest { public: - CV_CharucoDetection(); + CV_CharucoDetection(bool _legacyPattern) : legacyPattern(_legacyPattern) {} protected: void run(int); + + bool legacyPattern; }; -CV_CharucoDetection::CV_CharucoDetection() {} - - void CV_CharucoDetection::run(int) { int iter = 0; @@ -100,6 +103,7 @@ void CV_CharucoDetection::run(int) { aruco::DetectorParameters params; params.minDistanceToBorder = 3; aruco::CharucoBoard board(Size(4, 4), 0.03f, 0.015f, aruco::getPredefinedDictionary(aruco::DICT_6X6_250)); + board.setLegacyPattern(legacyPattern); aruco::CharucoDetector detector(board, aruco::CharucoParameters(), params); cameraMatrix.at(0, 0) = cameraMatrix.at(1, 1) = 600; @@ -140,11 +144,7 @@ void CV_CharucoDetection::run(int) { 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; - } + ASSERT_GT(ids.size(), std::vector< int >::size_type(0)) << "Marker detection failed"; // check results vector< Point2f > projectedCharucoCorners; @@ -161,20 +161,11 @@ void CV_CharucoDetection::run(int) { 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; - } + ASSERT_LT(currentId, (int)board.getChessboardCorners().size()) << "Invalid Charuco corner id"; 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; - } + ASSERT_LE(repError, 5.) << "Charuco corner reprojection error too high"; } } } @@ -188,30 +179,31 @@ void CV_CharucoDetection::run(int) { */ class CV_CharucoPoseEstimation : public cvtest::BaseTest { public: - CV_CharucoPoseEstimation(); + CV_CharucoPoseEstimation(bool _legacyPattern) : legacyPattern(_legacyPattern) {} protected: void run(int); + + bool legacyPattern; }; -CV_CharucoPoseEstimation::CV_CharucoPoseEstimation() {} - - void CV_CharucoPoseEstimation::run(int) { int iter = 0; Mat cameraMatrix = Mat::eye(3, 3, CV_64FC1); - Size imgSize(500, 500); + Size imgSize(750, 750); aruco::DetectorParameters params; params.minDistanceToBorder = 3; aruco::CharucoBoard board(Size(4, 4), 0.03f, 0.015f, aruco::getPredefinedDictionary(aruco::DICT_6X6_250)); + board.setLegacyPattern(legacyPattern); aruco::CharucoDetector detector(board, aruco::CharucoParameters(), params); - cameraMatrix.at(0, 0) = cameraMatrix.at< double >(1, 1) = 650; + cameraMatrix.at(0, 0) = cameraMatrix.at< double >(1, 1) = 1000; cameraMatrix.at(0, 2) = imgSize.width / 2; cameraMatrix.at(1, 2) = imgSize.height / 2; Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0)); + // for different perspectives for(double distance : {0.2, 0.25}) { for(int yaw = -55; yaw <= 50; yaw += 25) { @@ -252,12 +244,21 @@ void CV_CharucoPoseEstimation::run(int) { // check axes - const float offset = (board.getSquareLength() - board.getMarkerLength()) / 2.f; + const float aruco_offset = (board.getSquareLength() - board.getMarkerLength()) / 2.f; + Point2f offset; + vector topLeft, bottomLeft; + if(legacyPattern) { // white box in upper left corner for even row count chessboard patterns + offset = Point2f(aruco_offset + board.getSquareLength(), aruco_offset); + topLeft = getMarkerById(board.getIds()[1], corners, ids); + bottomLeft = getMarkerById(board.getIds()[2], corners, ids); + } else { // always a black box in the upper left corner + offset = Point2f(aruco_offset, aruco_offset); + topLeft = getMarkerById(board.getIds()[0], corners, ids); + bottomLeft = getMarkerById(board.getIds()[2], corners, ids); + } vector axes = getAxis(cameraMatrix, distCoeffs, rvec, tvec, board.getSquareLength(), offset); - vector 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 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); @@ -271,20 +272,11 @@ void CV_CharucoPoseEstimation::run(int) { 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; - } + ASSERT_LT(currentId, (int)board.getChessboardCorners().size()) << "Invalid Charuco corner id"; 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; - } + ASSERT_LE(repError, 5.) << "Charuco corner reprojection error too high"; } } } @@ -490,12 +482,26 @@ void CV_CharucoBoardCreation::run(int) TEST(CV_CharucoDetection, accuracy) { - CV_CharucoDetection test; + const bool legacyPattern = false; + CV_CharucoDetection test(legacyPattern); + test.safe_run(); +} + +TEST(CV_CharucoDetection, accuracy_legacyPattern) { + const bool legacyPattern = true; + CV_CharucoDetection test(legacyPattern); test.safe_run(); } TEST(CV_CharucoPoseEstimation, accuracy) { - CV_CharucoPoseEstimation test; + const bool legacyPattern = false; + CV_CharucoPoseEstimation test(legacyPattern); + test.safe_run(); +} + +TEST(CV_CharucoPoseEstimation, accuracy_legacyPattern) { + const bool legacyPattern = true; + CV_CharucoPoseEstimation test(legacyPattern); test.safe_run(); }