From 2ad416ed00f98d46bbc82c864aa0dcd8de34f64d Mon Sep 17 00:00:00 2001 From: Jonas Date: Sat, 28 Jan 2023 20:10:09 +0100 Subject: [PATCH 1/8] include pixel-based uncertainty for aruco marker detection --- .../opencv2/objdetect/aruco_detector.hpp | 26 +++ .../opencv2/objdetect/aruco_dictionary.hpp | 5 + .../objdetect/src/aruco/aruco_detector.cpp | 86 ++++++++-- .../objdetect/src/aruco/aruco_dictionary.cpp | 59 +++++++ .../objdetect/test/test_arucodetection.cpp | 154 ++++++++++++++++++ 5 files changed, 317 insertions(+), 13 deletions(-) diff --git a/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp b/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp index c081989645..017669975b 100644 --- a/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp +++ b/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp @@ -318,6 +318,32 @@ public: CV_WRAP void detectMarkers(InputArray image, OutputArrayOfArrays corners, OutputArray ids, OutputArrayOfArrays rejectedImgPoints = noArray()) const; + /** @brief Marker detection with uncertainty computation + * + * @param image input image + * @param corners vector of detected marker corners. For each marker, its four corners + * are provided, (e.g std::vector > ). For N detected markers, + * the dimensions of this array is Nx4. The order of the corners is clockwise. + * @param ids vector of identifiers of the detected markers. The identifier is of type int + * (e.g. std::vector). For N detected markers, the size of ids is also N. + * The identifiers have the same order than the markers in the imgPoints array. + * @param rejectedImgPoints contains the imgPoints of those squares whose inner code has not a + * correct codification. Useful for debugging purposes. + * @param markersUnc contains the normalized uncertainty [0;1] of the markers' detection, + * defined as percentage of incorrect pixel detections, with 0 describing a pixel perfect detection. + * The uncertainties are of type float (e.g. std::vector) + * + * Performs marker detection in the input image. Only markers included in the first specified dictionary + * are searched. For each detected marker, it returns the 2D position of its corner in the image + * and its corresponding identifier. + * Note that this function does not perform pose estimation. + * @note The function does not correct lens distortion or takes it into account. It's recommended to undistort + * input image with corresponding camera model, if camera parameters are known + * @sa undistort, estimatePoseSingleMarkers, estimatePoseBoard + */ + CV_WRAP void detectMarkersWithUnc(InputArray image, OutputArrayOfArrays corners, OutputArray ids, + OutputArrayOfArrays rejectedImgPoints = noArray(), OutputArray markersUnc = noArray()) const; + /** @brief Refine not detected markers based on the already detected and the board layout * * @param image input image diff --git a/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp b/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp index bc7b934b2a..760853546e 100644 --- a/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp +++ b/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp @@ -71,6 +71,11 @@ class CV_EXPORTS_W_SIMPLE Dictionary { */ CV_WRAP int getDistanceToId(InputArray bits, int id, bool allRotations = true) const; + /** @brief Given a matrix containing the percentage of white pixels in each marker cell, returns the normalized marker uncertainty [0;1] for the specific id. + * The uncertainty is defined as percentage of incorrect pixel detections, with 0 describing a pixel perfect detection. + * The rotation is set to 0,1,2,3 for [0, 90, 180, 270] deg CCW rotations. + */ + CV_WRAP float getMarkerUnc(InputArray whitePixelRatio, int id, int rotation = 0, int borderBits = 1) const; /** @brief Generate a canonical marker image */ diff --git a/modules/objdetect/src/aruco/aruco_detector.cpp b/modules/objdetect/src/aruco/aruco_detector.cpp index a0f5c64390..5ab726a703 100644 --- a/modules/objdetect/src/aruco/aruco_detector.cpp +++ b/modules/objdetect/src/aruco/aruco_detector.cpp @@ -313,10 +313,10 @@ static void _detectInitialCandidates(const Mat &grey, vector > & * the border bits */ static Mat _extractBits(InputArray _image, const vector& corners, int markerSize, - int markerBorderBits, int cellSize, double cellMarginRate, double minStdDevOtsu) { + int markerBorderBits, int cellSize, double cellMarginRate, double minStdDevOtsu, OutputArray _whitePixRatio = noArray()) { CV_Assert(_image.getMat().channels() == 1); CV_Assert(corners.size() == 4ull); - CV_Assert(markerBorderBits > 0 && cellSize > 0 && cellMarginRate >= 0 && cellMarginRate <= 1); + CV_Assert(markerBorderBits > 0 && cellSize > 0 && cellMarginRate >= 0 && cellMarginRate <= 0.5); CV_Assert(minStdDevOtsu >= 0); // number of bits in the marker @@ -339,6 +339,7 @@ static Mat _extractBits(InputArray _image, const vector& corners, int m // output image containing the bits Mat bits(markerSizeWithBorders, markerSizeWithBorders, CV_8UC1, Scalar::all(0)); + Mat whitePixRatio(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) @@ -349,10 +350,15 @@ 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) + if(mean.ptr< double >(0)[0] > 127){ bits.setTo(1); - else + whitePixRatio.setTo(1); + } + else { bits.setTo(0); + whitePixRatio.setTo(0); + } + if(_whitePixRatio.needed()) whitePixRatio.copyTo(_whitePixRatio); return bits; } @@ -369,9 +375,39 @@ static Mat _extractBits(InputArray _image, const vector& corners, int m // 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; + + if(_whitePixRatio.needed()){ + + // Get white pixel ratio from the complete cell + if(cellMarginPixels > 0){ + // Consider the full cell. If perspectiveRemoveIgnoredMarginPerCell != 0, manually include the pixels of the margins + Mat topRect = resultImg(Rect(Xstart - cellMarginPixels, Ystart - cellMarginPixels, cellSize, cellMarginPixels)); + size_t nZMarginPixels = (size_t) countNonZero(topRect); + size_t totalMarginPixels = topRect.total(); + + Mat leftRect = resultImg(Rect(Xstart - cellMarginPixels, Ystart, cellMarginPixels, cellSize - 2 * cellMarginPixels)); + nZMarginPixels += (size_t) countNonZero(leftRect); + totalMarginPixels += leftRect.total(); + + Mat bottomRect = resultImg(Rect(Xstart - cellMarginPixels, Ystart + cellSize - 2 * cellMarginPixels, cellSize, cellMarginPixels)); + nZMarginPixels += (size_t) countNonZero(bottomRect); + totalMarginPixels += bottomRect.total(); + + Mat rightRect = resultImg(Rect(Xstart + cellSize - 2 * cellMarginPixels, Ystart, cellMarginPixels, cellSize - 2 * cellMarginPixels)); + nZMarginPixels += (size_t) countNonZero(rightRect); + totalMarginPixels += rightRect.total(); + + whitePixRatio.at(y, x) = (nZ + nZMarginPixels) / (float)(square.total() + totalMarginPixels); + } + else { + whitePixRatio.at(y, x) = (nZ / (float)square.total()); + } + } } } + if(_whitePixRatio.needed()) whitePixRatio.copyTo(_whitePixRatio); + return bits; } @@ -412,6 +448,7 @@ static int _getBorderErrors(const Mat &bits, int markerSize, int borderSize) { static uint8_t _identifyOneCandidate(const Dictionary& dictionary, const Mat& _image, const vector& _corners, int& idx, const DetectorParameters& params, int& rotation, + float &markerUnc, const float scale = 1.f) { CV_DbgAssert(params.markerBorderBits > 0); uint8_t typ=1; @@ -423,10 +460,12 @@ static uint8_t _identifyOneCandidate(const Dictionary& dictionary, const Mat& _i scaled_corners[i].y = _corners[i].y * scale; } + Mat whitePixRatio; Mat candidateBits = _extractBits(_image, scaled_corners, dictionary.markerSize, params.markerBorderBits, params.perspectiveRemovePixelPerCell, - params.perspectiveRemoveIgnoredMarginPerCell, params.minOtsuStdDev); + params.perspectiveRemoveIgnoredMarginPerCell, params.minOtsuStdDev, + whitePixRatio); // analyze border bits int maximumErrorsInBorder = @@ -443,6 +482,7 @@ static uint8_t _identifyOneCandidate(const Dictionary& dictionary, const Mat& _i if(invBError 0); @@ -717,6 +766,7 @@ struct ArucoDetector::ArucoDetectorImpl { vector > candidates; vector > contours; vector ids; + vector markersUnc; /// STEP 2.a Detect marker candidates :: using AprilTag if(detectorParams.cornerRefinementMethod == (int)CORNER_REFINE_APRILTAG){ @@ -738,7 +788,7 @@ struct ArucoDetector::ArucoDetectorImpl { /// STEP 2: Check candidate codification (identify markers) identifyCandidates(grey, grey_pyramid, selectedCandidates, candidates, contours, - ids, dictionary, rejectedImgPoints); + ids, dictionary, rejectedImgPoints, markersUnc); /// STEP 3: Corner refinement :: use corner subpix if (detectorParams.cornerRefinementMethod == (int)CORNER_REFINE_SUBPIX) { @@ -766,7 +816,7 @@ struct ArucoDetector::ArucoDetectorImpl { // temporary variable to store the current candidates vector> currentCandidates; identifyCandidates(grey, grey_pyramid, candidatesPerDictionarySize.at(currentDictionary.markerSize), currentCandidates, contours, - ids, currentDictionary, rejectedImgPoints); + ids, currentDictionary, rejectedImgPoints, markersUnc); if (_dictIndices.needed()) { dictIndices.insert(dictIndices.end(), currentCandidates.size(), dictIndex); } @@ -849,6 +899,9 @@ struct ArucoDetector::ArucoDetectorImpl { if (_dictIndices.needed()) { Mat(dictIndices).copyTo(_dictIndices); } + if (_markersUnc.needed()) { + Mat(markersUnc).copyTo(_markersUnc); + } } /** @@ -982,9 +1035,10 @@ struct ArucoDetector::ArucoDetectorImpl { */ void identifyCandidates(const Mat& grey, const vector& image_pyr, vector& selectedContours, vector >& accepted, vector >& contours, - vector& ids, const Dictionary& currentDictionary, vector>& rejected) const { + vector& ids, const Dictionary& currentDictionary, vector>& rejected, vector& markersUnc) const { size_t ncandidates = selectedContours.size(); + vector markersUncTmp(ncandidates, 1.f); vector idsTmp(ncandidates, -1); vector rotated(ncandidates, 0); vector validCandidates(ncandidates, 0); @@ -1018,11 +1072,11 @@ struct ArucoDetector::ArucoDetectorImpl { } const float scale = detectorParams.useAruco3Detection ? img.cols / static_cast(grey.cols) : 1.f; - validCandidates[v] = _identifyOneCandidate(currentDictionary, img, selectedContours[v].corners, idsTmp[v], detectorParams, rotated[v], scale); + validCandidates[v] = _identifyOneCandidate(currentDictionary, img, selectedContours[v].corners, idsTmp[v], detectorParams, rotated[v], markersUncTmp[v], scale); if (validCandidates[v] == 0 && checkCloseContours) { for (const MarkerCandidate& closeMarkerCandidate: selectedContours[v].closeContours) { - validCandidates[v] = _identifyOneCandidate(currentDictionary, img, closeMarkerCandidate.corners, idsTmp[v], detectorParams, rotated[v], scale); + validCandidates[v] = _identifyOneCandidate(currentDictionary, img, closeMarkerCandidate.corners, idsTmp[v], detectorParams, rotated[v], markersUncTmp[v], scale); if (validCandidates[v] > 0) { selectedContours[v].corners = closeMarkerCandidate.corners; selectedContours[v].contour = closeMarkerCandidate.contour; @@ -1058,6 +1112,7 @@ struct ArucoDetector::ArucoDetectorImpl { accepted.push_back(selectedContours[i].corners); contours.push_back(selectedContours[i].contour); ids.push_back(idsTmp[i]); + markersUnc.push_back(markersUncTmp[i]); } else { rejected.push_back(selectedContours[i].corners); @@ -1103,14 +1158,19 @@ ArucoDetector::ArucoDetector(const vector &_dictionaries, arucoDetectorImpl = makePtr(_dictionaries, _detectorParams, _refineParams); } +void ArucoDetector::detectMarkersWithUnc(InputArray _image, OutputArrayOfArrays _corners, OutputArray _ids, + OutputArrayOfArrays _rejectedImgPoints, OutputArray _markersUnc) const { + arucoDetectorImpl->detectMarkers(_image, _corners, _ids, _rejectedImgPoints, noArray(), _markersUnc, DictionaryMode::Single); +} + void ArucoDetector::detectMarkers(InputArray _image, OutputArrayOfArrays _corners, OutputArray _ids, OutputArrayOfArrays _rejectedImgPoints) const { - arucoDetectorImpl->detectMarkers(_image, _corners, _ids, _rejectedImgPoints, noArray(), DictionaryMode::Single); + arucoDetectorImpl->detectMarkers(_image, _corners, _ids, _rejectedImgPoints, noArray(), noArray(), DictionaryMode::Single); } void ArucoDetector::detectMarkersMultiDict(InputArray _image, OutputArrayOfArrays _corners, OutputArray _ids, OutputArrayOfArrays _rejectedImgPoints, OutputArray _dictIndices) const { - arucoDetectorImpl->detectMarkers(_image, _corners, _ids, _rejectedImgPoints, _dictIndices, DictionaryMode::Multi); + arucoDetectorImpl->detectMarkers(_image, _corners, _ids, _rejectedImgPoints, _dictIndices, noArray(), DictionaryMode::Multi); } /** diff --git a/modules/objdetect/src/aruco/aruco_dictionary.cpp b/modules/objdetect/src/aruco/aruco_dictionary.cpp index 3d5f9b1bfd..15af31ea37 100644 --- a/modules/objdetect/src/aruco/aruco_dictionary.cpp +++ b/modules/objdetect/src/aruco/aruco_dictionary.cpp @@ -110,6 +110,65 @@ bool Dictionary::identify(const Mat &onlyBits, int &idx, int &rotation, double m return idx != -1; } +float Dictionary::getMarkerUnc(InputArray _whitePixRatio, int id, int rotation, int borderSize) const { + + CV_Assert(id >= 0 && id < bytesList.rows); + const int sizeWithBorders = markerSize + 2 * borderSize; + + Mat whitePixRatio = _whitePixRatio.getMat(); + + CV_Assert(markerSize > 0 && whitePixRatio.cols == sizeWithBorders && whitePixRatio.rows == sizeWithBorders); + + // Get border uncertainty. Assuming black borders, the uncertainty is the ratio of white pixels. + float tempBorderUnc = 0.f; + for(int y = 0; y < sizeWithBorders; y++) { + for(int k = 0; k < borderSize; k++) { + // Left and right vertical sides + tempBorderUnc += whitePixRatio.ptr(y)[k]; + tempBorderUnc += whitePixRatio.ptr(y)[sizeWithBorders - 1 - k]; + } + } + for(int x = borderSize; x < sizeWithBorders - borderSize; x++) { + for(int k = 0; k < borderSize; k++) { + // Top and bottom horizontal sides + tempBorderUnc += whitePixRatio.ptr(k)[x]; + tempBorderUnc += whitePixRatio.ptr(sizeWithBorders - 1 - k)[x]; + } + } + + // Get the ground truth bits and rotate them: + Mat groundTruthbits = getBitsFromByteList(bytesList.rowRange(id, id + 1), markerSize); + CV_Assert(groundTruthbits.cols == markerSize && groundTruthbits.rows == markerSize); + + if(rotation == 1){ + // 90 deg CCW + transpose(groundTruthbits, groundTruthbits); + flip(groundTruthbits, groundTruthbits,0); + + } else if (rotation == 2){ + // 180 deg CCW + flip(groundTruthbits, groundTruthbits,-1); + + } else if (rotation == 3){ + // 90 deg CW + transpose(groundTruthbits, groundTruthbits); + flip(groundTruthbits, groundTruthbits,1); + } + + // Get the inner marker uncertainty. For a white or black cell, the uncertainty is the ratio of black or white pixels respectively. + float tempInnerUnc = 0.f; + for(int y = borderSize; y < markerSize + borderSize; y++) { + for(int x = borderSize; x < markerSize + borderSize; x++) { + tempInnerUnc += abs(groundTruthbits.ptr(y - borderSize)[x - borderSize] - whitePixRatio.ptr(y)[x]); + } + } + + // Compute the overall normalized marker uncertainty + float normalizedMarkerUnc = (tempInnerUnc + tempBorderUnc) / (sizeWithBorders * sizeWithBorders); + + return normalizedMarkerUnc; +} + int Dictionary::getDistanceToId(InputArray bits, int id, bool allRotations) const { diff --git a/modules/objdetect/test/test_arucodetection.cpp b/modules/objdetect/test/test_arucodetection.cpp index fd957698b2..506f90ab0f 100644 --- a/modules/objdetect/test/test_arucodetection.cpp +++ b/modules/objdetect/test/test_arucodetection.cpp @@ -322,6 +322,148 @@ void CV_ArucoDetectionPerspective::run(int) { } +/** + * @brief Draw 2D synthetic markers, temper with some pixels, detect them and compute their uncertainty. + */ +class CV_ArucoDetectionUnc : public cvtest::BaseTest { + public: + CV_ArucoDetectionUnc(ArucoAlgParams arucoAlgParam) : arucoAlgParams(arucoAlgParam) {} + + protected: + void run(int); + ArucoAlgParams arucoAlgParams; +}; + + +void CV_ArucoDetectionUnc::run(int) { + + aruco::DetectorParameters params; + aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_6X6_250), params); + + // Params to test + float ingnoreMarginPerCell[3] = {0.0, 0.1, 0.2}; + int borderBitsTest[3] = {1,2,3}; + + const int markerSidePixels = 150; + const int imageSize = (markerSidePixels * 2) + 3 * (markerSidePixels / 2); + + // 25 images containing 4 markers. + for(int i = 0; i < 25; i++) { + + // Modify default params + params.perspectiveRemovePixelPerCell = 6 + i; + params.perspectiveRemoveIgnoredMarginPerCell = ingnoreMarginPerCell[i % 3]; + params.markerBorderBits = borderBitsTest[i % 3]; + + // draw synthetic image + vector groundTruthUncs; + vector groundTruthIds; + Mat img = Mat(imageSize, imageSize, CV_8UC1, Scalar::all(255)); + + // Invert the pixel value of a % of each cell [0%, 2%, 4%, ..., 48%] + float invertPixelPercent = 2 * i / 100.f; + int markerSizeWithBorders = 6 + 2 * params.markerBorderBits; + int cellSidePixelsSize = markerSidePixels / markerSizeWithBorders; + int cellSidePixelsInvert = int(sqrt(invertPixelPercent) * cellSidePixelsSize); + int cellMarginPixels = (cellSidePixelsSize - cellSidePixelsInvert) / 2; // Invert center of the cell + + float groundTruthUnc; + + // Generate 4 markers + for(int y = 0; y < 2; y++) { + for(int x = 0; x < 2; x++) { + Mat marker; + int id = i * 4 + y * 2 + x; + groundTruthIds.push_back(id); + + // Generate marker + aruco::generateImageMarker(detector.getDictionary(), id, markerSidePixels, marker, params.markerBorderBits); + + // Test all 4 rotations: [0, 90, 180, 270] + if(y == 0 && x == 0){ + // Rotate 90 deg CCW + cv::transpose(marker, marker); + cv::flip(marker, marker,0); + } else if (y == 0 && x == 1){ + // Rotate 90 deg CW + cv::transpose(marker, marker); + cv::flip(marker, marker,1); + } else if (y == 1 && x == 0){ + // Rotate 180 deg CCW + cv::flip(marker, marker,-1); + } + + // Invert the pixel value of a % of each cell [0%, 2%, 4%, ..., 48%] + if(cellSidePixelsInvert > 0){ + // loop over each cell + for(int k = 0; k < markerSizeWithBorders; k++) { + for(int p = 0; p < markerSizeWithBorders; p++) { + int Xstart = p * (cellSidePixelsSize) + cellMarginPixels; + int Ystart = k * (cellSidePixelsSize) + cellMarginPixels; + Mat square(marker, Rect(Xstart, Ystart, cellSidePixelsInvert, cellSidePixelsInvert)); + square = ~square; + } + } + } + + // Assume a perfect marker detection and thus a ground truth equal to the percentage of inverted pixels. + groundTruthUnc = markerSizeWithBorders * markerSizeWithBorders * cellSidePixelsInvert * cellSidePixelsInvert / (float)(markerSidePixels * markerSidePixels); + groundTruthUncs.push_back(groundTruthUnc); + + // Make sure that the marker is still detected when it was highly tempered. + if(groundTruthUnc >= 0.2) params.perspectiveRemoveIgnoredMarginPerCell = 0; + + // Copy marker into full image + Point2f firstCorner = + Point2f(markerSidePixels / 2.f + x * (1.5f * markerSidePixels), + markerSidePixels / 2.f + y * (1.5f * markerSidePixels)); + Mat aux = img.colRange((int)firstCorner.x, (int)firstCorner.x + markerSidePixels) + .rowRange((int)firstCorner.y, (int)firstCorner.y + markerSidePixels); + + marker.copyTo(aux); + } + } + + // Test inverted markers + if(ArucoAlgParams::DETECT_INVERTED_MARKER == arucoAlgParams){ + img = ~img; + params.detectInvertedMarker = true; + } + + detector.setDetectorParameters(params); + + // detect markers and compute uncertainty + vector > corners, rejected; + vector ids; + vector markerUnc; + + detector.detectMarkersWithUnc(img, corners, ids, rejected, markerUnc); + + // check detection results + for(unsigned int m = 0; m < groundTruthIds.size(); m++) { + int idx = -1; + for(unsigned int k = 0; k < ids.size(); k++) { + if(groundTruthIds[m] == ids[k]) { + idx = (int)k; + break; + } + } + if(idx == -1) { + ts->printf(cvtest::TS::LOG, "Marker not detected"); + ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH); + return; + } + double dist = (double)cv::abs(groundTruthUncs[m] - markerUnc[idx]); // TODO cvtest + if(dist > 0.05) { + ts->printf(cvtest::TS::LOG, "Marker: %d is incorrect: uncertainty: %.2f (GT: %.2f) ", m, markerUnc[idx], groundTruthUncs[m]); + ts->printf(cvtest::TS::LOG, ""); + ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY); + return; + } + } + } +} + /** * @brief Check max and min size in marker detection parameters */ @@ -552,6 +694,18 @@ TEST(CV_ArucoBitCorrection, algorithmic) { test.safe_run(); } +typedef CV_ArucoDetectionUnc CV_InvertedArucoDetectionUnc; + +TEST(CV_ArucoDetectionUnc, algorithmic) { + CV_ArucoDetectionUnc test(ArucoAlgParams::USE_DEFAULT); + test.safe_run(); +} + +TEST(CV_InvertedArucoDetectionUnc, algorithmic) { + CV_InvertedArucoDetectionUnc test(ArucoAlgParams::DETECT_INVERTED_MARKER); + test.safe_run(); +} + TEST(CV_ArucoDetectMarkers, regression_3192) { aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_4X4_50)); From 4f5dff64ff23d7c6cfcf5ab40fd7e1ff296561ff Mon Sep 17 00:00:00 2001 From: Jonas Date: Sat, 28 Jan 2023 21:57:49 +0100 Subject: [PATCH 2/8] update python test to unpack marker uncertainty --- modules/objdetect/misc/python/test/test_objdetect_aruco.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/objdetect/misc/python/test/test_objdetect_aruco.py b/modules/objdetect/misc/python/test/test_objdetect_aruco.py index 305d2bdca9..30e9f76bc6 100644 --- a/modules/objdetect/misc/python/test/test_objdetect_aruco.py +++ b/modules/objdetect/misc/python/test/test_objdetect_aruco.py @@ -156,7 +156,7 @@ class aruco_objdetect_test(NewOpenCVTests): gold_corners = np.array([[offset, offset],[marker_size+offset-1.0,offset], [marker_size+offset-1.0,marker_size+offset-1.0], [offset, marker_size+offset-1.0]], dtype=np.float32) - corners, ids, rejected = aruco_detector.detectMarkers(img_marker) + corners, ids, rejected, marker_unc = aruco_detector.detectMarkers(img_marker) self.assertEqual(1, len(ids)) self.assertEqual(id, ids[0]) @@ -171,7 +171,7 @@ class aruco_objdetect_test(NewOpenCVTests): board = cv.aruco.GridBoard(board_size, 5.0, 1.0, aruco_dict) board_image = board.generateImage((board_size[0]*50, board_size[1]*50), marginSize=10) - corners, ids, rejected = aruco_detector.detectMarkers(board_image) + corners, ids, rejected, marker_unc = aruco_detector.detectMarkers(board_image) self.assertEqual(board_size[0]*board_size[1], len(ids)) part_corners, part_ids, part_rejected = corners[:-1], ids[:-1], list(rejected) From 406dc309138cb9c10e26c16448b651ac9c5c3fe9 Mon Sep 17 00:00:00 2001 From: jonas Date: Fri, 14 Mar 2025 09:44:24 +0100 Subject: [PATCH 3/8] clean up rebase --- .../misc/python/test/test_objdetect_aruco.py | 4 ++-- .../objdetect/test/test_arucodetection.cpp | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/objdetect/misc/python/test/test_objdetect_aruco.py b/modules/objdetect/misc/python/test/test_objdetect_aruco.py index 30e9f76bc6..305d2bdca9 100644 --- a/modules/objdetect/misc/python/test/test_objdetect_aruco.py +++ b/modules/objdetect/misc/python/test/test_objdetect_aruco.py @@ -156,7 +156,7 @@ class aruco_objdetect_test(NewOpenCVTests): gold_corners = np.array([[offset, offset],[marker_size+offset-1.0,offset], [marker_size+offset-1.0,marker_size+offset-1.0], [offset, marker_size+offset-1.0]], dtype=np.float32) - corners, ids, rejected, marker_unc = aruco_detector.detectMarkers(img_marker) + corners, ids, rejected = aruco_detector.detectMarkers(img_marker) self.assertEqual(1, len(ids)) self.assertEqual(id, ids[0]) @@ -171,7 +171,7 @@ class aruco_objdetect_test(NewOpenCVTests): board = cv.aruco.GridBoard(board_size, 5.0, 1.0, aruco_dict) board_image = board.generateImage((board_size[0]*50, board_size[1]*50), marginSize=10) - corners, ids, rejected, marker_unc = aruco_detector.detectMarkers(board_image) + corners, ids, rejected = aruco_detector.detectMarkers(board_image) self.assertEqual(board_size[0]*board_size[1], len(ids)) part_corners, part_ids, part_rejected = corners[:-1], ids[:-1], list(rejected) diff --git a/modules/objdetect/test/test_arucodetection.cpp b/modules/objdetect/test/test_arucodetection.cpp index 506f90ab0f..7e28f9331a 100644 --- a/modules/objdetect/test/test_arucodetection.cpp +++ b/modules/objdetect/test/test_arucodetection.cpp @@ -341,8 +341,8 @@ void CV_ArucoDetectionUnc::run(int) { aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_6X6_250), params); // Params to test - float ingnoreMarginPerCell[3] = {0.0, 0.1, 0.2}; - int borderBitsTest[3] = {1,2,3}; + const float ingnoreMarginPerCell[3] = {0.0f, 0.1f, 0.2f}; + const int borderBitsTest[3] = {1,2,3}; const int markerSidePixels = 150; const int imageSize = (markerSidePixels * 2) + 3 * (markerSidePixels / 2); @@ -361,11 +361,11 @@ void CV_ArucoDetectionUnc::run(int) { Mat img = Mat(imageSize, imageSize, CV_8UC1, Scalar::all(255)); // Invert the pixel value of a % of each cell [0%, 2%, 4%, ..., 48%] - float invertPixelPercent = 2 * i / 100.f; - int markerSizeWithBorders = 6 + 2 * params.markerBorderBits; - int cellSidePixelsSize = markerSidePixels / markerSizeWithBorders; - int cellSidePixelsInvert = int(sqrt(invertPixelPercent) * cellSidePixelsSize); - int cellMarginPixels = (cellSidePixelsSize - cellSidePixelsInvert) / 2; // Invert center of the cell + const float invertPixelPercent = 2 * i / 100.f; + const int markerSizeWithBorders = 6 + 2 * params.markerBorderBits; + const int cellSidePixelsSize = markerSidePixels / markerSizeWithBorders; + const int cellSidePixelsInvert = int(sqrt(invertPixelPercent) * cellSidePixelsSize); + const int cellMarginPixels = (cellSidePixelsSize - cellSidePixelsInvert) / 2; // Invert center of the cell float groundTruthUnc; @@ -373,7 +373,7 @@ void CV_ArucoDetectionUnc::run(int) { for(int y = 0; y < 2; y++) { for(int x = 0; x < 2; x++) { Mat marker; - int id = i * 4 + y * 2 + x; + const int id = i * 4 + y * 2 + x; groundTruthIds.push_back(id); // Generate marker @@ -398,8 +398,8 @@ void CV_ArucoDetectionUnc::run(int) { // loop over each cell for(int k = 0; k < markerSizeWithBorders; k++) { for(int p = 0; p < markerSizeWithBorders; p++) { - int Xstart = p * (cellSidePixelsSize) + cellMarginPixels; - int Ystart = k * (cellSidePixelsSize) + cellMarginPixels; + const int Xstart = p * (cellSidePixelsSize) + cellMarginPixels; + const int Ystart = k * (cellSidePixelsSize) + cellMarginPixels; Mat square(marker, Rect(Xstart, Ystart, cellSidePixelsInvert, cellSidePixelsInvert)); square = ~square; } From c26776b5155313ec65bf6be4fe691875fa43eb81 Mon Sep 17 00:00:00 2001 From: jonas Date: Fri, 14 Mar 2025 13:42:33 +0100 Subject: [PATCH 4/8] Remove getMarkerUncertainty from dict public API --- .../opencv2/objdetect/aruco_dictionary.hpp | 6 -- .../objdetect/src/aruco/aruco_detector.cpp | 66 ++++++++++++++++++- .../objdetect/src/aruco/aruco_dictionary.cpp | 59 ----------------- 3 files changed, 65 insertions(+), 66 deletions(-) diff --git a/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp b/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp index 760853546e..3c274ac33b 100644 --- a/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp +++ b/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp @@ -71,12 +71,6 @@ class CV_EXPORTS_W_SIMPLE Dictionary { */ CV_WRAP int getDistanceToId(InputArray bits, int id, bool allRotations = true) const; - /** @brief Given a matrix containing the percentage of white pixels in each marker cell, returns the normalized marker uncertainty [0;1] for the specific id. - * The uncertainty is defined as percentage of incorrect pixel detections, with 0 describing a pixel perfect detection. - * The rotation is set to 0,1,2,3 for [0, 90, 180, 270] deg CCW rotations. - */ - CV_WRAP float getMarkerUnc(InputArray whitePixelRatio, int id, int rotation = 0, int borderBits = 1) const; - /** @brief Generate a canonical marker image */ CV_WRAP void generateImageMarker(int id, int sidePixels, OutputArray _img, int borderBits = 1) const; diff --git a/modules/objdetect/src/aruco/aruco_detector.cpp b/modules/objdetect/src/aruco/aruco_detector.cpp index 5ab726a703..2851e59caf 100644 --- a/modules/objdetect/src/aruco/aruco_detector.cpp +++ b/modules/objdetect/src/aruco/aruco_detector.cpp @@ -439,6 +439,70 @@ static int _getBorderErrors(const Mat &bits, int markerSize, int borderSize) { } +/** @brief Given a matrix containing the percentage of white pixels in each marker cell, returns the normalized marker uncertainty [0;1] for the specific id. + * The uncertainty is defined as percentage of incorrect pixel detections, with 0 describing a pixel perfect detection. + * The rotation is set to 0,1,2,3 for [0, 90, 180, 270] deg CCW rotations. + */ +static float _getMarkerUnc(const Dictionary& dictionary, const Mat &whitePixRatio, const int id, + const int rotation, const int borderSize) { + + CV_Assert(id >= 0 && id < dictionary.bytesList.rows); + const int markerSize = dictionary.markerSize; + const int sizeWithBorders = markerSize + 2 * borderSize; + + CV_Assert(markerSize > 0 && whitePixRatio.cols == sizeWithBorders && whitePixRatio.rows == sizeWithBorders); + + // Get border uncertainty. Assuming black borders, the uncertainty is the ratio of white pixels. + float tempBorderUnc = 0.f; + for(int y = 0; y < sizeWithBorders; y++) { + for(int k = 0; k < borderSize; k++) { + // Left and right vertical sides + tempBorderUnc += whitePixRatio.ptr(y)[k]; + tempBorderUnc += whitePixRatio.ptr(y)[sizeWithBorders - 1 - k]; + } + } + for(int x = borderSize; x < sizeWithBorders - borderSize; x++) { + for(int k = 0; k < borderSize; k++) { + // Top and bottom horizontal sides + tempBorderUnc += whitePixRatio.ptr(k)[x]; + tempBorderUnc += whitePixRatio.ptr(sizeWithBorders - 1 - k)[x]; + } + } + + // Get the ground truth bits and rotate them: + Mat groundTruthbits = dictionary.getBitsFromByteList(dictionary.bytesList.rowRange(id, id + 1), markerSize); + CV_Assert(groundTruthbits.cols == markerSize && groundTruthbits.rows == markerSize); + + if(rotation == 1){ + // 90 deg CCW + transpose(groundTruthbits, groundTruthbits); + flip(groundTruthbits, groundTruthbits,0); + + } else if (rotation == 2){ + // 180 deg CCW + flip(groundTruthbits, groundTruthbits,-1); + + } else if (rotation == 3){ + // 90 deg CW + transpose(groundTruthbits, groundTruthbits); + flip(groundTruthbits, groundTruthbits,1); + } + + // Get the inner marker uncertainty. For a white or black cell, the uncertainty is the ratio of black or white pixels respectively. + float tempInnerUnc = 0.f; + for(int y = borderSize; y < markerSize + borderSize; y++) { + for(int x = borderSize; x < markerSize + borderSize; x++) { + tempInnerUnc += abs(groundTruthbits.ptr(y - borderSize)[x - borderSize] - whitePixRatio.ptr(y)[x]); + } + } + + // Compute the overall normalized marker uncertainty + float normalizedMarkerUnc = (tempInnerUnc + tempBorderUnc) / (sizeWithBorders * sizeWithBorders); + + return normalizedMarkerUnc; +} + + /** * @brief Tries to identify one candidate given the dictionary * @return candidate typ. zero if the candidate is not valid, @@ -505,7 +569,7 @@ static uint8_t _identifyOneCandidate(const Dictionary& dictionary, const Mat& _i return 0; // compute the candidate's uncertainty - markerUnc = dictionary.getMarkerUnc(whitePixRatio, idx, rotation, params.markerBorderBits); + markerUnc = _getMarkerUnc(dictionary, whitePixRatio, idx, rotation, params.markerBorderBits); return typ; } diff --git a/modules/objdetect/src/aruco/aruco_dictionary.cpp b/modules/objdetect/src/aruco/aruco_dictionary.cpp index 15af31ea37..3d5f9b1bfd 100644 --- a/modules/objdetect/src/aruco/aruco_dictionary.cpp +++ b/modules/objdetect/src/aruco/aruco_dictionary.cpp @@ -110,65 +110,6 @@ bool Dictionary::identify(const Mat &onlyBits, int &idx, int &rotation, double m return idx != -1; } -float Dictionary::getMarkerUnc(InputArray _whitePixRatio, int id, int rotation, int borderSize) const { - - CV_Assert(id >= 0 && id < bytesList.rows); - const int sizeWithBorders = markerSize + 2 * borderSize; - - Mat whitePixRatio = _whitePixRatio.getMat(); - - CV_Assert(markerSize > 0 && whitePixRatio.cols == sizeWithBorders && whitePixRatio.rows == sizeWithBorders); - - // Get border uncertainty. Assuming black borders, the uncertainty is the ratio of white pixels. - float tempBorderUnc = 0.f; - for(int y = 0; y < sizeWithBorders; y++) { - for(int k = 0; k < borderSize; k++) { - // Left and right vertical sides - tempBorderUnc += whitePixRatio.ptr(y)[k]; - tempBorderUnc += whitePixRatio.ptr(y)[sizeWithBorders - 1 - k]; - } - } - for(int x = borderSize; x < sizeWithBorders - borderSize; x++) { - for(int k = 0; k < borderSize; k++) { - // Top and bottom horizontal sides - tempBorderUnc += whitePixRatio.ptr(k)[x]; - tempBorderUnc += whitePixRatio.ptr(sizeWithBorders - 1 - k)[x]; - } - } - - // Get the ground truth bits and rotate them: - Mat groundTruthbits = getBitsFromByteList(bytesList.rowRange(id, id + 1), markerSize); - CV_Assert(groundTruthbits.cols == markerSize && groundTruthbits.rows == markerSize); - - if(rotation == 1){ - // 90 deg CCW - transpose(groundTruthbits, groundTruthbits); - flip(groundTruthbits, groundTruthbits,0); - - } else if (rotation == 2){ - // 180 deg CCW - flip(groundTruthbits, groundTruthbits,-1); - - } else if (rotation == 3){ - // 90 deg CW - transpose(groundTruthbits, groundTruthbits); - flip(groundTruthbits, groundTruthbits,1); - } - - // Get the inner marker uncertainty. For a white or black cell, the uncertainty is the ratio of black or white pixels respectively. - float tempInnerUnc = 0.f; - for(int y = borderSize; y < markerSize + borderSize; y++) { - for(int x = borderSize; x < markerSize + borderSize; x++) { - tempInnerUnc += abs(groundTruthbits.ptr(y - borderSize)[x - borderSize] - whitePixRatio.ptr(y)[x]); - } - } - - // Compute the overall normalized marker uncertainty - float normalizedMarkerUnc = (tempInnerUnc + tempBorderUnc) / (sizeWithBorders * sizeWithBorders); - - return normalizedMarkerUnc; -} - int Dictionary::getDistanceToId(InputArray bits, int id, bool allRotations) const { From 92a24f0499a5579ced7a9ad44484adb30aa9b43e Mon Sep 17 00:00:00 2001 From: jonas Date: Fri, 14 Mar 2025 18:27:53 +0100 Subject: [PATCH 5/8] Update unit test coverage to include perspective-distorted cases --- .../objdetect/src/aruco/aruco_detector.cpp | 49 ++- .../objdetect/test/test_arucodetection.cpp | 295 ++++++++++++------ 2 files changed, 220 insertions(+), 124 deletions(-) diff --git a/modules/objdetect/src/aruco/aruco_detector.cpp b/modules/objdetect/src/aruco/aruco_detector.cpp index 2851e59caf..519ecb56be 100644 --- a/modules/objdetect/src/aruco/aruco_detector.cpp +++ b/modules/objdetect/src/aruco/aruco_detector.cpp @@ -313,7 +313,7 @@ static void _detectInitialCandidates(const Mat &grey, vector > & * the border bits */ static Mat _extractBits(InputArray _image, const vector& corners, int markerSize, - int markerBorderBits, int cellSize, double cellMarginRate, double minStdDevOtsu, OutputArray _whitePixRatio = noArray()) { + int markerBorderBits, int cellSize, double cellMarginRate, double minStdDevOtsu, OutputArray _cellPixelRatio = noArray()) { CV_Assert(_image.getMat().channels() == 1); CV_Assert(corners.size() == 4ull); CV_Assert(markerBorderBits > 0 && cellSize > 0 && cellMarginRate >= 0 && cellMarginRate <= 0.5); @@ -339,7 +339,7 @@ static Mat _extractBits(InputArray _image, const vector& corners, int m // output image containing the bits Mat bits(markerSizeWithBorders, markerSizeWithBorders, CV_8UC1, Scalar::all(0)); - Mat whitePixRatio(markerSizeWithBorders, markerSizeWithBorders, CV_32FC1, Scalar::all(0)); + 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) @@ -352,13 +352,13 @@ static Mat _extractBits(InputArray _image, const vector& corners, int m // all black or all white, depending on mean value if(mean.ptr< double >(0)[0] > 127){ bits.setTo(1); - whitePixRatio.setTo(1); + cellPixelRatio.setTo(1); } else { bits.setTo(0); - whitePixRatio.setTo(0); + cellPixelRatio.setTo(0); } - if(_whitePixRatio.needed()) whitePixRatio.copyTo(_whitePixRatio); + if(_cellPixelRatio.needed()) cellPixelRatio.copyTo(_cellPixelRatio); return bits; } @@ -376,7 +376,8 @@ static Mat _extractBits(InputArray _image, const vector& corners, int m size_t nZ = (size_t) countNonZero(square); if(nZ > square.total() / 2) bits.at(y, x) = 1; - if(_whitePixRatio.needed()){ + // define the cell pixel ratio as the ratio of the white pixels. For inverted markers, the ratio will be inverted. + if(_cellPixelRatio.needed()){ // Get white pixel ratio from the complete cell if(cellMarginPixels > 0){ @@ -397,16 +398,16 @@ static Mat _extractBits(InputArray _image, const vector& corners, int m nZMarginPixels += (size_t) countNonZero(rightRect); totalMarginPixels += rightRect.total(); - whitePixRatio.at(y, x) = (nZ + nZMarginPixels) / (float)(square.total() + totalMarginPixels); + cellPixelRatio.at(y, x) = (nZ + nZMarginPixels) / (float)(square.total() + totalMarginPixels); } else { - whitePixRatio.at(y, x) = (nZ / (float)square.total()); + cellPixelRatio.at(y, x) = (nZ / (float)square.total()); } } } } - if(_whitePixRatio.needed()) whitePixRatio.copyTo(_whitePixRatio); + if(_cellPixelRatio.needed()) cellPixelRatio.copyTo(_cellPixelRatio); return bits; } @@ -443,29 +444,29 @@ static int _getBorderErrors(const Mat &bits, int markerSize, int borderSize) { * The uncertainty is defined as percentage of incorrect pixel detections, with 0 describing a pixel perfect detection. * The rotation is set to 0,1,2,3 for [0, 90, 180, 270] deg CCW rotations. */ -static float _getMarkerUnc(const Dictionary& dictionary, const Mat &whitePixRatio, const int id, +static float _getMarkerUnc(const Dictionary& dictionary, const Mat &cellPixelRatio, const int id, const int rotation, const int borderSize) { CV_Assert(id >= 0 && id < dictionary.bytesList.rows); const int markerSize = dictionary.markerSize; const int sizeWithBorders = markerSize + 2 * borderSize; - CV_Assert(markerSize > 0 && whitePixRatio.cols == sizeWithBorders && whitePixRatio.rows == sizeWithBorders); + CV_Assert(markerSize > 0 && cellPixelRatio.cols == sizeWithBorders && cellPixelRatio.rows == sizeWithBorders); - // Get border uncertainty. Assuming black borders, the uncertainty is the ratio of white pixels. + // Get border uncertainty. cellPixelRatio has the opposite color as the borders --> it is the uncertainty. float tempBorderUnc = 0.f; for(int y = 0; y < sizeWithBorders; y++) { for(int k = 0; k < borderSize; k++) { // Left and right vertical sides - tempBorderUnc += whitePixRatio.ptr(y)[k]; - tempBorderUnc += whitePixRatio.ptr(y)[sizeWithBorders - 1 - k]; + tempBorderUnc += cellPixelRatio.ptr(y)[k]; + tempBorderUnc += cellPixelRatio.ptr(y)[sizeWithBorders - 1 - k]; } } for(int x = borderSize; x < sizeWithBorders - borderSize; x++) { for(int k = 0; k < borderSize; k++) { // Top and bottom horizontal sides - tempBorderUnc += whitePixRatio.ptr(k)[x]; - tempBorderUnc += whitePixRatio.ptr(sizeWithBorders - 1 - k)[x]; + tempBorderUnc += cellPixelRatio.ptr(k)[x]; + tempBorderUnc += cellPixelRatio.ptr(sizeWithBorders - 1 - k)[x]; } } @@ -492,7 +493,7 @@ static float _getMarkerUnc(const Dictionary& dictionary, const Mat &whitePixRati float tempInnerUnc = 0.f; for(int y = borderSize; y < markerSize + borderSize; y++) { for(int x = borderSize; x < markerSize + borderSize; x++) { - tempInnerUnc += abs(groundTruthbits.ptr(y - borderSize)[x - borderSize] - whitePixRatio.ptr(y)[x]); + tempInnerUnc += abs(groundTruthbits.ptr(y - borderSize)[x - borderSize] - cellPixelRatio.ptr(y)[x]); } } @@ -524,12 +525,12 @@ static uint8_t _identifyOneCandidate(const Dictionary& dictionary, const Mat& _i scaled_corners[i].y = _corners[i].y * scale; } - Mat whitePixRatio; + Mat cellPixelRatio; Mat candidateBits = _extractBits(_image, scaled_corners, dictionary.markerSize, params.markerBorderBits, params.perspectiveRemovePixelPerCell, params.perspectiveRemoveIgnoredMarginPerCell, params.minOtsuStdDev, - whitePixRatio); + cellPixelRatio); // analyze border bits int maximumErrorsInBorder = @@ -542,11 +543,11 @@ static uint8_t _identifyOneCandidate(const Dictionary& dictionary, const Mat& _i // to get from 255 to 1 Mat invertedImg = ~candidateBits-254; int invBError = _getBorderErrors(invertedImg, dictionary.markerSize, params.markerBorderBits); + cellPixelRatio = -1.0 * cellPixelRatio + 1; // white marker if(invBError 0) { + for (int row = 0; row < markerSizeWithBorders; row++) { + for (int col = 0; col < markerSizeWithBorders; col++) { + int xStart = col * cellSidePixelsSize + cellMarginPixels; + int yStart = row * cellSidePixelsSize + cellMarginPixels; + Rect cellRect(xStart, yStart, cellSidePixelsInvert, cellSidePixelsInvert); + Mat cellROI = marker(cellRect); + bitwise_not(cellROI, cellROI); + numCellsInverted++; + } + } + } + + // Compute ground-truth uncertainty as (inverted area)/(total marker area). + groundTruthUnc = (numCellsInverted * cellSidePixelsInvert * cellSidePixelsInvert) / + static_cast(markerConfig.markerSidePixels * markerConfig.markerSidePixels); + + // Optionally apply a distortion (a perspective warp) to simulate a non-ideal capture. + if (detectorConfig.distortionRatio > 0.f) { + vector src = { {0, 0}, + {static_cast(marker.cols), 0}, + {static_cast(marker.cols), static_cast(marker.rows)}, + {0, static_cast(marker.rows)} }; + float offset = marker.cols * detectorConfig.distortionRatio; // distortionRatio % offset for distortion + vector dst = { {offset, offset}, + {marker.cols - offset, 0}, + {marker.cols - offset, marker.rows - offset}, + {0, marker.rows - offset} }; + Mat M = getPerspectiveTransform(src, dst); + warpPerspective(marker, marker, M, marker.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar(255)); + } + + return marker; +} + + +/** + * @brief Copies a marker image into a larger image at the given top-left position. + */ +void placeMarker(Mat &img, const Mat &marker, const Point2f &topLeft) +{ + Rect roi(Point(static_cast(topLeft.x), static_cast(topLeft.y)), marker.size()); + marker.copyTo(img(roi)); +} + + +/** + * @brief Test the marker uncertainty computations. + * Loops over a set of detector configurations (expected uncertainty, 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). + * Finally, it runs the detector and checks that each marker is detected and + * that its computed uncertainty is close to the ground truth value. */ class CV_ArucoDetectionUnc : public cvtest::BaseTest { public: - CV_ArucoDetectionUnc(ArucoAlgParams arucoAlgParam) : arucoAlgParams(arucoAlgParam) {} + // The parameter arucoAlgParam allows switching between detecting normal and inverted markers. + CV_ArucoDetectionUnc(ArucoAlgParams algParam) : arucoAlgParam(algParam) {} protected: void run(int); - ArucoAlgParams arucoAlgParams; + ArucoAlgParams arucoAlgParam; }; @@ -340,123 +448,116 @@ void CV_ArucoDetectionUnc::run(int) { aruco::DetectorParameters params; aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_6X6_250), params); - // Params to test - const float ingnoreMarginPerCell[3] = {0.0f, 0.1f, 0.2f}; - const int borderBitsTest[3] = {1,2,3}; + const bool detectInvertedMarker = (arucoAlgParam == ArucoAlgParams::DETECT_INVERTED_MARKER); - const int markerSidePixels = 150; - const int imageSize = (markerSidePixels * 2) + 3 * (markerSidePixels / 2); + // Define several detector configurations to test different settings. + // perspectiveRemovePixelPerCell, perspectiveRemoveIgnoredMarginPerCell, markerBorderBits, invertPixelPercent, distortionRatio + vector detectorConfigs = { + // No margins, No distortion + {8, 0.0f, 1, 0.0f, 0.f}, + {8, 0.0f, 1, 0.01f, 0.f}, + {8, 0.0f, 2, 0.05f, 0.f}, + {8, 0.0f, 1, 0.1f, 0.f}, + // Margins, No distortion + {8, 0.05f, 1, 0.0f, 0.f}, + {8, 0.05f, 2, 0.01f, 0.f}, + {8, 0.1f, 3, 0.05f, 0.f}, + {8, 0.15f, 1, 0.1f, 0.f}, + // No margins, distortion + {8, 0.0f, 1, 0.0f, 0.01f}, + {8, 0.0f, 1, 0.01f, 0.02f}, + {8, 0.0f, 2, 0.05f, 0.05f}, + {8, 0.0f, 1, 0.1f, 0.1f}, + {8, 0.0f, 2, 0.1f, 0.2f}, + // Margins, distortion + {8, 0.05f, 2, 0.0f, 0.01f}, + {8, 0.05f, 1, 0.01f, 0.02f}, + {8, 0.1f, 2, 0.05f, 0.05f}, + {8, 0.15f, 1, 0.1f, 0.1f}, + {8, 0.0f, 1, 0.1f, 0.2f}, + }; - // 25 images containing 4 markers. - for(int i = 0; i < 25; i++) { + // Define marker configurations for the 4 markers. + const int markerSidePixels = 480; + // id, markerSidePixels, rotation + vector markerCreationConfig = { + {0, markerSidePixels, 90, }, + {1, markerSidePixels, 270,}, + {2, markerSidePixels, 0, }, + {3, markerSidePixels, 180,} + }; - // Modify default params - params.perspectiveRemovePixelPerCell = 6 + i; - params.perspectiveRemoveIgnoredMarginPerCell = ingnoreMarginPerCell[i % 3]; - params.markerBorderBits = borderBitsTest[i % 3]; + // Loop over each detector configuration. + for (size_t cfgIdx = 0; cfgIdx < detectorConfigs.size(); cfgIdx++) { + ArucoUncTestConfig detCfg = detectorConfigs[cfgIdx]; - // draw synthetic image - vector groundTruthUncs; + // Update detector parameters. + params.perspectiveRemovePixelPerCell = detCfg.perspectiveRemovePixelPerCell; + 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 groundTruthUncs; vector groundTruthIds; - Mat img = Mat(imageSize, imageSize, CV_8UC1, Scalar::all(255)); + const aruco::Dictionary &dictionary = detector.getDictionary(); - // Invert the pixel value of a % of each cell [0%, 2%, 4%, ..., 48%] - const float invertPixelPercent = 2 * i / 100.f; - const int markerSizeWithBorders = 6 + 2 * params.markerBorderBits; - const int cellSidePixelsSize = markerSidePixels / markerSizeWithBorders; - const int cellSidePixelsInvert = int(sqrt(invertPixelPercent) * cellSidePixelsSize); - const int cellMarginPixels = (cellSidePixelsSize - cellSidePixelsInvert) / 2; // Invert center of the cell + // 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()); + groundTruthIds.push_back(markerCfg.id); - float groundTruthUnc; + double gtUnc = 0.0; + Mat markerImg = generateMarkerImage(markerCfg, detCfg, dictionary, gtUnc); + groundTruthUncs.push_back(gtUnc); - // Generate 4 markers - for(int y = 0; y < 2; y++) { - for(int x = 0; x < 2; x++) { - Mat marker; - const int id = i * 4 + y * 2 + x; - groundTruthIds.push_back(id); - - // Generate marker - aruco::generateImageMarker(detector.getDictionary(), id, markerSidePixels, marker, params.markerBorderBits); - - // Test all 4 rotations: [0, 90, 180, 270] - if(y == 0 && x == 0){ - // Rotate 90 deg CCW - cv::transpose(marker, marker); - cv::flip(marker, marker,0); - } else if (y == 0 && x == 1){ - // Rotate 90 deg CW - cv::transpose(marker, marker); - cv::flip(marker, marker,1); - } else if (y == 1 && x == 0){ - // Rotate 180 deg CCW - cv::flip(marker, marker,-1); - } - - // Invert the pixel value of a % of each cell [0%, 2%, 4%, ..., 48%] - if(cellSidePixelsInvert > 0){ - // loop over each cell - for(int k = 0; k < markerSizeWithBorders; k++) { - for(int p = 0; p < markerSizeWithBorders; p++) { - const int Xstart = p * (cellSidePixelsSize) + cellMarginPixels; - const int Ystart = k * (cellSidePixelsSize) + cellMarginPixels; - Mat square(marker, Rect(Xstart, Ystart, cellSidePixelsInvert, cellSidePixelsInvert)); - square = ~square; - } - } - } - - // Assume a perfect marker detection and thus a ground truth equal to the percentage of inverted pixels. - groundTruthUnc = markerSizeWithBorders * markerSizeWithBorders * cellSidePixelsInvert * cellSidePixelsInvert / (float)(markerSidePixels * markerSidePixels); - groundTruthUncs.push_back(groundTruthUnc); - - // Make sure that the marker is still detected when it was highly tempered. - if(groundTruthUnc >= 0.2) params.perspectiveRemoveIgnoredMarginPerCell = 0; - - // Copy marker into full image - Point2f firstCorner = - Point2f(markerSidePixels / 2.f + x * (1.5f * markerSidePixels), - markerSidePixels / 2.f + y * (1.5f * markerSidePixels)); - Mat aux = img.colRange((int)firstCorner.x, (int)firstCorner.x + markerSidePixels) - .rowRange((int)firstCorner.y, (int)firstCorner.y + markerSidePixels); - - marker.copyTo(aux); + // Place marker in the image. + Point2f topLeft(margin + col * (markerSidePixels + margin), + margin + row * (markerSidePixels + margin)); + placeMarker(img, markerImg, topLeft); } } - // Test inverted markers - if(ArucoAlgParams::DETECT_INVERTED_MARKER == arucoAlgParams){ - img = ~img; - params.detectInvertedMarker = true; + // If testing inverted markers globally, invert the whole image. + if (detectInvertedMarker) { + bitwise_not(img, img); } - detector.setDetectorParameters(params); - - // detect markers and compute uncertainty - vector > corners, rejected; + // Run detection. + vector> corners, rejected; vector ids; vector markerUnc; - detector.detectMarkersWithUnc(img, corners, ids, rejected, markerUnc); - // check detection results - for(unsigned int m = 0; m < groundTruthIds.size(); m++) { - int idx = -1; - for(unsigned int k = 0; k < ids.size(); k++) { - if(groundTruthIds[m] == ids[k]) { - idx = (int)k; + // Verify that every marker is detected and its uncertainty is within tolerance. + for (size_t m = 0; m < groundTruthIds.size(); m++) { + int detectedIdx = -1; + for (size_t k = 0; k < ids.size(); k++) { + if (groundTruthIds[m] == ids[k]) { + detectedIdx = static_cast(k); break; } } - if(idx == -1) { - ts->printf(cvtest::TS::LOG, "Marker not detected"); + if (detectedIdx == -1) { + ts->printf(cvtest::TS::LOG, "Marker id %d: not detected (detector config %zu)\n", + groundTruthIds[m], cfgIdx); ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH); return; } - double dist = (double)cv::abs(groundTruthUncs[m] - markerUnc[idx]); // TODO cvtest - if(dist > 0.05) { - ts->printf(cvtest::TS::LOG, "Marker: %d is incorrect: uncertainty: %.2f (GT: %.2f) ", m, markerUnc[idx], groundTruthUncs[m]); - ts->printf(cvtest::TS::LOG, ""); + double diff = fabs(groundTruthUncs[m] - markerUnc[detectedIdx]); + if (diff > 0.05) { + ts->printf(cvtest::TS::LOG, + "Marker id %d: computed uncertainty %.2f differs from ground truth %.2f (diff=%.2f) (detector config %zu)\n", + groundTruthIds[m], markerUnc[detectedIdx], groundTruthUncs[m], diff, cfgIdx); ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY); return; } From 9f137b16e345ab3ef295136e499ca562ec507f98 Mon Sep 17 00:00:00 2001 From: jonas Date: Tue, 18 Mar 2025 19:04:54 +0100 Subject: [PATCH 6/8] Only compute uncertainty within perspectiveRemoveIgnoredMarginPerCell and extend getBitsFromByteList to include rotation --- .../opencv2/objdetect/aruco_dictionary.hpp | 2 +- .../objdetect/src/aruco/aruco_detector.cpp | 63 +++---------------- .../objdetect/src/aruco/aruco_dictionary.cpp | 14 +++-- .../objdetect/test/test_arucodetection.cpp | 27 +++++--- 4 files changed, 41 insertions(+), 65 deletions(-) diff --git a/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp b/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp index 3c274ac33b..6a90876bf9 100644 --- a/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp +++ b/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp @@ -83,7 +83,7 @@ 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); + CV_WRAP static Mat getBitsFromByteList(const Mat &byteList, int markerSize, int rotationId = 0); }; diff --git a/modules/objdetect/src/aruco/aruco_detector.cpp b/modules/objdetect/src/aruco/aruco_detector.cpp index 519ecb56be..194643e4b4 100644 --- a/modules/objdetect/src/aruco/aruco_detector.cpp +++ b/modules/objdetect/src/aruco/aruco_detector.cpp @@ -377,33 +377,7 @@ static Mat _extractBits(InputArray _image, const vector& corners, int m 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()){ - - // Get white pixel ratio from the complete cell - if(cellMarginPixels > 0){ - // Consider the full cell. If perspectiveRemoveIgnoredMarginPerCell != 0, manually include the pixels of the margins - Mat topRect = resultImg(Rect(Xstart - cellMarginPixels, Ystart - cellMarginPixels, cellSize, cellMarginPixels)); - size_t nZMarginPixels = (size_t) countNonZero(topRect); - size_t totalMarginPixels = topRect.total(); - - Mat leftRect = resultImg(Rect(Xstart - cellMarginPixels, Ystart, cellMarginPixels, cellSize - 2 * cellMarginPixels)); - nZMarginPixels += (size_t) countNonZero(leftRect); - totalMarginPixels += leftRect.total(); - - Mat bottomRect = resultImg(Rect(Xstart - cellMarginPixels, Ystart + cellSize - 2 * cellMarginPixels, cellSize, cellMarginPixels)); - nZMarginPixels += (size_t) countNonZero(bottomRect); - totalMarginPixels += bottomRect.total(); - - Mat rightRect = resultImg(Rect(Xstart + cellSize - 2 * cellMarginPixels, Ystart, cellMarginPixels, cellSize - 2 * cellMarginPixels)); - nZMarginPixels += (size_t) countNonZero(rightRect); - totalMarginPixels += rightRect.total(); - - cellPixelRatio.at(y, x) = (nZ + nZMarginPixels) / (float)(square.total() + totalMarginPixels); - } - else { - cellPixelRatio.at(y, x) = (nZ / (float)square.total()); - } - } + if(_cellPixelRatio.needed()) cellPixelRatio.at(y, x) = (nZ / (float)square.total()); } } @@ -444,13 +418,12 @@ static int _getBorderErrors(const Mat &bits, int markerSize, int borderSize) { * The uncertainty is defined as percentage of incorrect pixel detections, with 0 describing a pixel perfect detection. * The rotation is set to 0,1,2,3 for [0, 90, 180, 270] deg CCW rotations. */ -static float _getMarkerUnc(const Dictionary& dictionary, const Mat &cellPixelRatio, const int id, - const int rotation, const int borderSize) { - CV_Assert(id >= 0 && id < dictionary.bytesList.rows); - const int markerSize = dictionary.markerSize; +static float _getMarkerUnc(const Mat& groundTruthbits, const Mat &cellPixelRatio, const int markerSize, const int borderSize) { + + CV_Assert(markerSize == groundTruthbits.cols && markerSize == groundTruthbits.rows); + const int sizeWithBorders = markerSize + 2 * borderSize; - CV_Assert(markerSize > 0 && cellPixelRatio.cols == sizeWithBorders && cellPixelRatio.rows == sizeWithBorders); // Get border uncertainty. cellPixelRatio has the opposite color as the borders --> it is the uncertainty. @@ -470,30 +443,11 @@ static float _getMarkerUnc(const Dictionary& dictionary, const Mat &cellPixelRat } } - // Get the ground truth bits and rotate them: - Mat groundTruthbits = dictionary.getBitsFromByteList(dictionary.bytesList.rowRange(id, id + 1), markerSize); - CV_Assert(groundTruthbits.cols == markerSize && groundTruthbits.rows == markerSize); - - if(rotation == 1){ - // 90 deg CCW - transpose(groundTruthbits, groundTruthbits); - flip(groundTruthbits, groundTruthbits,0); - - } else if (rotation == 2){ - // 180 deg CCW - flip(groundTruthbits, groundTruthbits,-1); - - } else if (rotation == 3){ - // 90 deg CW - transpose(groundTruthbits, groundTruthbits); - flip(groundTruthbits, groundTruthbits,1); - } - // Get the inner marker uncertainty. For a white or black cell, the uncertainty is the ratio of black or white pixels respectively. float tempInnerUnc = 0.f; for(int y = borderSize; y < markerSize + borderSize; y++) { for(int x = borderSize; x < markerSize + borderSize; x++) { - tempInnerUnc += abs(groundTruthbits.ptr(y - borderSize)[x - borderSize] - cellPixelRatio.ptr(y)[x]); + tempInnerUnc += abs(groundTruthbits.ptr(y - borderSize)[x - borderSize] - cellPixelRatio.ptr(y)[x]); } } @@ -564,7 +518,10 @@ static uint8_t _identifyOneCandidate(const Dictionary& dictionary, const Mat& _i return 0; // compute the candidate's uncertainty - markerUnc = _getMarkerUnc(dictionary, cellPixelRatio, idx, rotation, params.markerBorderBits); + Mat groundTruthbits; + Mat bitsUints = dictionary.getBitsFromByteList(dictionary.bytesList.rowRange(idx, idx + 1), dictionary.markerSize, rotation); + bitsUints.convertTo(groundTruthbits, CV_32F); + markerUnc = _getMarkerUnc(groundTruthbits, cellPixelRatio, dictionary.markerSize, params.markerBorderBits); return typ; } diff --git a/modules/objdetect/src/aruco/aruco_dictionary.cpp b/modules/objdetect/src/aruco/aruco_dictionary.cpp index 3d5f9b1bfd..15f198acba 100644 --- a/modules/objdetect/src/aruco/aruco_dictionary.cpp +++ b/modules/objdetect/src/aruco/aruco_dictionary.cpp @@ -194,17 +194,23 @@ Mat Dictionary::getByteListFromBits(const Mat &bits) { } -Mat Dictionary::getBitsFromByteList(const Mat &byteList, int markerSize) { +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 < 4); + Mat bits(markerSize, markerSize, CV_8UC1, Scalar::all(0)); unsigned char base2List[] = { 128, 64, 32, 16, 8, 4, 2, 1 }; + + // Use a base offset for the selected rotation + int nbytes = (bits.cols * bits.rows + 8 - 1) / 8; // integer ceil + int base = rotationId * nbytes; int currentByteIdx = 0; - // we only need the bytes in normal rotation - unsigned char currentByte = byteList.ptr()[0]; + unsigned char currentByte = byteList.ptr()[base + currentByteIdx]; int currentBit = 0; + for(int row = 0; row < bits.rows; row++) { for(int col = 0; col < bits.cols; col++) { if(currentByte >= base2List[currentBit]) { @@ -214,7 +220,7 @@ Mat Dictionary::getBitsFromByteList(const Mat &byteList, int markerSize) { currentBit++; if(currentBit == 8) { currentByteIdx++; - currentByte = byteList.ptr()[currentByteIdx]; + currentByte = byteList.ptr()[base + currentByteIdx]; // if not enough bits for one more byte, we are in the end // update bit position accordingly if(8 * (currentByteIdx + 1) > (int)bits.total()) diff --git a/modules/objdetect/test/test_arucodetection.cpp b/modules/objdetect/test/test_arucodetection.cpp index 2b0fca6570..8f21add18b 100644 --- a/modules/objdetect/test/test_arucodetection.cpp +++ b/modules/objdetect/test/test_arucodetection.cpp @@ -371,18 +371,21 @@ Mat generateMarkerImage(const MarkerCreationConfig &markerConfig, const ArucoUnc // Compute the number of cells in one dimension. const int markerSizeWithBorders = dictionary.markerSize + 2 * detectorConfig.markerBorderBits; const int cellSidePixelsSize = markerConfig.markerSidePixels / markerSizeWithBorders; + // Size of the cell used for marker identification and uncertainty computations + const int cellMarginPixels = int(detectorConfig.perspectiveRemoveIgnoredMarginPerCell * cellSidePixelsSize); + const int innerCellSizePixelsSize = cellSidePixelsSize - 2 * cellMarginPixels; // We want the inverted square area to have an area ratio equal to invertPixelPercent. - // That is: (cellSidePixelsInvert/cellSidePixelsSize)^2 = invertPixelPercent. - int cellSidePixelsInvert = int(cellSidePixelsSize * std::sqrt(detectorConfig.invertPixelPercent)); - int cellMarginPixels = (cellSidePixelsSize - cellSidePixelsInvert) / 2; + // That is: (cellSidePixelsInvert/innerCellSizePixelsSize)^2 = invertPixelPercent. + const int cellSidePixelsInvert = int(innerCellSizePixelsSize * std::sqrt(detectorConfig.invertPixelPercent)); + const int inversionOffsetPixels = (cellSidePixelsSize - cellSidePixelsInvert) / 2; int numCellsInverted = 0; // Loop over each cell in the marker grid. if (cellSidePixelsInvert > 0) { for (int row = 0; row < markerSizeWithBorders; row++) { for (int col = 0; col < markerSizeWithBorders; col++) { - int xStart = col * cellSidePixelsSize + cellMarginPixels; - int yStart = row * cellSidePixelsSize + cellMarginPixels; + int xStart = col * cellSidePixelsSize + inversionOffsetPixels; + int yStart = row * cellSidePixelsSize + inversionOffsetPixels; Rect cellRect(xStart, yStart, cellSidePixelsInvert, cellSidePixelsInvert); Mat cellROI = marker(cellRect); bitwise_not(cellROI, cellROI); @@ -391,9 +394,9 @@ Mat generateMarkerImage(const MarkerCreationConfig &markerConfig, const ArucoUnc } } - // Compute ground-truth uncertainty as (inverted area)/(total marker area). + // Compute ground-truth uncertainty as (inverted area)/(total area used to detect marker). groundTruthUnc = (numCellsInverted * cellSidePixelsInvert * cellSidePixelsInvert) / - static_cast(markerConfig.markerSidePixels * markerConfig.markerSidePixels); + static_cast(markerSizeWithBorders * innerCellSizePixelsSize * markerSizeWithBorders * innerCellSizePixelsSize); // Optionally apply a distortion (a perspective warp) to simulate a non-ideal capture. if (detectorConfig.distortionRatio > 0.f) { @@ -553,6 +556,16 @@ void CV_ArucoDetectionUnc::run(int) { ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH); return; } + + double gtComputationDiff = fabs(groundTruthUncs[m] - detCfg.invertPixelPercent); + if (gtComputationDiff > 0.02) { + ts->printf(cvtest::TS::LOG, + "Marker id %d: ground truth uncertainty %.2f differs test config uncertainty %.2f (diff=%.2f) (detector config %zu)\n", + groundTruthIds[m], groundTruthUncs[m], detCfg.invertPixelPercent, gtComputationDiff, cfgIdx); + ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY); + return; + } + double diff = fabs(groundTruthUncs[m] - markerUnc[detectedIdx]); if (diff > 0.05) { ts->printf(cvtest::TS::LOG, From 3314b7d085b4731e7a9d249d71dca45946dedc15 Mon Sep 17 00:00:00 2001 From: jonas Date: Tue, 18 Mar 2025 19:12:31 +0100 Subject: [PATCH 7/8] markerUnc as required outputArray when calling detectMarkersWithUnc --- .../include/opencv2/objdetect/aruco_detector.hpp | 8 ++++---- modules/objdetect/src/aruco/aruco_detector.cpp | 4 ++-- modules/objdetect/test/test_arucodetection.cpp | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp b/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp index 017669975b..fbc864076a 100644 --- a/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp +++ b/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp @@ -327,11 +327,11 @@ public: * @param ids vector of identifiers of the detected markers. The identifier is of type int * (e.g. std::vector). For N detected markers, the size of ids is also N. * The identifiers have the same order than the markers in the imgPoints array. - * @param rejectedImgPoints contains the imgPoints of those squares whose inner code has not a - * correct codification. Useful for debugging purposes. * @param markersUnc contains the normalized uncertainty [0;1] of the markers' detection, * defined as percentage of incorrect pixel detections, with 0 describing a pixel perfect detection. * The uncertainties are of type float (e.g. std::vector) + * @param rejectedImgPoints contains the imgPoints of those squares whose inner code has not a + * correct codification. Useful for debugging purposes. * * Performs marker detection in the input image. Only markers included in the first specified dictionary * are searched. For each detected marker, it returns the 2D position of its corner in the image @@ -341,8 +341,8 @@ public: * input image with corresponding camera model, if camera parameters are known * @sa undistort, estimatePoseSingleMarkers, estimatePoseBoard */ - CV_WRAP void detectMarkersWithUnc(InputArray image, OutputArrayOfArrays corners, OutputArray ids, - OutputArrayOfArrays rejectedImgPoints = noArray(), OutputArray markersUnc = noArray()) const; + CV_WRAP void detectMarkersWithUnc(InputArray image, OutputArrayOfArrays corners, OutputArray ids, OutputArray markersUnc, + OutputArrayOfArrays rejectedImgPoints = noArray()) const; /** @brief Refine not detected markers based on the already detected and the board layout * diff --git a/modules/objdetect/src/aruco/aruco_detector.cpp b/modules/objdetect/src/aruco/aruco_detector.cpp index 194643e4b4..97ea8dd351 100644 --- a/modules/objdetect/src/aruco/aruco_detector.cpp +++ b/modules/objdetect/src/aruco/aruco_detector.cpp @@ -1174,8 +1174,8 @@ ArucoDetector::ArucoDetector(const vector &_dictionaries, arucoDetectorImpl = makePtr(_dictionaries, _detectorParams, _refineParams); } -void ArucoDetector::detectMarkersWithUnc(InputArray _image, OutputArrayOfArrays _corners, OutputArray _ids, - OutputArrayOfArrays _rejectedImgPoints, OutputArray _markersUnc) const { +void ArucoDetector::detectMarkersWithUnc(InputArray _image, OutputArrayOfArrays _corners, OutputArray _ids, OutputArray _markersUnc, + OutputArrayOfArrays _rejectedImgPoints) const { arucoDetectorImpl->detectMarkers(_image, _corners, _ids, _rejectedImgPoints, noArray(), _markersUnc, DictionaryMode::Single); } diff --git a/modules/objdetect/test/test_arucodetection.cpp b/modules/objdetect/test/test_arucodetection.cpp index 8f21add18b..8542ca9063 100644 --- a/modules/objdetect/test/test_arucodetection.cpp +++ b/modules/objdetect/test/test_arucodetection.cpp @@ -539,7 +539,7 @@ void CV_ArucoDetectionUnc::run(int) { vector> corners, rejected; vector ids; vector markerUnc; - detector.detectMarkersWithUnc(img, corners, ids, rejected, markerUnc); + detector.detectMarkersWithUnc(img, corners, ids, markerUnc, rejected); // Verify that every marker is detected and its uncertainty is within tolerance. for (size_t m = 0; m < groundTruthIds.size(); m++) { From 1e020196ceaeb127c05ac318bca9f8e492026705 Mon Sep 17 00:00:00 2001 From: jonas Date: Thu, 20 Mar 2025 18:18:49 +0100 Subject: [PATCH 8/8] Improve unit testing coverage --- .../objdetect/test/test_arucodetection.cpp | 397 ++++++++++++------ 1 file changed, 260 insertions(+), 137 deletions(-) diff --git a/modules/objdetect/test/test_arucodetection.cpp b/modules/objdetect/test/test_arucodetection.cpp index 8542ca9063..a008452fd1 100644 --- a/modules/objdetect/test/test_arucodetection.cpp +++ b/modules/objdetect/test/test_arucodetection.cpp @@ -321,99 +321,213 @@ void CV_ArucoDetectionPerspective::run(int) { } } -// Helper struc and functions for CV_ArucoDetectionUnc +// Helper struct and functions for CV_ArucoDetectionUnc + +// Inverts a square subregion inside selected cells of a marker to simulate uncertainty +enum class MarkerRegionToTemper { + BORDER, // Only invert cells within the marker border bits + INNER, // Only invert cells in the inner part of the marker (excluding borders) + ALL // Invert any cells +}; + +// Define the characteristics of cell inversions +struct MarkerTemperingConfig { + float cellRatioToTemper; // [0,1] ratio of the cell to invert + int numCellsToTemper; // Number of cells to invert + MarkerRegionToTemper markerRegionToTemper; // Which cells to invert (BORDER, INNER, ALL) +}; + +// Test configs for CV_ArucoDetectionUnc struct ArucoUncTestConfig { - // Number of bits (per dimension) for each cell of the marker when removing the perspective (default 4). - int perspectiveRemovePixelPerCell; - // Width of the margin of pixels on each cell not considered for the determination of the cell bit. - // This parameter is relative to the total size of the cell. - // For instance if the cell size is 40 pixels and the value of this parameter is 0.1, a margin of 40*0.1=4 pixels is ignored in the cells. - float perspectiveRemoveIgnoredMarginPerCell; - // Number of bits of the marker border, i.e. marker border width (default 1). - int markerBorderBits; - // Fraction of tempered (inverted) pixels per cell (area ratio, e.g. 0.02 for 2%) - float invertPixelPercent; - // Percentage of offset used for perspective distortion, bigger means more distorted - float distortionRatio; + MarkerTemperingConfig markerTemperingConfig; // Configuration of cells to invert (percentage, number and markerRegionToTemper) + 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 +}; + +enum class markerRot +{ + NONE = 0, + ROT_90, + ROT_180, + ROT_270 +}; + +struct markerDetectionGT { + int id; // Marker identification + double uncertainty; // Pixel-based uncertainty defined as inverted area / total area + bool expectDetection; // True if we expect to detect the marker }; struct MarkerCreationConfig { - int id; // Unique marker ID (will be offset per test run) - int markerSidePixels; // Marker size (in pixels) - int rotation; // Rotation of the marker in degrees (0, 90, 180, 270) + int id; // Marker identification + int markerSidePixels; // Marker size (in pixels) + markerRot rotation; // Rotation of the marker in degrees (0, 90, 180, 270) }; -/** - * @brief Create a synthetic image of a marker - * Applies an optional rotation and an optional perspective warp to simulate a distorted marker. - * Inverts a square region within each cell (including borders) to simulate uncertainty in detection. - * Computes the ground-truth uncertainty as the ratio of inverted area to the total marker area. - */ -Mat generateMarkerImage(const MarkerCreationConfig &markerConfig, const ArucoUncTestConfig &detectorConfig, - const aruco::Dictionary &dictionary, double &groundTruthUnc) +void rotateMarker(Mat &marker, const markerRot rotation) { - Mat marker; - // Generate the synthetic marker image - aruco::generateImageMarker(dictionary, markerConfig.id, markerConfig.markerSidePixels, - marker, detectorConfig.markerBorderBits); + if(rotation == markerRot::NONE) + return; - // Rotate the marker if needed. - if (markerConfig.rotation == 90) { + if (rotation == markerRot::ROT_90) { cv::transpose(marker, marker); cv::flip(marker, marker, 0); - } else if (markerConfig.rotation == 180) { + } else if (rotation == markerRot::ROT_180) { cv::flip(marker, marker, -1); - } else if (markerConfig.rotation == 270) { + } else if (rotation == markerRot::ROT_270) { cv::transpose(marker, marker); cv::flip(marker, marker, 1); } +} - // Compute the number of cells in one dimension. - const int markerSizeWithBorders = dictionary.markerSize + 2 * detectorConfig.markerBorderBits; - const int cellSidePixelsSize = markerConfig.markerSidePixels / markerSizeWithBorders; - // Size of the cell used for marker identification and uncertainty computations - const int cellMarginPixels = int(detectorConfig.perspectiveRemoveIgnoredMarginPerCell * cellSidePixelsSize); - const int innerCellSizePixelsSize = cellSidePixelsSize - 2 * cellMarginPixels; - // We want the inverted square area to have an area ratio equal to invertPixelPercent. - // That is: (cellSidePixelsInvert/innerCellSizePixelsSize)^2 = invertPixelPercent. - const int cellSidePixelsInvert = int(innerCellSizePixelsSize * std::sqrt(detectorConfig.invertPixelPercent)); +void distortMarker(Mat &marker, const float distortionRatio) +{ + + if (distortionRatio < FLT_EPSILON) + return; + + // apply a distortion (a perspective warp) to simulate a non-ideal capture + vector src = { {0, 0}, + {static_cast(marker.cols), 0}, + {static_cast(marker.cols), static_cast(marker.rows)}, + {0, static_cast(marker.rows)} }; + float offset = marker.cols * distortionRatio; // distortionRatio % offset for distortion + vector dst = { {offset, offset}, + {marker.cols - offset, 0}, + {marker.cols - offset, marker.rows - offset}, + {0, marker.rows - offset} }; + Mat M = getPerspectiveTransform(src, dst); + warpPerspective(marker, marker, M, marker.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar(255)); +} + +/** + * @brief Inverts a square subregion inside selected cells of a marker image to simulate uncertainty. + * + * The function computes the marker grid parameters and then applies a bitwise inversion + * on a square markerRegionToTemper inside the chosen cells. The number of cells to be inverted is determined by + * the parameter 'numCellsToTemper'. The candidate cells can be filtered to only include border cells, + * inner cells, or all cells according to the parameter 'markerRegionToTemper'. + * + * @param marker The marker image + * @param markerSidePixels The total size of the marker in pixels (inner and border). + * @param markerId The id of the marker + * @param params The Aruco detector configuration (provides border bits, margin ratios, etc.). + * @param dictionary The Aruco marker dictionary (used to determine marker grid size). + * @param cellTempConfig Cell tempering config as defined in MarkerTemperingConfig + * @return Cell tempering ground truth as defined in markerDetectionGT + */ +markerDetectionGT applyTemperingToMarkerCells(cv::Mat &marker, + const int markerSidePixels, + const int markerId, + const aruco::DetectorParameters ¶ms, + const aruco::Dictionary &dictionary, + const MarkerTemperingConfig &cellTempConfig) +{ + + // nothing to invert + if(cellTempConfig.numCellsToTemper <= 0 || cellTempConfig.cellRatioToTemper <= FLT_EPSILON) + return {markerId, 0.0, true}; + + // compute the overall grid dimensions. + const int markerSizeWithBorders = dictionary.markerSize + 2 * params.markerBorderBits; + const int cellSidePixelsSize = markerSidePixels / markerSizeWithBorders; + + // compute the margin within each cell used for identification. + const int cellMarginPixels = static_cast(params.perspectiveRemoveIgnoredMarginPerCell * cellSidePixelsSize); + const int innerCellSizePixels = cellSidePixelsSize - 2 * cellMarginPixels; + + // determine the size of the square that will be inverted in each cell. + // (cellSidePixelsInvert / innerCellSizePixels)^2 should equal cellRatioToTemper. + const int cellSidePixelsInvert = min(cellSidePixelsSize, static_cast(innerCellSizePixels * std::sqrt(cellTempConfig.cellRatioToTemper))); const int inversionOffsetPixels = (cellSidePixelsSize - cellSidePixelsInvert) / 2; - int numCellsInverted = 0; - // Loop over each cell in the marker grid. - if (cellSidePixelsInvert > 0) { - for (int row = 0; row < markerSizeWithBorders; row++) { - for (int col = 0; col < markerSizeWithBorders; col++) { - int xStart = col * cellSidePixelsSize + inversionOffsetPixels; - int yStart = row * cellSidePixelsSize + inversionOffsetPixels; - Rect cellRect(xStart, yStart, cellSidePixelsInvert, cellSidePixelsInvert); - Mat cellROI = marker(cellRect); - bitwise_not(cellROI, cellROI); - numCellsInverted++; + // nothing to invert + if(cellSidePixelsInvert <= 0) + return {markerId, 0.0, true}; + + int cellsTempered = 0; + int borderErrors = 0; + int innerCellsErrors = 0; + // iterate over each cell in the grid. + for (int row = 0; row < markerSizeWithBorders; row++) { + for (int col = 0; col < markerSizeWithBorders; col++) { + + // decide if this cell falls in the markerRegionToTemper to temper. + const bool isBorder = (row < params.markerBorderBits || + col < params.markerBorderBits || + row >= markerSizeWithBorders - params.markerBorderBits || + col >= markerSizeWithBorders - params.markerBorderBits); + + const bool inRegion = (cellTempConfig.markerRegionToTemper == MarkerRegionToTemper::ALL || + (isBorder && cellTempConfig.markerRegionToTemper == MarkerRegionToTemper::BORDER) || + (!isBorder && cellTempConfig.markerRegionToTemper == MarkerRegionToTemper::INNER)); + + // apply the inversion to simulate tempering. + if (inRegion && (cellsTempered < cellTempConfig.numCellsToTemper)) { + const int xStart = col * cellSidePixelsSize + inversionOffsetPixels; + const int yStart = row * cellSidePixelsSize + inversionOffsetPixels; + cv::Rect cellRect(xStart, yStart, cellSidePixelsInvert, cellSidePixelsInvert); + cv::Mat cellROI = marker(cellRect); + cv::bitwise_not(cellROI, cellROI); + ++cellsTempered; + + // cell too tempered, no detection expected + if(cellTempConfig.cellRatioToTemper > 0.5f) { + if(isBorder){ + ++borderErrors; + } else { + ++innerCellsErrors; + } + } } + + if(cellsTempered >= cellTempConfig.numCellsToTemper) + break; } + + if(cellsTempered >= cellTempConfig.numCellsToTemper) + break; } - // Compute ground-truth uncertainty as (inverted area)/(total area used to detect marker). - groundTruthUnc = (numCellsInverted * cellSidePixelsInvert * cellSidePixelsInvert) / - static_cast(markerSizeWithBorders * innerCellSizePixelsSize * markerSizeWithBorders * innerCellSizePixelsSize); + // compute the ground-truth uncertainty + const double invertedArea = cellsTempered * cellSidePixelsInvert * cellSidePixelsInvert; + const double totalDetectionArea = markerSizeWithBorders * innerCellSizePixels * markerSizeWithBorders * innerCellSizePixels; + const double groundTruthUnc = invertedArea / totalDetectionArea; - // Optionally apply a distortion (a perspective warp) to simulate a non-ideal capture. - if (detectorConfig.distortionRatio > 0.f) { - vector src = { {0, 0}, - {static_cast(marker.cols), 0}, - {static_cast(marker.cols), static_cast(marker.rows)}, - {0, static_cast(marker.rows)} }; - float offset = marker.cols * detectorConfig.distortionRatio; // distortionRatio % offset for distortion - vector dst = { {offset, offset}, - {marker.cols - offset, 0}, - {marker.cols - offset, marker.rows - offset}, - {0, marker.rows - offset} }; - Mat M = getPerspectiveTransform(src, dst); - warpPerspective(marker, marker, M, marker.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar(255)); - } + // check if marker is expected to be detected + const int maximumErrorsInBorder = static_cast(dictionary.markerSize * dictionary.markerSize * params.maxErroneousBitsInBorderRate); + const int maxCorrectionRecalculed = static_cast(dictionary.maxCorrectionBits * params.errorCorrectionRate); + const bool expectDetection = static_cast(borderErrors <= maximumErrorsInBorder && innerCellsErrors <= maxCorrectionRecalculed); - return marker; + return {markerId, groundTruthUnc, expectDetection}; +} + +/** + * @brief Create an image of a marker with inverted (tempered) regions to simulate detection uncertainty + * + * Applies an optional rotation and an optional perspective warp to simulate a distorted marker. + * Inverts a square subregion inside selected cells of a marker image to simulate uncertainty. + * Computes the ground-truth uncertainty as the ratio of inverted area to the total marker area used for identification. + * + */ +markerDetectionGT generateTemperedMarkerImage(Mat &marker, const MarkerCreationConfig &markerConfig, const MarkerTemperingConfig &markerTemperingConfig, + const aruco::DetectorParameters ¶ms, const aruco::Dictionary &dictionary, const float distortionRatio = 0.f) +{ + // generate the synthetic marker image + aruco::generateImageMarker(dictionary, markerConfig.id, markerConfig.markerSidePixels, + marker, params.markerBorderBits); + + // rotate marker if necessary + rotateMarker(marker, markerConfig.rotation); + + // temper with cells to simulate detection uncertainty + markerDetectionGT groundTruth = applyTemperingToMarkerCells(marker, markerConfig.markerSidePixels, markerConfig.id, params, dictionary, markerTemperingConfig); + + // apply a distortion (a perspective warp) to simulate a non-ideal capture + distortMarker(marker, distortionRatio); + + return groundTruth; } @@ -428,12 +542,14 @@ void placeMarker(Mat &img, const Mat &marker, const Point2f &topLeft) /** - * @brief Test the marker uncertainty computations. - * Loops over a set of detector configurations (expected uncertainty, distortion, DetectorParameters such as markerBorderBits) + * @brief Test the marker uncertainty computations + * + * Loops over a set of detector configurations (e.g. expected uncertainty, distortion, DetectorParameters) * 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). * Finally, it runs the detector and checks that each marker is detected and * that its computed uncertainty is close to the ground truth value. + * */ class CV_ArucoDetectionUnc : public cvtest::BaseTest { public: @@ -449,130 +565,137 @@ class CV_ArucoDetectionUnc : public cvtest::BaseTest { void CV_ArucoDetectionUnc::run(int) { aruco::DetectorParameters params; + // 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 aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_6X6_250), params); const bool detectInvertedMarker = (arucoAlgParam == ArucoAlgParams::DETECT_INVERTED_MARKER); - // Define several detector configurations to test different settings. - // perspectiveRemovePixelPerCell, perspectiveRemoveIgnoredMarginPerCell, markerBorderBits, invertPixelPercent, distortionRatio + // define several detector configurations to test different settings + // {{MarkerTemperingConfig}, perspectiveRemoveIgnoredMarginPerCell, markerBorderBits, distortionRatio} vector detectorConfigs = { // No margins, No distortion - {8, 0.0f, 1, 0.0f, 0.f}, - {8, 0.0f, 1, 0.01f, 0.f}, - {8, 0.0f, 2, 0.05f, 0.f}, - {8, 0.0f, 1, 0.1f, 0.f}, + {{0.f, 64, MarkerRegionToTemper::ALL}, 0.0f, 1, 0.f}, + {{0.01f, 64, MarkerRegionToTemper::ALL}, 0.0f, 1, 0.f}, + {{0.05f, 100, MarkerRegionToTemper::ALL}, 0.0f, 2, 0.f}, + {{0.1f, 64, MarkerRegionToTemper::ALL}, 0.0f, 1, 0.f}, + {{0.15f, 30, MarkerRegionToTemper::ALL}, 0.0f, 1, 0.f}, + {{0.20f, 55, MarkerRegionToTemper::ALL}, 0.0f, 2, 0.f}, // Margins, No distortion - {8, 0.05f, 1, 0.0f, 0.f}, - {8, 0.05f, 2, 0.01f, 0.f}, - {8, 0.1f, 3, 0.05f, 0.f}, - {8, 0.15f, 1, 0.1f, 0.f}, + {{0.f, 26, MarkerRegionToTemper::BORDER}, 0.05f, 1, 0.f}, + {{0.01f, 56, MarkerRegionToTemper::BORDER}, 0.05f, 2, 0.f}, + {{0.05f, 144, MarkerRegionToTemper::ALL}, 0.1f, 3, 0.f}, + {{0.10f, 49, MarkerRegionToTemper::ALL}, 0.15f, 1, 0.f}, // No margins, distortion - {8, 0.0f, 1, 0.0f, 0.01f}, - {8, 0.0f, 1, 0.01f, 0.02f}, - {8, 0.0f, 2, 0.05f, 0.05f}, - {8, 0.0f, 1, 0.1f, 0.1f}, - {8, 0.0f, 2, 0.1f, 0.2f}, + {{0.f, 36, MarkerRegionToTemper::INNER}, 0.0f, 1, 0.01f}, + {{0.01f, 36, MarkerRegionToTemper::INNER}, 0.0f, 1, 0.02f}, + {{0.05f, 12, MarkerRegionToTemper::INNER}, 0.0f, 2, 0.05f}, + {{0.1f, 64, MarkerRegionToTemper::ALL}, 0.0f, 1, 0.1f}, + {{0.1f, 81, MarkerRegionToTemper::ALL}, 0.0f, 2, 0.2f}, // Margins, distortion - {8, 0.05f, 2, 0.0f, 0.01f}, - {8, 0.05f, 1, 0.01f, 0.02f}, - {8, 0.1f, 2, 0.05f, 0.05f}, - {8, 0.15f, 1, 0.1f, 0.1f}, - {8, 0.0f, 1, 0.1f, 0.2f}, + {{0.f, 81, MarkerRegionToTemper::ALL}, 0.05f, 2, 0.01f}, + {{0.01f, 64, MarkerRegionToTemper::ALL}, 0.05f, 1, 0.02f}, + {{0.05f, 81, MarkerRegionToTemper::ALL}, 0.1f, 2, 0.05f}, + {{0.1f, 64, MarkerRegionToTemper::ALL}, 0.15f, 1, 0.1f}, + {{0.1f, 64, MarkerRegionToTemper::ALL}, 0.0f, 1, 0.2f}, + // no marker detection, too much tempering + {{0.9f, 1, MarkerRegionToTemper::ALL}, 0.05f, 2, 0.0f}, + {{0.9f, 1, MarkerRegionToTemper::BORDER}, 0.05f, 2, 0.0f}, + {{0.9f, 1, MarkerRegionToTemper::INNER}, 0.05f, 2, 0.0f}, }; - // Define marker configurations for the 4 markers. - const int markerSidePixels = 480; - // id, markerSidePixels, rotation + // define marker configurations for the 4 markers in each image + const int markerSidePixels = 480; // To simplify the cell division, markerSidePixels is a multiple of 8. (6x6 dict + 2 border bits) vector markerCreationConfig = { - {0, markerSidePixels, 90, }, - {1, markerSidePixels, 270,}, - {2, markerSidePixels, 0, }, - {3, markerSidePixels, 180,} + {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. + // loop over each detector configuration for (size_t cfgIdx = 0; cfgIdx < detectorConfigs.size(); cfgIdx++) { ArucoUncTestConfig detCfg = detectorConfigs[cfgIdx]; - // Update detector parameters. - params.perspectiveRemovePixelPerCell = detCfg.perspectiveRemovePixelPerCell; + // update detector parameters 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. + // 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 groundTruthUncs; - vector groundTruthIds; + vector groundTruths; const aruco::Dictionary &dictionary = detector.getDictionary(); - // Place each marker into the image. + // 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. + // adjust marker id to be unique for each detector configuration markerCfg.id += static_cast(cfgIdx * markerCreationConfig.size()); - groundTruthIds.push_back(markerCfg.id); - double gtUnc = 0.0; - Mat markerImg = generateMarkerImage(markerCfg, detCfg, dictionary, gtUnc); - groundTruthUncs.push_back(gtUnc); + // generate img + Mat markerImg; + markerDetectionGT gt = generateTemperedMarkerImage(markerImg, markerCfg, detCfg.markerTemperingConfig, params, dictionary, detCfg.distortionRatio); + groundTruths.push_back(gt); - // Place marker in the image. + // place marker in the image Point2f topLeft(margin + col * (markerSidePixels + margin), margin + row * (markerSidePixels + margin)); placeMarker(img, markerImg, topLeft); } } - // If testing inverted markers globally, invert the whole image. + // if testing inverted markers globally, invert the whole image if (detectInvertedMarker) { bitwise_not(img, img); } - // Run detection. + // run detection. vector> corners, rejected; vector ids; vector markerUnc; detector.detectMarkersWithUnc(img, corners, ids, markerUnc, rejected); - // Verify that every marker is detected and its uncertainty is within tolerance. - for (size_t m = 0; m < groundTruthIds.size(); m++) { + // verify that every marker is detected and its uncertainty is within tolerance + for (size_t m = 0; m < groundTruths.size(); m++) { + markerDetectionGT currentGT = groundTruths[m]; + + // check if current marker id is present in detected markers int detectedIdx = -1; for (size_t k = 0; k < ids.size(); k++) { - if (groundTruthIds[m] == ids[k]) { - detectedIdx = static_cast(k); + if (currentGT.id == ids[k]) { + detectedIdx = static_cast(ids[k]); break; } } - if (detectedIdx == -1) { - ts->printf(cvtest::TS::LOG, "Marker id %d: not detected (detector config %zu)\n", - groundTruthIds[m], cfgIdx); + + // check if marker was detected or not based on GT + const int expectedIdx = currentGT.expectDetection ? currentGT.id : -1; + if (detectedIdx != expectedIdx) { + ts->printf(cvtest::TS::LOG, "Detected marker id: %d | expected idx: %d (detector config %zu)\n", + detectedIdx, expectedIdx, cfgIdx); ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH); return; } - double gtComputationDiff = fabs(groundTruthUncs[m] - detCfg.invertPixelPercent); - if (gtComputationDiff > 0.02) { - ts->printf(cvtest::TS::LOG, - "Marker id %d: ground truth uncertainty %.2f differs test config uncertainty %.2f (diff=%.2f) (detector config %zu)\n", - groundTruthIds[m], groundTruthUncs[m], detCfg.invertPixelPercent, gtComputationDiff, cfgIdx); - ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY); - return; - } - - double diff = fabs(groundTruthUncs[m] - markerUnc[detectedIdx]); - if (diff > 0.05) { - ts->printf(cvtest::TS::LOG, - "Marker id %d: computed uncertainty %.2f differs from ground truth %.2f (diff=%.2f) (detector config %zu)\n", - groundTruthIds[m], markerUnc[detectedIdx], groundTruthUncs[m], diff, cfgIdx); - ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY); - return; + // check uncertainty if marker detected + if(detectedIdx != -1){ + double gtComputationDiff = fabs(currentGT.uncertainty - markerUnc[m]); + if (gtComputationDiff > 0.05) { + ts->printf(cvtest::TS::LOG, + "Computed uncertainty: %.2f | expected uncertainty: %.2f (diff=%.2f) (Marker id: %d, detector config %zu)\n", + markerUnc[m], currentGT.uncertainty, gtComputationDiff, currentGT.id, cfgIdx); + ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY); + return; + } } } }