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; }