diff --git a/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp b/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp index c081989645..fbc864076a 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 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 + * 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, OutputArray markersUnc, + OutputArrayOfArrays rejectedImgPoints = 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..6a90876bf9 100644 --- a/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp +++ b/modules/objdetect/include/opencv2/objdetect/aruco_dictionary.hpp @@ -71,7 +71,6 @@ class CV_EXPORTS_W_SIMPLE Dictionary { */ CV_WRAP int getDistanceToId(InputArray bits, int id, bool allRotations = true) const; - /** @brief Generate a canonical marker image */ CV_WRAP void generateImageMarker(int id, int sidePixels, OutputArray _img, int borderBits = 1) const; @@ -84,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 a0f5c64390..97ea8dd351 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 _cellPixelRatio = 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 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) @@ -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 + cellPixelRatio.setTo(1); + } + else { bits.setTo(0); + cellPixelRatio.setTo(0); + } + if(_cellPixelRatio.needed()) cellPixelRatio.copyTo(_cellPixelRatio); return bits; } @@ -369,9 +375,14 @@ 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; + + // 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()); } } + if(_cellPixelRatio.needed()) cellPixelRatio.copyTo(_cellPixelRatio); + return bits; } @@ -403,6 +414,50 @@ 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 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. + float tempBorderUnc = 0.f; + for(int y = 0; y < sizeWithBorders; y++) { + for(int k = 0; k < borderSize; k++) { + // Left and right vertical sides + 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 += cellPixelRatio.ptr(k)[x]; + tempBorderUnc += cellPixelRatio.ptr(sizeWithBorders - 1 - k)[x]; + } + } + + // 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]); + } + } + + // 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, @@ -412,6 +467,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 +479,12 @@ 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); + params.perspectiveRemoveIgnoredMarginPerCell, params.minOtsuStdDev, + cellPixelRatio); // analyze border bits int maximumErrorsInBorder = @@ -439,6 +497,7 @@ 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); @@ -717,6 +782,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 +804,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 +832,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 +915,9 @@ struct ArucoDetector::ArucoDetectorImpl { if (_dictIndices.needed()) { Mat(dictIndices).copyTo(_dictIndices); } + if (_markersUnc.needed()) { + Mat(markersUnc).copyTo(_markersUnc); + } } /** @@ -982,9 +1051,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 +1088,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 +1128,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 +1174,19 @@ ArucoDetector::ArucoDetector(const vector &_dictionaries, arucoDetectorImpl = makePtr(_dictionaries, _detectorParams, _refineParams); } +void ArucoDetector::detectMarkersWithUnc(InputArray _image, OutputArrayOfArrays _corners, OutputArray _ids, OutputArray _markersUnc, + OutputArrayOfArrays _rejectedImgPoints) 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..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 399aa36102..77b831e47e 100644 --- a/modules/objdetect/test/test_arucodetection.cpp +++ b/modules/objdetect/test/test_arucodetection.cpp @@ -321,6 +321,385 @@ void CV_ArucoDetectionPerspective::run(int) { } } +// 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 { + 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; // Marker identification + int markerSidePixels; // Marker size (in pixels) + markerRot rotation; // Rotation of the marker in degrees (0, 90, 180, 270) +}; + +void rotateMarker(Mat &marker, const markerRot rotation) +{ + if(rotation == markerRot::NONE) + return; + + if (rotation == markerRot::ROT_90) { + cv::transpose(marker, marker); + cv::flip(marker, marker, 0); + } else if (rotation == markerRot::ROT_180) { + cv::flip(marker, marker, -1); + } else if (rotation == markerRot::ROT_270) { + cv::transpose(marker, marker); + cv::flip(marker, marker, 1); + } +} + +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; + + // 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 the ground-truth uncertainty + const double invertedArea = cellsTempered * cellSidePixelsInvert * cellSidePixelsInvert; + const double totalDetectionArea = markerSizeWithBorders * innerCellSizePixels * markerSizeWithBorders * innerCellSizePixels; + const double groundTruthUnc = invertedArea / totalDetectionArea; + + // 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 {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; +} + + +/** + * @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 (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: + // The parameter arucoAlgParam allows switching between detecting normal and inverted markers. + CV_ArucoDetectionUnc(ArucoAlgParams algParam) : arucoAlgParam(algParam) {} + + protected: + void run(int); + ArucoAlgParams arucoAlgParam; +}; + + +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 + // {{MarkerTemperingConfig}, perspectiveRemoveIgnoredMarginPerCell, markerBorderBits, distortionRatio} + vector detectorConfigs = { + // No margins, No distortion + {{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 + {{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 + {{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 + {{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 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, 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++) { + ArucoUncTestConfig detCfg = detectorConfigs[cfgIdx]; + + // 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 + 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(margin + col * (markerSidePixels + margin), + 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 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 < 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 (currentGT.id == ids[k]) { + detectedIdx = static_cast(ids[k]); + break; + } + } + + // 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; + } + + // 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; + } + } + } + } +} /** * @brief Check max and min size in marker detection parameters @@ -552,6 +931,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));