diff --git a/doc/pattern_tools/test_charuco_board.py b/doc/pattern_tools/test_charuco_board.py index 83d355b7a6..0258e4034f 100644 --- a/doc/pattern_tools/test_charuco_board.py +++ b/doc/pattern_tools/test_charuco_board.py @@ -34,7 +34,7 @@ class aruco_objdetect_test(NewOpenCVTests): aruco_dict = cv.aruco.getPredefinedDictionary(aruco_type[aruco_type_i]) board = cv.aruco.CharucoBoard((cols, rows), square_size, marker_size, aruco_dict) charuco_detector = cv.aruco.CharucoDetector(board) - from_cv_img = board.generateImage((cols*square_size*10, rows*square_size*10)) + from_cv_img = board.generateImage((cols*square_size, rows*square_size)) #draw desk using svg fd1, filesvg = tempfile.mkstemp(prefix="out", suffix=".svg") @@ -50,15 +50,20 @@ class aruco_objdetect_test(NewOpenCVTests): pm.make_charuco_board() pm.save() drawing = svg2rlg(filesvg) - renderPM.drawToFile(drawing, filepng, fmt='PNG', dpi=720) + renderPM.drawToFile(drawing, filepng, fmt='PNG', dpi=72) from_svg_img = cv.imread(filepng) + _charucoCorners, _charuco_ids_svg, marker_corners_svg, marker_ids_svg = charuco_detector.detectBoard(from_svg_img) + _charucoCorners, _charuco_ids_cv, marker_corners_cv, marker_ids_cv = charuco_detector.detectBoard(from_cv_img) + marker_corners_svg_map, marker_corners_cv_map = {}, {} + for i in range(len(marker_ids_svg)): + marker_corners_svg_map[int(marker_ids_svg[i][0])] = marker_corners_svg[i] + for i in range(len(marker_ids_cv)): + marker_corners_cv_map[int(marker_ids_cv[i][0])] = marker_corners_cv[i] - #test - _charucoCorners, _charucoIds, markerCorners_svg, markerIds_svg = charuco_detector.detectBoard(from_svg_img) - _charucoCorners, _charucoIds, markerCorners_cv, markerIds_cv = charuco_detector.detectBoard(from_cv_img) - - np.testing.assert_allclose(markerCorners_svg, markerCorners_cv, 0.1, 0.1) - np.testing.assert_allclose(markerIds_svg, markerIds_cv, 0.1, 0.1) + for key_svg in marker_corners_svg_map.keys(): + marker_svg = marker_corners_svg_map[key_svg] + marker_cv = marker_corners_cv_map[key_svg] + np.testing.assert_allclose(marker_svg, marker_cv, 0.1, 0.1) finally: if os.path.exists(filesvg): os.remove(filesvg) @@ -87,7 +92,7 @@ class aruco_objdetect_test(NewOpenCVTests): aruco_dict = cv.aruco.getPredefinedDictionary(aruco_type) board = cv.aruco.CharucoBoard((cols, rows), square_size, marker_size, aruco_dict) charuco_detector = cv.aruco.CharucoDetector(board) - from_cv_img = board.generateImage((cols*square_size*10, rows*square_size*10)) + from_cv_img = board.generateImage((cols*square_size, rows*square_size)) #draw desk using svg fd1, filesvg = tempfile.mkstemp(prefix="out", suffix=".svg") @@ -102,17 +107,24 @@ class aruco_objdetect_test(NewOpenCVTests): pm.make_charuco_board() pm.save() drawing = svg2rlg(filesvg) - renderPM.drawToFile(drawing, filepng, fmt='PNG', dpi=720) + renderPM.drawToFile(drawing, filepng, fmt='PNG', dpi=72) from_svg_img = cv.imread(filepng) #test - _charucoCorners, _charucoIds, markerCorners_svg, markerIds_svg = charuco_detector.detectBoard(from_svg_img) - _charucoCorners, _charucoIds, markerCorners_cv, markerIds_cv = charuco_detector.detectBoard(from_cv_img) + _charucoCorners, _charuco_ids_svg, marker_corners_svg, marker_ids_svg = charuco_detector.detectBoard(from_svg_img) + _charucoCorners, _charuco_ids_cv, marker_corners_cv, marker_ids_cv = charuco_detector.detectBoard(from_cv_img) + marker_corners_svg_map, marker_corners_cv_map = {}, {} + for i in range(len(marker_ids_svg)): + marker_corners_svg_map[int(marker_ids_svg[i][0])] = marker_corners_svg[i] + for i in range(len(marker_ids_cv)): + marker_corners_cv_map[int(marker_ids_cv[i][0])] = marker_corners_cv[i] - np.testing.assert_allclose(markerCorners_svg, markerCorners_cv, 0.1, 0.1) - np.testing.assert_allclose(markerIds_svg, markerIds_cv, 0.1, 0.1) + for key_svg in marker_corners_svg_map.keys(): + marker_svg = marker_corners_svg_map[key_svg] + marker_cv = marker_corners_cv_map[key_svg] + np.testing.assert_allclose(marker_svg, marker_cv, 0.1, 0.1) finally: if os.path.exists(filesvg): os.remove(filesvg) if os.path.exists(filepng): - os.remove(filepng) + os.remove(filepng) \ No newline at end of file diff --git a/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp b/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp index c51abb0c93..d537e43dc5 100644 --- a/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp +++ b/modules/objdetect/include/opencv2/objdetect/aruco_detector.hpp @@ -33,7 +33,7 @@ struct CV_EXPORTS_W_SIMPLE DetectorParameters { polygonalApproxAccuracyRate = 0.03; minCornerDistanceRate = 0.05; minDistanceToBorder = 3; - minMarkerDistanceRate = 0.05; + minMarkerDistanceRate = 0.125; cornerRefinementMethod = (int)CORNER_REFINE_NONE; cornerRefinementWinSize = 5; relativeCornerRefinmentWinSize = 0.3f; @@ -100,12 +100,26 @@ struct CV_EXPORTS_W_SIMPLE DetectorParameters { /// minimum distance of any corner to the image border for detected markers (in pixels) (default 3) CV_PROP_RW int minDistanceToBorder; - /** @brief minimum mean distance beetween two marker corners to be considered imilar, so that the smaller one is removed. + /** @brief minimum average distance between the corners of the two markers to be grouped (default 0.125). * - * The rate is relative to the smaller perimeter of the two markers (default 0.05). + * The rate is relative to the smaller perimeter of the two markers. + * Two markers are grouped if average distance between the corners of the two markers is less than + * min(MarkerPerimeter1, MarkerPerimeter2)*minMarkerDistanceRate. + * + * default value is 0.125 because 0.125*MarkerPerimeter = (MarkerPerimeter / 4) * 0.5 = half the side of the marker. + * + * @note default value was changed from 0.05 after 4.8.1 release, because the filtering algorithm has been changed. + * Now a few candidates from the same group can be added to the list of candidates if they are far from each other. + * @sa minGroupDistance. */ CV_PROP_RW double minMarkerDistanceRate; + /** @brief minimum average distance between the corners of the two markers in group to add them to the list of candidates + * + * The average distance between the corners of the two markers is calculated relative to its module size (default 0.21). + */ + CV_PROP_RW float minGroupDistance = 0.21f; + /** @brief default value CORNER_REFINE_NONE */ CV_PROP_RW int cornerRefinementMethod; diff --git a/modules/objdetect/src/aruco/aruco_detector.cpp b/modules/objdetect/src/aruco/aruco_detector.cpp index f5aba9b1be..97a40746a8 100644 --- a/modules/objdetect/src/aruco/aruco_detector.cpp +++ b/modules/objdetect/src/aruco/aruco_detector.cpp @@ -50,6 +50,7 @@ static inline bool readWrite(DetectorParameters ¶ms, const FileNode* readNod readNode, writeStorage); check |= readWriteParameter("minOtsuStdDev", params.minOtsuStdDev, readNode, writeStorage); check |= readWriteParameter("errorCorrectionRate", params.errorCorrectionRate, readNode, writeStorage); + check |= readWriteParameter("minGroupDistance", params.minGroupDistance, readNode, writeStorage); // new aruco 3 functionality check |= readWriteParameter("useAruco3Detection", params.useAruco3Detection, readNode, writeStorage); check |= readWriteParameter("minSideLengthCanonicalImg", params.minSideLengthCanonicalImg, readNode, writeStorage); @@ -212,149 +213,65 @@ static void _reorderCandidatesCorners(vector > &candidates) { } } -/** - * @brief to make sure that the corner's order of both candidates (default/white) is the same - */ -static vector alignContourOrder(Point2f corner, vector candidate) { - uint8_t r=0; - double min = norm( Vec2f( corner - candidate[0] ), NORM_L2SQR); - for(uint8_t pos=1; pos < 4; pos++) { - double nDiff = norm( Vec2f( corner - candidate[pos] ), NORM_L2SQR); - if(nDiff < min){ - r = pos; - min =nDiff; - } +static float getAverageModuleSize(const vector& markerCorners, int markerSize, int markerBorderBits) { + float averageArucoModuleSize = 0.f; + for (size_t i = 0ull; i < 4ull; i++) { + averageArucoModuleSize += sqrt(normL2Sqr(Point2f(markerCorners[i] - markerCorners[(i+1ull) % 4ull]))); } - std::rotate(candidate.begin(), candidate.begin() + r, candidate.end()); - return candidate; + int numModules = markerSize + markerBorderBits * 2; + averageArucoModuleSize /= ((float)markerCorners.size()*numModules); + return averageArucoModuleSize; } -/** - * @brief Check candidates that are too close to each other, save the potential candidates - * (i.e. biggest/smallest contour) and remove the rest - */ -static void _filterTooCloseCandidates(const vector > &candidatesIn, - vector > > &candidatesSetOut, - const vector > &contoursIn, - vector > > &contoursSetOut, - double minMarkerDistanceRate, bool detectInvertedMarker) { +static bool checkMarker1InMarker2(const vector& marker1, const vector& marker2) { + return pointPolygonTest(marker2, marker1[0], false) >= 0 && pointPolygonTest(marker2, marker1[1], false) >= 0 && + pointPolygonTest(marker2, marker1[2], false) >= 0 && pointPolygonTest(marker2, marker1[3], false) >= 0; +} - CV_Assert(minMarkerDistanceRate >= 0); - vector candGroup; - candGroup.resize(candidatesIn.size(), -1); - vector > groupedCandidates; - for(unsigned int i = 0; i < candidatesIn.size(); i++) { - bool isSingleContour = true; - for(unsigned int j = i + 1; j < candidatesIn.size(); j++) { +struct MarkerCandidate { + vector corners; + vector contour; + float perimeter = 0.f; +}; - int minimumPerimeter = min((int)contoursIn[i].size(), (int)contoursIn[j].size() ); +struct MarkerCandidateTree : MarkerCandidate{ + int parent = -1; + int depth = 0; + vector closeContours; - // fc is the first corner considered on one of the markers, 4 combinations are possible - for(int fc = 0; fc < 4; fc++) { - double distSq = 0; - for(int c = 0; c < 4; c++) { - // modC is the corner considering first corner is fc - int modC = (c + fc) % 4; - distSq += (candidatesIn[i][modC].x - candidatesIn[j][c].x) * - (candidatesIn[i][modC].x - candidatesIn[j][c].x) + - (candidatesIn[i][modC].y - candidatesIn[j][c].y) * - (candidatesIn[i][modC].y - candidatesIn[j][c].y); - } - distSq /= 4.; + MarkerCandidateTree() {} - // if mean square distance is too low, remove the smaller one of the two markers - double minMarkerDistancePixels = double(minimumPerimeter) * minMarkerDistanceRate; - if(distSq < minMarkerDistancePixels * minMarkerDistancePixels) { - isSingleContour = false; - // i and j are not related to a group - if(candGroup[i]<0 && candGroup[j]<0){ - // mark candidates with their corresponding group number - candGroup[i] = candGroup[j] = (int)groupedCandidates.size(); - - // create group - vector grouped; - grouped.push_back(i); - grouped.push_back(j); - groupedCandidates.push_back( grouped ); - } - // i is related to a group - else if(candGroup[i] > -1 && candGroup[j] == -1){ - int group = candGroup[i]; - candGroup[j] = group; - - // add to group - groupedCandidates[group].push_back( j ); - } - // j is related to a group - else if(candGroup[j] > -1 && candGroup[i] == -1){ - int group = candGroup[j]; - candGroup[i] = group; - - // add to group - groupedCandidates[group].push_back( i ); - } - } - } - } - if (isSingleContour && candGroup[i] < 0) - { - candGroup[i] = (int)groupedCandidates.size(); - vector grouped; - grouped.push_back(i); - grouped.push_back(i); // step "save possible candidates" require minimum 2 elements - groupedCandidates.push_back(grouped); + MarkerCandidateTree(vector&& corners_, vector&& contour_) { + corners = std::move(corners_); + contour = std::move(contour_); + perimeter = 0.f; + for (size_t i = 0ull; i < 4ull; i++) { + perimeter += sqrt(normL2Sqr(Point2f(corners[i] - corners[(i+1ull) % 4ull]))); } } - // save possible candidates - candidatesSetOut.clear(); - contoursSetOut.clear(); - - vector > biggerCandidates; - vector > biggerContours; - vector > smallerCandidates; - vector > smallerContours; - - // save possible candidates - for(unsigned int i = 0; i < groupedCandidates.size(); i++) { - unsigned int smallerIdx = groupedCandidates[i][0]; - unsigned int biggerIdx = smallerIdx; - double smallerArea = contourArea(candidatesIn[smallerIdx]); - double biggerArea = smallerArea; - - // evaluate group elements - for(unsigned int j = 1; j < groupedCandidates[i].size(); j++) { - unsigned int currIdx = groupedCandidates[i][j]; - double currArea = contourArea(candidatesIn[currIdx]); - - // check if current contour is bigger - if(currArea >= biggerArea) { - biggerIdx = currIdx; - biggerArea = currArea; - } - - // check if current contour is smaller - if(currArea < smallerArea && detectInvertedMarker) { - smallerIdx = currIdx; - smallerArea = currArea; - } - } - - // add contours and candidates - biggerCandidates.push_back(candidatesIn[biggerIdx]); - biggerContours.push_back(contoursIn[biggerIdx]); - if(detectInvertedMarker) { - smallerCandidates.push_back(alignContourOrder(candidatesIn[biggerIdx][0], candidatesIn[smallerIdx])); - smallerContours.push_back(contoursIn[smallerIdx]); - } + bool operator<(const MarkerCandidateTree& m) const { + // sorting the contors in descending order + return perimeter > m.perimeter; } - // to preserve the structure :: candidateSet< defaultCandidates, whiteCandidates > - // default candidates - candidatesSetOut.push_back(biggerCandidates); - contoursSetOut.push_back(biggerContours); - // white candidates - candidatesSetOut.push_back(smallerCandidates); - contoursSetOut.push_back(smallerContours); +}; + + +// returns the average distance between the marker points +float static inline getAverageDistance(const std::vector& marker1, const std::vector& marker2) { + float minDistSq = std::numeric_limits::max(); + // fc is the first corner considered on one of the markers, 4 combinations are possible + for(int fc = 0; fc < 4; fc++) { + float distSq = 0; + for(int c = 0; c < 4; c++) { + // modC is the corner considering first corner is fc + int modC = (c + fc) % 4; + distSq += normL2Sqr(marker1[modC] - marker2[c]); + } + distSq /= 4.f; + minDistSq = min(minDistSq, distSq); + } + return sqrt(minDistSq); } /** @@ -403,29 +320,6 @@ static void _detectInitialCandidates(const Mat &grey, vector > & } -/** - * @brief Detect square candidates in the input image - */ -static void _detectCandidates(InputArray _grayImage, vector > >& candidatesSetOut, - vector > >& contoursSetOut, const DetectorParameters &_params) { - Mat grey = _grayImage.getMat(); - CV_DbgAssert(grey.total() != 0); - CV_DbgAssert(grey.type() == CV_8UC1); - - /// 1. DETECT FIRST SET OF CANDIDATES - vector > candidates; - vector > contours; - _detectInitialCandidates(grey, candidates, contours, _params); - /// 2. SORT CORNERS - _reorderCandidatesCorners(candidates); - - /// 3. FILTER OUT NEAR CANDIDATE PAIRS - // save the outter/inner border (i.e. potential candidates) - _filterTooCloseCandidates(candidates, candidatesSetOut, contours, contoursSetOut, - _params.minMarkerDistanceRate, _params.detectInvertedMarker); -} - - /** * @brief Given an input image and candidate corners, extract the bits of the candidate, including * the border bits @@ -527,12 +421,10 @@ static int _getBorderErrors(const Mat &bits, int markerSize, int borderSize) { * 1 if the candidate is a black candidate (default candidate) * 2 if the candidate is a white candidate */ -static uint8_t _identifyOneCandidate(const Dictionary& dictionary, InputArray _image, +static uint8_t _identifyOneCandidate(const Dictionary& dictionary, const Mat& _image, const vector& _corners, int& idx, const DetectorParameters& params, int& rotation, const float scale = 1.f) { - CV_DbgAssert(_corners.size() == 4); - CV_DbgAssert(_image.getMat().total() != 0); CV_DbgAssert(params.markerBorderBits > 0); uint8_t typ=1; // get bits @@ -610,87 +502,6 @@ static size_t _findOptPyrImageForCanonicalImg( return optLevel; } -/** - * @brief Identify square candidates according to a marker dictionary - */ - -static void _identifyCandidates(InputArray grey, - const vector& image_pyr, - vector > >& _candidatesSet, - vector > >& _contoursSet, const Dictionary &_dictionary, - vector >& _accepted, vector >& _contours, vector& ids, - const DetectorParameters ¶ms, - OutputArrayOfArrays _rejected = noArray()) { - CV_DbgAssert(grey.getMat().total() != 0); - CV_DbgAssert(grey.getMat().type() == CV_8UC1); - int ncandidates = (int)_candidatesSet[0].size(); - vector > accepted; - vector > rejected; - vector > contours; - - vector idsTmp(ncandidates, -1); - vector rotated(ncandidates, 0); - vector validCandidates(ncandidates, 0); - - //// Analyze each of the candidates - parallel_for_(Range(0, ncandidates), [&](const Range &range) { - const int begin = range.start; - const int end = range.end; - - vector >& candidates = params.detectInvertedMarker ? _candidatesSet[1] : _candidatesSet[0]; - vector >& contourS = params.detectInvertedMarker ? _contoursSet[1] : _contoursSet[0]; - - for(int i = begin; i < end; i++) { - int currId = -1; - // implements equation (4) - if (params.useAruco3Detection) { - const int perimeterOfContour = static_cast(contourS[i].size()); - const int min_perimeter = params.minSideLengthCanonicalImg * 4; - const size_t nearestImgId = _findOptPyrImageForCanonicalImg(image_pyr, grey.cols(), perimeterOfContour, min_perimeter); - const float scale = image_pyr[nearestImgId].cols / static_cast(grey.cols()); - - validCandidates[i] = _identifyOneCandidate(_dictionary, image_pyr[nearestImgId], candidates[i], currId, params, rotated[i], scale); - } - else { - validCandidates[i] = _identifyOneCandidate(_dictionary, grey, candidates[i], currId, params, rotated[i]); - } - - if(validCandidates[i] > 0) - idsTmp[i] = currId; - } - }); - - for(int i = 0; i < ncandidates; i++) { - if(validCandidates[i] > 0) { - // to choose the right set of candidates :: 0 for default, 1 for white markers - uint8_t set = validCandidates[i]-1; - - // shift corner positions to the correct rotation - correctCornerPosition(_candidatesSet[set][i], rotated[i]); - - if( !params.detectInvertedMarker && validCandidates[i] == 2 ) - continue; - - // add valid candidate - accepted.push_back(_candidatesSet[set][i]); - ids.push_back(idsTmp[i]); - - contours.push_back(_contoursSet[set][i]); - - } else { - rejected.push_back(_candidatesSet[0][i]); - } - } - - // parse output - _accepted = accepted; - - _contours= contours; - - if(_rejected.needed()) { - _copyVector2Output(rejected, _rejected); - } -} /** * Line fitting A * B = C :: Called from function refineCandidateLines @@ -846,16 +657,210 @@ struct ArucoDetector::ArucoDetectorImpl { ArucoDetectorImpl(const Dictionary &_dictionary, const DetectorParameters &_detectorParams, const RefineParameters& _refineParams): dictionary(_dictionary), detectorParams(_detectorParams), refineParams(_refineParams) {} + /** + * @brief Detect square candidates in the input image + */ + void detectCandidates(const Mat& grey, vector >& candidates, vector >& contours) { + /// 1. DETECT FIRST SET OF CANDIDATES + _detectInitialCandidates(grey, candidates, contours, detectorParams); + /// 2. SORT CORNERS + _reorderCandidatesCorners(candidates); + } - float getAverageArucoPinSize(vector markerCorners) { - float averageArucoModuleSize = 0.f; - int numPins = dictionary.markerSize + detectorParams.markerBorderBits * 2; - for (size_t i = 0ull; i < markerCorners.size(); i++) { - averageArucoModuleSize += sqrt(normL2Sqr(Point2f(markerCorners[i] - markerCorners[(i+1ull)%markerCorners.size()]))); + /** + * @brief FILTER OUT NEAR CANDIDATE PAIRS + * + * save the outter/inner border (i.e. potential candidates) to vector, + * clear candidates and contours + */ + vector + filterTooCloseCandidates(vector > &candidates, vector > &contours) { + CV_Assert(detectorParams.minMarkerDistanceRate >= 0.); + vector candidateTree(candidates.size()); + for(size_t i = 0ull; i < candidates.size(); i++) { + candidateTree[i] = MarkerCandidateTree(std::move(candidates[i]), std::move(contours[i])); } - averageArucoModuleSize /= ((float)markerCorners.size()*numPins); - return averageArucoModuleSize; -} + candidates.clear(); + contours.clear(); + + // sort candidates from big to small + std::sort(candidateTree.begin(), candidateTree.end()); + // group index for each candidate + vector groupId(candidateTree.size(), -1); + vector > groupedCandidates; + vector isSelectedContours(candidateTree.size(), true); + + size_t countSelectedContours = 0ull; + for (size_t i = 0ull; i < candidateTree.size(); i++) { + for (size_t j = i + 1ull; j < candidateTree.size(); j++) { + float minDist = getAverageDistance(candidateTree[i].corners, candidateTree[j].corners); + // if mean distance is too low, group markers + // the distance between the points of two independent markers should be more than half the side of the marker + // half the side of the marker = (perimeter / 4) * 0.5 = perimeter * 0.125 + if(minDist < candidateTree[j].perimeter*(float)detectorParams.minMarkerDistanceRate) { + isSelectedContours[i] = false; + isSelectedContours[j] = false; + // i and j are not related to a group + if(groupId[i] < 0 && groupId[j] < 0){ + // mark candidates with their corresponding group number + groupId[i] = groupId[j] = (int)groupedCandidates.size(); + // create group + groupedCandidates.push_back({i, j}); + } + // i is related to a group + else if(groupId[i] > -1 && groupId[j] == -1) { + int group = groupId[i]; + groupId[j] = group; + // add to group + groupedCandidates[group].push_back(j); + } + // j is related to a group + else if(groupId[j] > -1 && groupId[i] == -1) { + int group = groupId[j]; + groupId[i] = group; + // add to group + groupedCandidates[group].push_back(i); + } + } + } + countSelectedContours += isSelectedContours[i]; + } + + for (vector& grouped : groupedCandidates) { + if (detectorParams.detectInvertedMarker) // if detectInvertedMarker choose smallest contours + std::sort(grouped.begin(), grouped.end(), [](const size_t &a, const size_t &b) { + return a > b; + }); + else // if detectInvertedMarker==false choose largest contours + std::sort(grouped.begin(), grouped.end()); + size_t currId = grouped[0]; + isSelectedContours[currId] = true; + for (size_t i = 1ull; i < grouped.size(); i++) { + size_t id = grouped[i]; + float dist = getAverageDistance(candidateTree[id].corners, candidateTree[currId].corners); + float moduleSize = getAverageModuleSize(candidateTree[id].corners, dictionary.markerSize, detectorParams.markerBorderBits); + if (dist > detectorParams.minGroupDistance*moduleSize) { + currId = id; + candidateTree[grouped[0]].closeContours.push_back(candidateTree[id]); + } + } + } + + vector selectedCandidates(countSelectedContours + groupedCandidates.size()); + countSelectedContours = 0ull; + for (size_t i = 0ull; i < candidateTree.size(); i++) { + if (isSelectedContours[i]) { + selectedCandidates[countSelectedContours] = std::move(candidateTree[i]); + countSelectedContours++; + } + } + + // find hierarchy in the candidate tree + for (int i = (int)selectedCandidates.size()-1; i >= 0; i--) { + for (int j = i - 1; j >= 0; j--) { + if (checkMarker1InMarker2(selectedCandidates[i].corners, selectedCandidates[j].corners)) { + selectedCandidates[i].parent = j; + selectedCandidates[j].depth = max(selectedCandidates[j].depth, selectedCandidates[i].depth + 1); + break; + } + } + } + return selectedCandidates; + } + + /** + * @brief Identify square candidates according to a marker dictionary + */ + void identifyCandidates(const Mat& grey, const vector& image_pyr, vector& selectedContours, + vector >& accepted, vector >& contours, + vector& ids, OutputArrayOfArrays _rejected = noArray()) { + size_t ncandidates = selectedContours.size(); + vector > rejected; + + vector idsTmp(ncandidates, -1); + vector rotated(ncandidates, 0); + vector validCandidates(ncandidates, 0); + vector was(ncandidates, false); + bool checkCloseContours = true; + + int maxDepth = 0; + for (size_t i = 0ull; i < selectedContours.size(); i++) + maxDepth = max(selectedContours[i].depth, maxDepth); + vector> depths(maxDepth+1); + for (size_t i = 0ull; i < selectedContours.size(); i++) { + depths[selectedContours[i].depth].push_back(i); + } + + //// Analyze each of the candidates + int depth = 0; + size_t counter = 0; + while (counter < ncandidates) { + parallel_for_(Range(0, (int)depths[depth].size()), [&](const Range& range) { + const int begin = range.start; + const int end = range.end; + for (int i = begin; i < end; i++) { + size_t v = depths[depth][i]; + was[v] = true; + Mat img = grey; + // implements equation (4) + if (detectorParams.useAruco3Detection) { + const int minPerimeter = detectorParams.minSideLengthCanonicalImg * 4; + const size_t nearestImgId = _findOptPyrImageForCanonicalImg(image_pyr, grey.cols, static_cast(selectedContours[v].contour.size()), minPerimeter); + img = image_pyr[nearestImgId]; + } + const float scale = detectorParams.useAruco3Detection ? img.cols / static_cast(grey.cols) : 1.f; + + validCandidates[v] = _identifyOneCandidate(dictionary, img, selectedContours[v].corners, idsTmp[v], detectorParams, rotated[v], scale); + + if (validCandidates[v] == 0 && checkCloseContours) { + for (const MarkerCandidate& closeMarkerCandidate: selectedContours[v].closeContours) { + validCandidates[v] = _identifyOneCandidate(dictionary, img, closeMarkerCandidate.corners, idsTmp[v], detectorParams, rotated[v], scale); + if (validCandidates[v] > 0) { + selectedContours[v].corners = closeMarkerCandidate.corners; + selectedContours[v].contour = closeMarkerCandidate.contour; + break; + } + } + } + } + }); + + // visit the parent vertices of the detected markers to skip identify parent contours + for(size_t v : depths[depth]) { + if(validCandidates[v] > 0) { + int parent = selectedContours[v].parent; + while (parent != -1) { + if (!was[parent]) { + was[parent] = true; + counter++; + } + parent = selectedContours[parent].parent; + } + } + counter++; + } + depth++; + } + + for (size_t i = 0ull; i < selectedContours.size(); i++) { + if (validCandidates[i] > 0) { + // shift corner positions to the correct rotation + correctCornerPosition(selectedContours[i].corners, rotated[i]); + + accepted.push_back(selectedContours[i].corners); + contours.push_back(selectedContours[i].contour); + ids.push_back(idsTmp[i]); + } + else { + rejected.push_back(selectedContours[i].corners); + } + } + + // parse output + if(_rejected.needed()) { + _copyVector2Output(rejected, _rejected); + } + } }; @@ -929,23 +934,21 @@ void ArucoDetector::detectMarkers(InputArray _image, OutputArrayOfArrays _corner vector > contours; vector ids; - vector > > candidatesSet; - vector > > contoursSet; - /// STEP 2.a Detect marker candidates :: using AprilTag if(detectorParams.cornerRefinementMethod == (int)CORNER_REFINE_APRILTAG){ _apriltag(grey, detectorParams, candidates, contours); - - candidatesSet.push_back(candidates); - contoursSet.push_back(contours); } /// STEP 2.b Detect marker candidates :: traditional way - else - _detectCandidates(grey, candidatesSet, contoursSet, detectorParams); + else { + arucoDetectorImpl->detectCandidates(grey, candidates, contours); + } + + /// STEP 2.c FILTER OUT NEAR CANDIDATE PAIRS + auto selectedCandidates = arucoDetectorImpl->filterTooCloseCandidates(candidates, contours); /// STEP 2: Check candidate codification (identify markers) - _identifyCandidates(grey, grey_pyramid, candidatesSet, contoursSet, dictionary, - candidates, contours, ids, detectorParams, _rejectedImgPoints); + arucoDetectorImpl->identifyCandidates(grey, grey_pyramid, selectedCandidates, candidates, contours, + ids, _rejectedImgPoints); /// STEP 3: Corner refinement :: use corner subpix if (detectorParams.cornerRefinementMethod == (int)CORNER_REFINE_SUBPIX) { @@ -963,7 +966,7 @@ void ArucoDetector::detectMarkers(InputArray _image, OutputArrayOfArrays _corner } else { int cornerRefinementWinSize = std::max(1, cvRound(detectorParams.relativeCornerRefinmentWinSize* - arucoDetectorImpl->getAverageArucoPinSize(candidates[i]))); + getAverageModuleSize(candidates[i], dictionary.markerSize, detectorParams.markerBorderBits))); cornerRefinementWinSize = min(cornerRefinementWinSize, detectorParams.cornerRefinementWinSize); cornerSubPix(grey, Mat(candidates[i]), Size(cornerRefinementWinSize, cornerRefinementWinSize), Size(-1, -1), TermCriteria(TermCriteria::MAX_ITER | TermCriteria::EPS, @@ -1238,7 +1241,7 @@ void ArucoDetector::refineDetectedMarkers(InputArray _image, const Board& _board std::vector marker(closestRotatedMarker.begin(), closestRotatedMarker.end()); int cornerRefinementWinSize = std::max(1, cvRound(detectorParams.relativeCornerRefinmentWinSize* - arucoDetectorImpl->getAverageArucoPinSize(marker))); + getAverageModuleSize(marker, dictionary.markerSize, detectorParams.markerBorderBits))); cornerRefinementWinSize = min(cornerRefinementWinSize, detectorParams.cornerRefinementWinSize); cornerSubPix(grey, closestRotatedMarker, Size(cornerRefinementWinSize, cornerRefinementWinSize), diff --git a/modules/objdetect/src/aruco/charuco_detector.cpp b/modules/objdetect/src/aruco/charuco_detector.cpp index 0ea7552a22..4f08747289 100644 --- a/modules/objdetect/src/aruco/charuco_detector.cpp +++ b/modules/objdetect/src/aruco/charuco_detector.cpp @@ -314,7 +314,9 @@ struct CharucoDetector::CharucoDetectorImpl { vector > rejectedMarkers; arucoDetector.detectMarkers(image, _markerCorners, _markerIds, rejectedMarkers); if (charucoParameters.tryRefineMarkers) - arucoDetector.refineDetectedMarkers(image, board, _markerCorners, _markerIds, rejectedMarkers); + arucoDetector.refineDetectedMarkers(image, board, _markerCorners, _markerIds, rejectedMarkers); + if (_markerCorners.empty() && _markerIds.empty()) + return; } // if camera parameters are avaible, use approximated calibration if(!charucoParameters.cameraMatrix.empty()) diff --git a/modules/objdetect/test/test_arucodetection.cpp b/modules/objdetect/test/test_arucodetection.cpp index 9cb6f07b33..7145b5d663 100644 --- a/modules/objdetect/test/test_arucodetection.cpp +++ b/modules/objdetect/test/test_arucodetection.cpp @@ -613,6 +613,32 @@ TEST(CV_ArucoDetectMarkers, regression_2492) } } + +TEST(CV_ArucoDetectMarkers, regression_contour_24220) +{ + aruco::ArucoDetector detector; + vector markerIds; + vector > markerCorners; + string imgPath = cvtest::findDataFile("aruco/failmask9.png"); + Mat image = imread(imgPath); + + const size_t N = 1ull; + const int goldCorners[8] = {392,175, 99,257, 117,109, 365,44}; + const int goldCornersId = 0; + + detector.detectMarkers(image, markerCorners, markerIds); + + ASSERT_EQ(N, markerIds.size()); + ASSERT_EQ(4ull, markerCorners[0].size()); + ASSERT_EQ(goldCornersId, markerIds[0]); + for (int j = 0; j < 4; j++) + { + EXPECT_NEAR(static_cast(goldCorners[j * 2]), markerCorners[0][j].x, 1.f); + EXPECT_NEAR(static_cast(goldCorners[j * 2 + 1]), markerCorners[0][j].y, 1.f); + } +} + + struct ArucoThreading: public testing::TestWithParam { struct NumThreadsSetter { diff --git a/modules/objdetect/test/test_charucodetection.cpp b/modules/objdetect/test/test_charucodetection.cpp index b60ad0ab51..87520c873e 100644 --- a/modules/objdetect/test/test_charucodetection.cpp +++ b/modules/objdetect/test/test_charucodetection.cpp @@ -650,7 +650,7 @@ TEST(Charuco, issue_14014) EXPECT_EQ(Size(4, 1), corners[0].size()); // check dimension of detected corners size_t numRejPoints = rejectedPoints.size(); - ASSERT_EQ(rejectedPoints.size(), 26ull); // optional check to track regressions + ASSERT_EQ(rejectedPoints.size(), 24ull); // optional check to track regressions EXPECT_EQ(Size(4, 1), rejectedPoints[0].size()); // check dimension of detected corners detector.refineDetectedMarkers(img, board, corners, ids, rejectedPoints);