From 1aa658fa75d9d8c0d8f85ea03f95e80eb8e7a05f Mon Sep 17 00:00:00 2001 From: Benjamin Knecht Date: Tue, 4 Mar 2025 15:24:03 +0100 Subject: [PATCH] Address more comments Use map to manage unique marker size candidate trees. Avoid code duplication. Add a test to show double detection with overlapping dictionaries. Generalize to marker sizes of not only predefined dictionaries. --- .../objdetect/src/aruco/aruco_detector.cpp | 89 +++++++------------ .../objdetect/test/test_arucodetection.cpp | 80 +++++++++++------ 2 files changed, 84 insertions(+), 85 deletions(-) diff --git a/modules/objdetect/src/aruco/aruco_detector.cpp b/modules/objdetect/src/aruco/aruco_detector.cpp index 4ea80e5faa..a128710f4d 100644 --- a/modules/objdetect/src/aruco/aruco_detector.cpp +++ b/modules/objdetect/src/aruco/aruco_detector.cpp @@ -10,7 +10,7 @@ #include "apriltag/apriltag_quad_thresh.hpp" #include "aruco_utils.hpp" #include -#include +#include namespace cv { namespace aruco { @@ -752,44 +752,20 @@ struct ArucoDetector::ArucoDetectorImpl { /// STEP 3: Corner refinement :: use corner subpix if (detectorParams.cornerRefinementMethod == (int)CORNER_REFINE_SUBPIX) { - CV_Assert(detectorParams.cornerRefinementWinSize > 0 && detectorParams.cornerRefinementMaxIterations > 0 && - detectorParams.cornerRefinementMinAccuracy > 0); - // Do subpixel estimation. In Aruco3 start on the lowest pyramid level and upscale the corners - parallel_for_(Range(0, (int)candidates.size()), [&](const Range& range) { - const int begin = range.start; - const int end = range.end; - - for (int i = begin; i < end; i++) { - if (detectorParams.useAruco3Detection) { - const float scale_init = (float) grey_pyramid[closest_pyr_image_idx].cols / grey.cols; - findCornerInPyrImage(scale_init, closest_pyr_image_idx, grey_pyramid, Mat(candidates[i]), detectorParams); - } else { - int cornerRefinementWinSize = std::max(1, cvRound(detectorParams.relativeCornerRefinmentWinSize* - 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, - detectorParams.cornerRefinementMaxIterations, - detectorParams.cornerRefinementMinAccuracy)); - } - } - }); + performCornerSubpixRefinement(grey, grey_pyramid, closest_pyr_image_idx, candidates, dictionary); } } else if (DictionaryMode::Multi == dictMode) { - unordered_set uniqueMarkerSizes; + map> candidatesPerDictionarySize; for (const Dictionary& dictionary : dictionaries) { - uniqueMarkerSizes.insert(dictionary.markerSize); + candidatesPerDictionarySize.emplace(dictionary.markerSize, vector()); } - // create at max 4 marker candidate trees for each dictionary size - vector> candidatesPerDictionarySize = {{}, {}, {}, {}}; - for (int markerSize : uniqueMarkerSizes) { - // min marker size is 4, so subtract 4 to get index - const auto dictionarySizeIndex = markerSize - 4; + // create candidate trees for each dictionary size + for (auto& candidatesTreeEntry : candidatesPerDictionarySize) { // copy candidates vector> candidatesCopy = candidates; vector > contoursCopy = contours; - candidatesPerDictionarySize[dictionarySizeIndex] = filterTooCloseCandidates(candidatesCopy, contoursCopy, markerSize); + candidatesTreeEntry.second = filterTooCloseCandidates(candidatesCopy, contoursCopy, candidatesTreeEntry.first); } candidates.clear(); contours.clear(); @@ -797,10 +773,9 @@ struct ArucoDetector::ArucoDetectorImpl { /// STEP 2: Check candidate codification (identify markers) int dictIndex = 0; for (const Dictionary& currentDictionary : dictionaries) { - const auto dictionarySizeIndex = currentDictionary.markerSize - 4; // temporary variable to store the current candidates vector> currentCandidates; - identifyCandidates(grey, grey_pyramid, candidatesPerDictionarySize[dictionarySizeIndex], currentCandidates, contours, + identifyCandidates(grey, grey_pyramid, candidatesPerDictionarySize.at(currentDictionary.markerSize), currentCandidates, contours, ids, currentDictionary, rejectedImgPoints); if (_dictIndices.needed()) { dictIndices.insert(dictIndices.end(), currentCandidates.size(), dictIndex); @@ -808,29 +783,7 @@ struct ArucoDetector::ArucoDetectorImpl { /// STEP 3: Corner refinement :: use corner subpix if (detectorParams.cornerRefinementMethod == (int)CORNER_REFINE_SUBPIX) { - CV_Assert(detectorParams.cornerRefinementWinSize > 0 && detectorParams.cornerRefinementMaxIterations > 0 && - detectorParams.cornerRefinementMinAccuracy > 0); - // Do subpixel estimation. In Aruco3 start on the lowest pyramid level and upscale the corners - parallel_for_(Range(0, (int)currentCandidates.size()), [&](const Range& range) { - const int begin = range.start; - const int end = range.end; - - for (int i = begin; i < end; i++) { - if (detectorParams.useAruco3Detection) { - const float scale_init = (float) grey_pyramid[closest_pyr_image_idx].cols / grey.cols; - findCornerInPyrImage(scale_init, closest_pyr_image_idx, grey_pyramid, Mat(currentCandidates[i]), detectorParams); - } - else { - int cornerRefinementWinSize = std::max(1, cvRound(detectorParams.relativeCornerRefinmentWinSize* - getAverageModuleSize(currentCandidates[i], currentDictionary.markerSize, detectorParams.markerBorderBits))); - cornerRefinementWinSize = min(cornerRefinementWinSize, detectorParams.cornerRefinementWinSize); - cornerSubPix(grey, Mat(currentCandidates[i]), Size(cornerRefinementWinSize, cornerRefinementWinSize), Size(-1, -1), - TermCriteria(TermCriteria::MAX_ITER | TermCriteria::EPS, - detectorParams.cornerRefinementMaxIterations, - detectorParams.cornerRefinementMinAccuracy)); - } - } - }); + performCornerSubpixRefinement(grey, grey_pyramid, closest_pyr_image_idx, currentCandidates, currentDictionary); } candidates.insert(candidates.end(), currentCandidates.begin(), currentCandidates.end()); dictIndex++; @@ -1105,6 +1058,30 @@ struct ArucoDetector::ArucoDetectorImpl { } } + void performCornerSubpixRefinement(const Mat& grey, const vector& grey_pyramid, int closest_pyr_image_idx, const vector>& candidates, const Dictionary& dictionary) const { + CV_Assert(detectorParams.cornerRefinementWinSize > 0 && detectorParams.cornerRefinementMaxIterations > 0 && + detectorParams.cornerRefinementMinAccuracy > 0); + // Do subpixel estimation. In Aruco3 start on the lowest pyramid level and upscale the corners + parallel_for_(Range(0, (int)candidates.size()), [&](const Range& range) { + const int begin = range.start; + const int end = range.end; + + for (int i = begin; i < end; i++) { + if (detectorParams.useAruco3Detection) { + const float scale_init = (float) grey_pyramid[closest_pyr_image_idx].cols / grey.cols; + findCornerInPyrImage(scale_init, closest_pyr_image_idx, grey_pyramid, Mat(candidates[i]), detectorParams); + } else { + int cornerRefinementWinSize = std::max(1, cvRound(detectorParams.relativeCornerRefinmentWinSize* + 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, + detectorParams.cornerRefinementMaxIterations, + detectorParams.cornerRefinementMinAccuracy)); + } + } + }); + } }; ArucoDetector::ArucoDetector(const Dictionary &_dictionary, diff --git a/modules/objdetect/test/test_arucodetection.cpp b/modules/objdetect/test/test_arucodetection.cpp index e580575ee5..fd957698b2 100644 --- a/modules/objdetect/test/test_arucodetection.cpp +++ b/modules/objdetect/test/test_arucodetection.cpp @@ -727,25 +727,55 @@ TEST(CV_ArucoMultiDict, multiMarkerDetection) } +TEST(CV_ArucoMultiDict, multiMarkerDoubleDetection) +{ + const int markerSidePixels = 100; + const int imageWidth = 2 * markerSidePixels + 3 * (markerSidePixels / 2); + const int imageHeight = markerSidePixels + 2 * (markerSidePixels / 2); + vector usedDictionaries = { + aruco::getPredefinedDictionary(aruco::DICT_5X5_50), + aruco::getPredefinedDictionary(aruco::DICT_5X5_100) + }; + + // draw synthetic image + Mat img = Mat(imageHeight, imageWidth, CV_8UC1, Scalar::all(255)); + for(int y = 0; y < 2; y++) { + Mat marker; + int id = 49 + y; + auto dict = aruco::getPredefinedDictionary(aruco::DICT_5X5_100); + aruco::generateImageMarker(dict, id, markerSidePixels, marker); + Point2f firstCorner(markerSidePixels / 2.f + y * (1.5f * markerSidePixels), + markerSidePixels / 2.f); + Mat aux = img(Rect((int)firstCorner.x, (int)firstCorner.y, markerSidePixels, markerSidePixels)); + marker.copyTo(aux); + } + img.convertTo(img, CV_8UC3); + + aruco::ArucoDetector detector(usedDictionaries); + + vector > markerCorners; + vector markerIds; + vector > rejectedImgPts; + vector dictIds; + detector.detectMarkersMultiDict(img, markerCorners, markerIds, rejectedImgPts, dictIds); + ASSERT_EQ(markerIds.size(), 3u); + ASSERT_EQ(dictIds.size(), 3u); + EXPECT_EQ(dictIds[0], 0); // 5X5_50 + EXPECT_EQ(dictIds[1], 1); // 5X5_100 + EXPECT_EQ(dictIds[2], 1); // 5X5_100 +} + + TEST(CV_ArucoMultiDict, serialization) { - const std::string fileName("test_aruco_serialization.json"); aruco::ArucoDetector detector; { - FileStorage fs(fileName, FileStorage::Mode::WRITE); - if (!fs.isOpened()) { - cout << "[ SKIPPED ] " << "CV_ArucoMultiDict.serialization" - << " - Skipping due to 'File system write error.'" << endl; - return; - } - detector.write(fs); - fs.release(); - FileStorage test_fs(fileName, FileStorage::Mode::READ); - if (!test_fs.isOpened()) { - cout << "[ SKIPPED ] " << "CV_ArucoMultiDict.serialization" - << " - Skipping due to 'File system read error.'" << endl; - return; - } + FileStorage fs_out(".json", FileStorage::WRITE + FileStorage::MEMORY); + ASSERT_TRUE(fs_out.isOpened()); + detector.write(fs_out); + std::string serialized_string = fs_out.releaseAndGetString(); + FileStorage test_fs(serialized_string, FileStorage::Mode::READ + FileStorage::MEMORY); + ASSERT_TRUE(test_fs.isOpened()); aruco::ArucoDetector test_detector; test_detector.read(test_fs.root()); // compare default constructor result @@ -753,20 +783,12 @@ TEST(CV_ArucoMultiDict, serialization) } detector.setDictionaries({aruco::getPredefinedDictionary(aruco::DICT_4X4_50), aruco::getPredefinedDictionary(aruco::DICT_5X5_100)}); { - FileStorage fs(fileName, FileStorage::Mode::WRITE); - if (!fs.isOpened()) { - cout << "[ SKIPPED ] " << "CV_ArucoMultiDict.serialization" - << " - Skipping due to 'File system write error.'" << endl; - return; - } - detector.write(fs); - fs.release(); - FileStorage test_fs(fileName, FileStorage::Mode::READ); - if (!test_fs.isOpened()) { - cout << "[ SKIPPED ] " << "CV_ArucoMultiDict.serialization" - << " - Skipping due to 'File system read error.'" << endl; - return; - } + FileStorage fs_out(".json", FileStorage::WRITE + FileStorage::MEMORY); + ASSERT_TRUE(fs_out.isOpened()); + detector.write(fs_out); + std::string serialized_string = fs_out.releaseAndGetString(); + FileStorage test_fs(serialized_string, FileStorage::Mode::READ + FileStorage::MEMORY); + ASSERT_TRUE(test_fs.isOpened()); aruco::ArucoDetector test_detector; test_detector.read(test_fs.root()); // check for one additional dictionary