diff --git a/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp b/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp index 61c8a00827..bf15437aa9 100644 --- a/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp +++ b/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp @@ -20,6 +20,8 @@ enum CornerRefineMethod{ CORNER_REFINE_APRILTAG, ///< Tag and corners detection based on the AprilTag 2 approach @cite wang2016iros }; +static constexpr float DEFAULT_VALID_BIT_ID_THRESHOLD{0.49f}; + /** @brief struct DetectorParameters is used by ArucoDetector */ struct CV_EXPORTS_W_SIMPLE DetectorParameters { @@ -49,7 +51,7 @@ struct CV_EXPORTS_W_SIMPLE DetectorParameters { aprilTagQuadSigma = 0.0; aprilTagMinClusterPixels = 5; aprilTagMaxNmaxima = 10; - aprilTagCriticalRad = (float)(10* CV_PI /180); + aprilTagCriticalRad = (float)(10 * CV_PI / 180); aprilTagMaxLineFitMse = 10.0; aprilTagMinWhiteBlackDiff = 5; aprilTagDeglitch = 0; @@ -57,6 +59,7 @@ struct CV_EXPORTS_W_SIMPLE DetectorParameters { useAruco3Detection = false; minSideLengthCanonicalImg = 32; minMarkerLengthRatioOriginalImg = 0.0; + validBitIdThreshold = DEFAULT_VALID_BIT_ID_THRESHOLD; } /** @brief Read a new set of DetectorParameters from FileNode (use FileStorage.root()). @@ -231,6 +234,9 @@ struct CV_EXPORTS_W_SIMPLE DetectorParameters { /// range [0,1], eq (2) from paper. The parameter tau_i has a direct influence on the processing speed. CV_PROP_RW float minMarkerLengthRatioOriginalImg; + + /// range [0,1], define the acceptable threshold when comparing the detected marker to the dictionary during marker identification. + CV_PROP_RW float validBitIdThreshold; }; /** @brief struct RefineParameters is used by ArucoDetector diff --git a/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp b/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp index 5f6fae64df..d918e08129 100644 --- a/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp +++ b/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp @@ -65,6 +65,12 @@ class CV_EXPORTS_W_SIMPLE Dictionary { */ CV_WRAP bool identify(const Mat &onlyBits, CV_OUT int &idx, CV_OUT int &rotation, double maxCorrectionRate) const; + /** @brief Given a matrix of pixel ratio raging from 0 to 1. Returns whether if marker is identified or not. + * + * Returns reference to the marker id in the dictionary (if any) and its rotation. + */ + CV_WRAP bool identify(const Mat &onlyCellPixelRatio, CV_OUT int &idx, CV_OUT int &rotation, double maxCorrectionRate, float validBitIdThreshold) const; + /** @brief Returns Hamming distance of the input bits to the specific id. * * If `allRotations` flag is set, the four possible marker rotations are considered @@ -84,6 +90,10 @@ class CV_EXPORTS_W_SIMPLE Dictionary { /** @brief Transform list of bytes to matrix of bits */ CV_WRAP static Mat getBitsFromByteList(const Mat &byteList, int markerSize, int rotationId = 0); + + /** @brief Get ground truth bits float + */ + CV_WRAP Mat getMarkerBits(int markerId, int rotationId = 0) const; }; diff --git a/modules/objdetect/misc/python/test/test_objdetect_aruco.py b/modules/objdetect/misc/python/test/test_objdetect_aruco.py index 45ffcf3241..10dffad514 100644 --- a/modules/objdetect/misc/python/test/test_objdetect_aruco.py +++ b/modules/objdetect/misc/python/test/test_objdetect_aruco.py @@ -323,6 +323,7 @@ class aruco_objdetect_test(NewOpenCVTests): imgSize = (500, 500) params = cv.aruco.DetectorParameters() params.minDistanceToBorder = 3 + params.validBitIdThreshold = 0.5 board = cv.aruco.CharucoBoard((4, 4), 0.03, 0.015, cv.aruco.getPredefinedDictionary(cv.aruco.DICT_6X6_250)) detector = cv.aruco.CharucoDetector(board, detectorParams=params) diff --git a/modules/objdetect/src/aruco/aruco_detector.cpp b/modules/objdetect/src/aruco/aruco_detector.cpp index 48b9ead516..434e709810 100644 --- a/modules/objdetect/src/aruco/aruco_detector.cpp +++ b/modules/objdetect/src/aruco/aruco_detector.cpp @@ -310,12 +310,11 @@ static void _detectInitialCandidates(const Mat &grey, vector > & /** - * @brief Given an input image and candidate corners, extract the bits of the candidate, including + * @brief Given an input image and candidate corners, extract the cell pixel ratio of the candidate, including * the border bits */ -static Mat _extractBits(InputArray _image, const vector& corners, int markerSize, - int markerBorderBits, int cellSize, double cellMarginRate, double minStdDevOtsu, - OutputArray _cellPixelRatio = noArray()) { +static Mat _extractCellPixelRatio(InputArray _image, const vector& corners, int markerSize, + int markerBorderBits, int cellSize, double cellMarginRate, double minStdDevOtsu) { CV_Assert(_image.getMat().channels() == 1); CV_Assert(corners.size() == 4ull); CV_Assert(markerBorderBits > 0 && cellSize > 0 && cellMarginRate >= 0 && cellMarginRate <= 0.5); @@ -339,11 +338,11 @@ static Mat _extractBits(InputArray _image, const vector& corners, int m warpPerspective(_image, resultImg, transformation, Size(resultImgSize, resultImgSize), INTER_NEAREST); - // output image containing the bits - Mat bits(markerSizeWithBorders, markerSizeWithBorders, CV_8UC1, Scalar::all(0)); + // output image containing the ratio of white pixels in each cell + Mat cellPixelRatio(markerSizeWithBorders, markerSizeWithBorders, CV_32FC1, Scalar::all(0)); // check if standard deviation is enough to apply Otsu - // if not enough, it probably means all bits are the same color (black or white) + // if not enough, it probably means all pixels are the same color (black or white) Mat mean, stddev; // Remove some border just to avoid border noise from perspective transformation Mat innerRegion = resultImg.colRange(cellSize / 2, resultImg.cols - cellSize / 2) @@ -351,18 +350,13 @@ static Mat _extractBits(InputArray _image, const vector& corners, int m meanStdDev(innerRegion, mean, stddev); if(stddev.ptr< double >(0)[0] < minStdDevOtsu) { // all black or all white, depending on mean value - if(mean.ptr< double >(0)[0] > 127) - bits.setTo(1); - else - bits.setTo(0); - if(_cellPixelRatio.needed()) bits.convertTo(_cellPixelRatio, CV_32F); - return bits; - } + if(mean.ptr< double >(0)[0] > 127){ + cellPixelRatio.setTo(1); + } else { + cellPixelRatio.setTo(0); + } - Mat cellPixelRatio; - if (_cellPixelRatio.needed()) { - _cellPixelRatio.create(markerSizeWithBorders, markerSizeWithBorders, CV_32FC1); - cellPixelRatio = _cellPixelRatio.getMatRef(); + return cellPixelRatio; } // now extract code, first threshold using Otsu @@ -377,38 +371,40 @@ static Mat _extractBits(InputArray _image, const vector& corners, int m cellSize - 2 * cellMarginPixels)); // count white pixels on each cell to assign its value size_t nZ = (size_t) countNonZero(square); - if(nZ > square.total() / 2) bits.at(y, x) = 1; // define the cell pixel ratio as the ratio of the white pixels. For inverted markers, the ratio will be inverted. - if(_cellPixelRatio.needed()) cellPixelRatio.at(y, x) = (nZ / (float)square.total()); + cellPixelRatio.at(y, x) = (nZ / (float)square.total()); } } - return bits; + return cellPixelRatio; } /** - * @brief Return number of erroneous bits in border, i.e. number of white bits in border. + * @brief Return number of erroneous bits in border, i.e. bits for which pixel ratio > validBitIdThreshold. */ -static int _getBorderErrors(const Mat &bits, int markerSize, int borderSize) { + static int _getBorderErrors(const Mat &cellPixelRatio, int markerSize, int borderSize, float validBitIdThreshold) { int sizeWithBorders = markerSize + 2 * borderSize; - CV_Assert(markerSize > 0 && bits.cols == sizeWithBorders && bits.rows == sizeWithBorders); + CV_Assert(markerSize > 0 && cellPixelRatio.cols == sizeWithBorders && cellPixelRatio.rows == sizeWithBorders); + // Get border error. cellPixelRatio has the opposite color as the borders. int totalErrors = 0; for(int y = 0; y < sizeWithBorders; y++) { for(int k = 0; k < borderSize; k++) { - if(bits.ptr(y)[k] != 0) totalErrors++; - if(bits.ptr(y)[sizeWithBorders - 1 - k] != 0) totalErrors++; + // Left and right vertical sides + if(cellPixelRatio.ptr(y)[k] > validBitIdThreshold) totalErrors++; + if(cellPixelRatio.ptr(y)[sizeWithBorders - 1 - k] > validBitIdThreshold) totalErrors++; } } for(int x = borderSize; x < sizeWithBorders - borderSize; x++) { for(int k = 0; k < borderSize; k++) { - if(bits.ptr(k)[x] != 0) totalErrors++; - if(bits.ptr(sizeWithBorders - 1 - k)[x] != 0) totalErrors++; + // Top and bottom horizontal sides + if(cellPixelRatio.ptr(k)[x] > validBitIdThreshold) totalErrors++; + if(cellPixelRatio.ptr(sizeWithBorders - 1 - k)[x] > validBitIdThreshold) totalErrors++; } } return totalErrors; @@ -482,49 +478,43 @@ static uint8_t _identifyOneCandidate(const Dictionary& dictionary, const Mat& _i scaled_corners[i].y = _corners[i].y * scale; } - Mat cellPixelRatio; - Mat candidateBits = - _extractBits(_image, scaled_corners, dictionary.markerSize, params.markerBorderBits, - params.perspectiveRemovePixelPerCell, - params.perspectiveRemoveIgnoredMarginPerCell, params.minOtsuStdDev, - cellPixelRatio); + Mat cellPixelRatio = + _extractCellPixelRatio(_image, scaled_corners, dictionary.markerSize, params.markerBorderBits, + params.perspectiveRemovePixelPerCell, + params.perspectiveRemoveIgnoredMarginPerCell, params.minOtsuStdDev); // analyze border bits int maximumErrorsInBorder = - int(dictionary.markerSize * dictionary.markerSize * params.maxErroneousBitsInBorderRate); + int(dictionary.markerSize * dictionary.markerSize * params.maxErroneousBitsInBorderRate); int borderErrors = - _getBorderErrors(candidateBits, dictionary.markerSize, params.markerBorderBits); + _getBorderErrors(cellPixelRatio, dictionary.markerSize, params.markerBorderBits, params.validBitIdThreshold); // check if it is a white marker if(params.detectInvertedMarker){ - // to get from 255 to 1 - Mat invertedImg = ~candidateBits-254; - int invBError = _getBorderErrors(invertedImg, dictionary.markerSize, params.markerBorderBits); + Mat invCellPixelRatio = 1.f - cellPixelRatio; + int invBError = _getBorderErrors(invCellPixelRatio, dictionary.markerSize, params.markerBorderBits, params.validBitIdThreshold); // white marker if(invBError maximumErrorsInBorder) return 0; // border is wrong // take only inner bits - Mat onlyBits = - candidateBits.rowRange(params.markerBorderBits, - candidateBits.rows - params.markerBorderBits) - .colRange(params.markerBorderBits, candidateBits.cols - params.markerBorderBits); + Mat onlyCellPixelRatio = + cellPixelRatio.rowRange(params.markerBorderBits, + cellPixelRatio.rows - params.markerBorderBits) + .colRange(params.markerBorderBits, cellPixelRatio.cols - params.markerBorderBits); - // try to indentify the marker - if(!dictionary.identify(onlyBits, idx, rotation, params.errorCorrectionRate)) + // try to identify the marker + if(!dictionary.identify(onlyCellPixelRatio, idx, rotation, params.errorCorrectionRate, params.validBitIdThreshold)) return 0; // compute the candidate's confidence if(confidenceNeeded) { - Mat groundTruthbits; - Mat bitsUints = dictionary.getBitsFromByteList(dictionary.bytesList.rowRange(idx, idx + 1), dictionary.markerSize, rotation); - bitsUints.convertTo(groundTruthbits, CV_32F); + Mat groundTruthbits = dictionary.getMarkerBits(idx, rotation); markerConfidence = _getMarkerConfidence(groundTruthbits, cellPixelRatio, dictionary.markerSize, params.markerBorderBits); } @@ -1403,11 +1393,14 @@ void ArucoDetector::refineDetectedMarkers(InputArray _image, const Board& _board if(refineParams.errorCorrectionRate >= 0) { // extract bits - Mat bits = _extractBits( + Mat cellPixelRatio = _extractCellPixelRatio( grey, rotatedMarker, dictionary.markerSize, detectorParams.markerBorderBits, detectorParams.perspectiveRemovePixelPerCell, detectorParams.perspectiveRemoveIgnoredMarginPerCell, detectorParams.minOtsuStdDev); + Mat bits; + cellPixelRatio.convertTo(bits, CV_8UC1); + Mat onlyBits = bits.rowRange(detectorParams.markerBorderBits, bits.rows - detectorParams.markerBorderBits) .colRange(detectorParams.markerBorderBits, bits.rows - detectorParams.markerBorderBits); diff --git a/modules/objdetect/src/aruco/aruco_dictionary.cpp b/modules/objdetect/src/aruco/aruco_dictionary.cpp index 350ccc63eb..7dfdcc3009 100644 --- a/modules/objdetect/src/aruco/aruco_dictionary.cpp +++ b/modules/objdetect/src/aruco/aruco_dictionary.cpp @@ -73,25 +73,32 @@ void Dictionary::writeDictionary(FileStorage& fs, const String &name) } -bool Dictionary::identify(const Mat &onlyBits, int &idx, int &rotation, double maxCorrectionRate) const { - CV_Assert(onlyBits.rows == markerSize && onlyBits.cols == markerSize); +bool Dictionary::identify(const Mat &onlyCellPixelRatio, CV_OUT int &idx, CV_OUT int &rotation, double maxCorrectionRate, float validBitIdThreshold) const { + CV_Assert(onlyCellPixelRatio.rows == markerSize && onlyCellPixelRatio.cols == markerSize); int maxCorrectionRecalculed = int(double(maxCorrectionBits) * maxCorrectionRate); - // get as a byte list - Mat candidateBytes = getByteListFromBits(onlyBits); - idx = -1; // by default, not found // search closest marker in dict for(int m = 0; m < bytesList.rows; m++) { int currentMinDistance = markerSize * markerSize + 1; int currentRotation = -1; - for(unsigned int r = 0; r < 4; r++) { - int currentHamming = cv::hal::normHamming( - bytesList.ptr(m)+r*candidateBytes.cols, - candidateBytes.ptr(), - candidateBytes.cols); + for(int r = 0; r < 4; r++) { + + Mat bitsRot = getBitsFromByteList(bytesList.rowRange(m, m + 1), markerSize, r); + bitsRot.convertTo(bitsRot, CV_32F); + + // Loop over all bits dictBitsList [m, markerSize * markerSize, 4]; onlyCellPixelRatio [markerSize, markerSize] + int currentHamming = 0; + for(int i = 0; i < markerSize; i++) { + for(int j = 0; j < markerSize; j++) { + // If detected bit is too far from the ground truth, consider it false. + if(fabs(onlyCellPixelRatio.at(i, j) - static_cast(bitsRot.at(i, j))) > validBitIdThreshold){ + currentHamming++; + } + } + } if(currentHamming < currentMinDistance) { currentMinDistance = currentHamming; @@ -111,6 +118,16 @@ bool Dictionary::identify(const Mat &onlyBits, int &idx, int &rotation, double m } +bool Dictionary::identify(const Mat &onlyBits, CV_OUT int &idx, CV_OUT int &rotation, double maxCorrectionRate) const { + CV_Assert(onlyBits.rows == markerSize && onlyBits.cols == markerSize); + + Mat candidateBitRatio; + onlyBits.convertTo(candidateBitRatio, CV_32F); + const float validBitIdThreshold = DEFAULT_VALID_BIT_ID_THRESHOLD; + return identify(candidateBitRatio, idx, rotation, maxCorrectionRate, validBitIdThreshold); +} + + int Dictionary::getDistanceToId(InputArray bits, int id, bool allRotations) const { CV_Assert(id >= 0 && id < bytesList.rows); @@ -147,7 +164,8 @@ void Dictionary::generateImageMarker(int id, int sidePixels, OutputArray _img, i Mat innerRegion = tinyMarker.rowRange(borderBits, tinyMarker.rows - borderBits) .colRange(borderBits, tinyMarker.cols - borderBits); // put inner bits - Mat bits = 255 * getBitsFromByteList(bytesList.rowRange(id, id + 1), markerSize); + Mat bits = getMarkerBits(id); + bits.convertTo(bits, CV_8U, 255.0); CV_Assert(innerRegion.total() == bits.total()); bits.copyTo(innerRegion); @@ -194,12 +212,28 @@ Mat Dictionary::getByteListFromBits(const Mat &bits) { } +Mat Dictionary::getMarkerBits(int markerId, int rotationId) const { + + const int nbRotations = 4; + CV_Assert(markerId < bytesList.rows); + CV_Assert(rotationId < nbRotations); + + Mat bits(markerSize, markerSize, CV_32F, Scalar::all(0)); + Mat bitsUints = getBitsFromByteList(bytesList.rowRange(markerId, markerId + 1), markerSize, rotationId); + bitsUints.convertTo(bits, CV_32F); + + CV_Assert(bits.rows == markerSize && bits.cols == markerSize); + return bits; +} + + Mat Dictionary::getBitsFromByteList(const Mat &byteList, int markerSize, int rotationId) { CV_Assert(byteList.total() > 0 && byteList.total() >= (unsigned int)markerSize * markerSize / 8 && byteList.total() <= (unsigned int)markerSize * markerSize / 8 + 1); - CV_Assert(rotationId >=0 && rotationId < 4); + CV_Assert(byteList.channels() >= 4); + CV_Assert(rotationId >= 0 && rotationId < 4); Mat bits(markerSize, markerSize, CV_8UC1, Scalar::all(0)); diff --git a/modules/objdetect/test/test_aruco_tutorial.cpp b/modules/objdetect/test/test_aruco_tutorial.cpp index 182c6a2cdc..686633504d 100644 --- a/modules/objdetect/test/test_aruco_tutorial.cpp +++ b/modules/objdetect/test/test_aruco_tutorial.cpp @@ -189,6 +189,7 @@ TEST(CV_ArucoTutorial, can_find_diamondmarkers) aruco::DetectorParameters detectorParams; detectorParams.readDetectorParameters(fs.root()); detectorParams.cornerRefinementMethod = aruco::CORNER_REFINE_APRILTAG; + detectorParams.validBitIdThreshold = 0.5f; aruco::CharucoBoard charucoBoard(Size(3, 3), 0.4f, 0.25f, dictionary); aruco::CharucoDetector detector(charucoBoard, aruco::CharucoParameters(), detectorParams); diff --git a/modules/objdetect/test/test_arucodetection.cpp b/modules/objdetect/test/test_arucodetection.cpp index d40bef6582..53f4302cc0 100644 --- a/modules/objdetect/test/test_arucodetection.cpp +++ b/modules/objdetect/test/test_arucodetection.cpp @@ -473,7 +473,7 @@ markerDetectionGT applyTemperingToMarkerCells(cv::Mat &marker, ++cellsTempered; // cell too tempered, no detection expected - if(cellTempConfig.cellRatioToTemper > 0.5f) { + if(cellTempConfig.cellRatioToTemper > params.validBitIdThreshold) { if(isBorder){ ++borderErrors; } else { @@ -556,7 +556,7 @@ static void runArucoDetectionConfidence(ArucoAlgParams arucoAlgParam) { // make sure there are no bits have any detection errors params.maxErroneousBitsInBorderRate = 0.0; params.errorCorrectionRate = 0.0; - params.perspectiveRemovePixelPerCell = 8; // esnsure that there is enough resolution to properly handle distortions + params.perspectiveRemovePixelPerCell = 8; // ensure that there is enough resolution to properly handle distortions aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_6X6_250), params); const bool detectInvertedMarker = (arucoAlgParam == ArucoAlgParams::DETECT_INVERTED_MARKER); @@ -674,6 +674,144 @@ static void runArucoDetectionConfidence(ArucoAlgParams arucoAlgParam) { } } + +// Helper struc and functions for CV_ArucoDetectionUnc +struct ArucoThresholdTestConfig { + MarkerTemperingConfig markerTemperingConfig; // Configuration of cells to invert (percentage, number and markerRegionToTemper) + float validBitIdThreshold; // range [0,1], define the acceptable threshold when comparing the detected marker to the dictionary during marker identification. + float perspectiveRemoveIgnoredMarginPerCell; // Width of the margin of pixels on each cell not considered for the marker identification + int markerBorderBits; // Number of bits of the marker border + float distortionRatio; // Percentage of offset used for perspective distortion, bigger means more distorted +}; + +/** + * @brief Test the param validBitIdThreshold + * Loops over a set of detector configurations (validBitIdThreshold, distortion, DetectorParameters such as markerBorderBits) + * For each configuration, it creates a synthetic image containing four markers arranged in a 2x2 grid. + * Each marker is generated with its own configuration (id, size, rotation). + * Make sure that markers are detected or not based on validBitIdThreshold and percentage of tempering. + * Finally, it runs the detector and checks that each marker is detected or not based on the threshold. + * + */ +static void runArucoDetectionThreshold(ArucoAlgParams arucoAlgParam) { + + aruco::DetectorParameters params; + // make sure there are no bits have any detection errors + params.perspectiveRemovePixelPerCell = 20; // ensure that there is enough resolution to properly handle distortions + params.maxErroneousBitsInBorderRate = 0.f; + params.errorCorrectionRate = 0.f; + aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_5X5_250), params); // Max correction: 6bits + + const bool detectInvertedMarker = (arucoAlgParam == ArucoAlgParams::DETECT_INVERTED_MARKER); + + // define several detector configurations to test different settings + // {{MarkerTemperingConfig}, validBitIdThreshold, perspectiveRemoveIgnoredMarginPerCell, markerBorderBits, distortionRatio} + + vector detectorConfigs = { + // No tempering, expect detection for every threshold + {{0.f, 0, MarkerRegionToTemper::ALL}, 0.3f, 0.f, 1, 0.f}, + {{0.f, 0, MarkerRegionToTemper::ALL}, 0.5f, 0.f, 1, 0.f}, + {{0.f, 0, MarkerRegionToTemper::ALL}, 0.9f, 0.f, 1, 0.f}, + // Include distortions + {{0.f, 0, MarkerRegionToTemper::ALL}, 0.3f, 0.f, 1, 0.05f}, + {{0.f, 0, MarkerRegionToTemper::ALL}, 0.5f, 0.f, 1, 0.1f}, + {{0.f, 0, MarkerRegionToTemper::ALL}, 0.9f, 0.f, 1, 0.2f}, + + // 20% temper, expect detection with threshold above 0.2 + {{0.2f, 5, MarkerRegionToTemper::BORDER}, 0.30f, 0.f, 1, 0.f}, // Detection + + {{0.2f, 1, MarkerRegionToTemper::BORDER}, 0.18f, 0.f, 1, 0.f}, // No detection + {{0.2f, 1, MarkerRegionToTemper::BORDER}, 0.18f, 0.f, 1, 0.f}, // No detection + {{0.2f, 10, MarkerRegionToTemper::INNER}, 0.22f, 0.f, 1, 0.f}, // Detection + {{0.2f, 1, MarkerRegionToTemper::INNER}, 0.18f, 0.f, 1, 0.f} // No detection + + // distortions + }; + + // define marker configurations for the 4 markers in each image + const int markerSidePixels = 700; // To simplify the cell division, markerSidePixels is a multiple of 7. (5x5 dict + 2 border bits) + vector markerCreationConfig = { + {0, markerSidePixels, markerRot::ROT_90}, // {id, markerSidePixels, rotation} + {1, markerSidePixels, markerRot::ROT_270}, + {2, markerSidePixels, markerRot::NONE}, + {3, markerSidePixels, markerRot::ROT_180} + }; + + // loop over each detector configuration + for (size_t cfgIdx = 0; cfgIdx < detectorConfigs.size(); cfgIdx++) { + ArucoThresholdTestConfig detCfg = detectorConfigs[cfgIdx]; + + // update detector parameters + params.validBitIdThreshold =detCfg.validBitIdThreshold; + params.perspectiveRemoveIgnoredMarginPerCell = detCfg.perspectiveRemoveIgnoredMarginPerCell; + params.markerBorderBits = detCfg.markerBorderBits; + params.detectInvertedMarker = detectInvertedMarker; + detector.setDetectorParameters(params); + + // create a blank image large enough to hold 4 markers in a 2x2 grid + const int margin = markerSidePixels / 2; + const int imageSize = (markerSidePixels * 2) + margin * 3; + Mat img(imageSize, imageSize, CV_8UC1, Scalar(255)); + + vector groundTruths; + const aruco::Dictionary &dictionary = detector.getDictionary(); + + // place each marker into the image + for (int row = 0; row < 2; row++) { + for (int col = 0; col < 2; col++) { + int index = row * 2 + col; + MarkerCreationConfig markerCfg = markerCreationConfig[index]; + // adjust marker id to be unique for each detector configuration + markerCfg.id += static_cast(cfgIdx * markerCreationConfig.size()); + + // generate img + Mat markerImg; + markerDetectionGT gt = generateTemperedMarkerImage(markerImg, markerCfg, detCfg.markerTemperingConfig, params, dictionary, detCfg.distortionRatio); + + groundTruths.push_back(gt); + + // place marker in the image + Point2f topLeft(static_cast(margin + col * (markerSidePixels + margin)), + static_cast(margin + row * (markerSidePixels + margin))); + placeMarker(img, markerImg, topLeft); + } + } + + // if testing inverted markers globally, invert the whole image + if (detectInvertedMarker) { + bitwise_not(img, img); + } + + // run detection. + vector> corners, rejected; + vector ids; + vector markerConfidence; + detector.detectMarkersWithConfidence(img, corners, ids, markerConfidence, rejected); + + ASSERT_EQ(ids.size(), corners.size()); + ASSERT_EQ(ids.size(), markerConfidence.size()); + + std::map confidenceById; + for (size_t i = 0; i < ids.size(); i++) { + confidenceById[ids[i]] = markerConfidence[i]; + } + + // verify that every marker is detected and its confidence is within tolerance + for (const auto& currentGT : groundTruths) { + const auto it = confidenceById.find(currentGT.id); + const bool detected = it != confidenceById.end(); + EXPECT_EQ(currentGT.expectDetection, detected) + << "Marker id: " << currentGT.id << " (detector config " << cfgIdx << ")"; + + if (currentGT.expectDetection && detected) { + EXPECT_NEAR(currentGT.confidence, it->second, 0.05) + << "Marker id: " << currentGT.id << " (detector config " << cfgIdx << ")"; + } + } + } +} + + /** * @brief Check max and min size in marker detection parameters */ @@ -981,6 +1119,14 @@ TEST(CV_InvertedFlagArucoDetectionConfidence, algorithmic) { } } +TEST(CV_ArucoDetectionThreshold, algorithmic) { + runArucoDetectionThreshold(ArucoAlgParams::USE_DEFAULT); +} + +TEST(CV_InvertedArucoDetectionThreshold, algorithmic) { + runArucoDetectionThreshold(ArucoAlgParams::DETECT_INVERTED_MARKER); +} + TEST(CV_ArucoDetectMarkers, regression_3192) { aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_4X4_50)); @@ -1304,6 +1450,7 @@ TEST_P(ArucoThreading, number_of_threads_does_not_change_results) aruco::DetectorParameters detectorParameters = detector.getDetectorParameters(); detectorParameters.cornerRefinementMethod = (int)GetParam(); + detectorParameters.validBitIdThreshold = 0.5f; detector.setDetectorParameters(detectorParameters); vector > original_corners; diff --git a/modules/objdetect/test/test_boarddetection.cpp b/modules/objdetect/test/test_boarddetection.cpp index 312bb5b56b..f95413ab32 100644 --- a/modules/objdetect/test/test_boarddetection.cpp +++ b/modules/objdetect/test/test_boarddetection.cpp @@ -66,6 +66,7 @@ void CV_ArucoBoardPose::run(int) { vector > corners; vector ids; detectorParameters.markerBorderBits = markerBorder; + detectorParameters.validBitIdThreshold = 0.5f; detector.setDetectorParameters(detectorParameters); detector.detectMarkers(img, corners, ids);