Skip to content

Extend ArUcoDetector to run multiple dictionaries in an efficient manner.#26934

Merged
asmorkalov merged 14 commits intoopencv:4.xfrom
BenjaminKnecht:new_4.x
Mar 11, 2025
Merged

Extend ArUcoDetector to run multiple dictionaries in an efficient manner.#26934
asmorkalov merged 14 commits intoopencv:4.xfrom
BenjaminKnecht:new_4.x

Conversation

@BenjaminKnecht
Copy link
Copy Markdown

@BenjaminKnecht BenjaminKnecht commented Feb 18, 2025

  • Add constructor for multiple dictionaries
  • Add get/set/remove/add functions for multiple dictionaries
  • Add unit tests

TESTED=unit tests

Pull Request Readiness Checklist

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

  • I agree to contribute to the project under Apache 2 License.
  • To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
  • The PR is proposed to the proper branch
  • There is a reference to the original bug report and related work
  • There is accuracy test, performance test and test data in opencv_extra repository, if applicable
    Patch to opencv_extra has the same branch name.
  • The feature is well documented and sample code can be built with the project CMake

manner.

* Add constructor for multiple dictionaries
* Add get/set/remove/add functions for multiple dictionaries
* Add unit tests

TESTED=unit tests
Copy link
Copy Markdown
Contributor

@asmorkalov asmorkalov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed API and tests. Works on details. Please add also Python a test to https://github.com/opencv/opencv/blob/4.x/modules/objdetect/misc/python/test/test_objdetect_aruco.py

@vrabaud
Copy link
Copy Markdown
Contributor

vrabaud commented Feb 18, 2025

For Python compatibility, please add typedef std::vector<aruco::Dictionary> vector_Dictionary; after

typedef std::vector<Scalar> vector_Scalar;

InputOutputArrayOfArrays _rejectedCorners, InputArray _cameraMatrix,
InputArray _distCoeffs, OutputArray _recoveredIdxs) const {
DetectorParameters& detectorParams = arucoDetectorImpl->detectorParams;
const Dictionary& dictionary = arucoDetectorImpl->dictionary;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to add refineDetectedMarkersMultiDict overload with dictionaries id like in detect. May be useful for multi-scale markers.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made sure refine is now looping over the markers and checked the output. Looks like this can be done safely without having to have two functions.

using multiple dictionaries for refinement (function split not necessary
as it's backwards compatible)
@asmorkalov asmorkalov requested review from opencv-alalek and removed request for opencv-alalek February 20, 2025 05:50
@asmorkalov
Copy link
Copy Markdown
Contributor

Warning in all Windows builds:

C:\GHA-OCV-3\_work\opencv\opencv\opencv\modules\objdetect\src\aruco\aruco_detector.cpp(840): warning C4305: 'initializing': truncation from 'double' to 'float'

@asmorkalov
Copy link
Copy Markdown
Contributor

@dkurt @AleksandrPanov @mshabunin What do you think?

@BenjaminKnecht
Copy link
Copy Markdown
Author

Warning in all Windows builds:

C:\GHA-OCV-3\_work\opencv\opencv\opencv\modules\objdetect\src\aruco\aruco_detector.cpp(840): warning C4305: 'initializing': truncation from 'double' to 'float'

Fixing it now.

I also will add the MultiDict version for the refineDetectedMarkers function. It might make sense after all if there are multiple boards in the image.

@BenjaminKnecht
Copy link
Copy Markdown
Author

So this hopefully fixes the warning on Windows (sorry, no access to Windows to test this on).

Concerning the refineDetectedMarkers function I'm not sure a multi-dict version makes sense as it improves the markers on a board. These boards usually (and also in the code) only have a single dictionary. So instead I think it's better to use just one dictionary (the one the board is using) to refine these markers. I added this insight as a @note to function so people don't stumble over this.

Let me know what you think. It's a bit unfortunate the extension wasn't possible without the additional optional function parameter in detectMarkers only. But the multiDict version of the function seems to work fine for what I'm trying to do detecting multiple dictionaries in the same image.

CV_WRAP const std::vector<Dictionary>& getDictionaries() const;
CV_WRAP void setDictionaries(const std::vector<Dictionary>& dictionaries);
CV_WRAP void addDictionary(const Dictionary& dictionary);
CV_WRAP void removeDictionary(int index);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. In my opinion, methods get(set)Dictionary, add(remove)Dictionary are redundant. It is enough to have get(set)Dictionaries. Perhaps it would be better to preserve old get(set)Dictionary, so that they would return the first one.

  2. Is it safe to return reference to internal vector in method getDictionaries? Maybe it would be better to return a copy.

  3. What about corner cases: empty vector, duplicates?

  4. Methods should be documented.

Copy link
Copy Markdown
Author

@BenjaminKnecht BenjaminKnecht Feb 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for the feedback.

  1. yeah, add/remove may be a bit redundant. I went for maximal convenience here so the dictionaries list can be manipulated in many ways. I want to point out though that the default behavior of get/setDictionary (so without the optional index) is preserved.
  2. As long as it's const it's safe. You can always get a copy, rather than a view of the insides by doing vector<Dictionary> copy = detector.getDictionaries() rather than vector<Dictionary>& reference = detector.getDictionary(). So in order to not do a copy you have to specify that you're taking a reference. If you're asking for safety in terms of a malevolent attacker rather than a potential source of a bug, then yes, returning a copy would be better.
  3. Empty vector isn't allowed. I have a check in the constructor and the removeDictionary() function for that, but I forgot to check in the setDictionaries(). Thanks for spotting this! On duplicates I purposefully did not add a check and leave some of the responsibilities to the user. I could change this to use an unsorted_set internally that automatically ignores duplicates, but the interface would remain a vector. This would also mean that getDictionaries() would return a copy for sure. Even then you could do nonsensical stuff like running DICT_4X4_50 and then DICT_4X4_100 and potentially find a lot of duplicated markers. This can of course be solved by just running the larger dictionary and ignoring the smaller one, but that's a lot of hand-holding that I am not used to with the OpenCV API.
  4. Sure, makes sense. I added some non-trivial complexity after all.

So for now I will fix the bug that allows for setting an empty vector and I'll add documentation. On the rest I'd like some confirmation for my suggestions and then I'm happy to make the changes.

Copy link
Copy Markdown
Contributor

@mshabunin mshabunin Feb 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Old get/setDictionary API would work, yes. But ABI compatibility would be broken. Can we avoid it?
  2. No I meant safety in terms of exposing internal structure. For example it allows user to create iterator which might become invalid suddenly. Also we won't be able to change internal data structure.
  3. Each access function must be tested for possible errors then.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Alright, I've updated the PR to keep the behavior of the set/get functions and the ABI the same.
  2. Yeah, good point with the iterators. I make it return a copy now.
  3. I think I had tests already, but since you asked about it earlier, the most current version now has no add/remove functions and thus it's a lot easier to use and also easier to test.

I hope this addresses all the concerns. I left a bit of documentation for the set/getDictionary functions so it's clear what they do with the dictionaries list.

* input image with corresponding camera model, if camera parameters are known
* @sa undistort, estimatePoseSingleMarkers, estimatePoseBoard
*/
CV_WRAP void detectMarkersMultiDict(InputArray image, OutputArrayOfArrays corners, OutputArray ids,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be more convenient to modify Dictionary class, so that it would be able to combine several dictionaries underneath. It could also handle duplicate markers and provide convenient indexing. Detector interface would stay the same and users would require minimal changes in their code.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bad idea, but the Dictionary class is also used for these calibration boards and the Charuco stuff as well. Its public members (errorCorrection, markerSize, bytesList) would all need to change because they would be ambiguous. Just from a quick check I think such a change would be more disruptive than the interface change suggested in this PR.

Copy link
Copy Markdown
Contributor

@asmorkalov asmorkalov Feb 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think such a change would be more disruptive than the interface change suggested in this PR.

Agree and support

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have function extendDictionary which adds randomized markers to the existing dictionary, so I believe creating similar function to combine several dictionaries with the same marker size would be quite easy. Or do we need to have dictionaries with different marker sizes?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least for the use case we have in mind you would want to scan dictionaries of different marker sizes, yes.

void ArucoDetector::write(FileStorage &fs) const
{
arucoDetectorImpl->dictionary.writeDictionary(fs);
void ArucoDetector::write(FileStorage &fs) const {
Copy link
Copy Markdown
Contributor

@mshabunin mshabunin Feb 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe (de)serialization is not tested. Is old format supported? I.e. if we have just one dictionary, would older versions of OpenCV be able to read it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, I couldn't find a unit test for it, but I tested it locally. I can clean the code up and add it as a test.

I made sure it could read the old format, but it wouldn't produce the old format if there's only one dictionary. I'll add that, so both read and write support the old format if there is only on dict.

@asmorkalov
Copy link
Copy Markdown
Contributor

Debug test failed:

[ RUN      ] CV_ArucoMultiDict.serialization
unknown file: Failure
C++ exception with description "OpenCV(4.12.0-dev) /home/ci/opencv/modules/core/include/opencv2/core/mat.inl.hpp:1014: error: (-215:Assertion failed) elemSize() == sizeof(_Tp) in function 'begin'
" thrown in the test body.
[  FAILED  ] CV_ArucoMultiDict.serialization (2 ms)

@BenjaminKnecht
Copy link
Copy Markdown
Author

Debug test failed:

[ RUN      ] CV_ArucoMultiDict.serialization
unknown file: Failure
C++ exception with description "OpenCV(4.12.0-dev) /home/ci/opencv/modules/core/include/opencv2/core/mat.inl.hpp:1014: error: (-215:Assertion failed) elemSize() == sizeof(_Tp) in function 'begin'
" thrown in the test body.
[  FAILED  ] CV_ArucoMultiDict.serialization (2 ms)

Took me a bit to understand the problem with the Mat type here and why it only failed in Debug mode. Should be fine now and I hope the serialization test is valuable.

@asmorkalov
Copy link
Copy Markdown
Contributor

@mshabunin Could you take a look again. I believe the patch is ready for integration.

});
}
} else if (DictionaryMode::Multi == dictMode) {
unordered_set<int> uniqueMarkerSizes;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not regular std::set?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made it a map now as marker size needs to map to candidate tree. I made this connection now more deliberately and direct so I also don't need to calculate indices for other containers which may cause bounds issues as you correctly pointed out.


TEST(CV_ArucoMultiDict, serialization)
{
const std::string fileName("test_aruco_serialization.json");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use cv::tempfile for file name and make sure file is removed using remove at the end (no ASSERT_ can be used).

Alternatively you can try to use in-memory storage:

FileStorage fs_in(ymlfile, FileStorage::READ + FileStorage::MEMORY);
matcher->read(fs_in.root());
FileStorage fs_out(".yml", FileStorage::WRITE + FileStorage::MEMORY);
matcher->write(fs_out);
std::string out = fs_out.releaseAndGetString();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that's what I was looking for.

{
FileStorage fs(fileName, FileStorage::Mode::WRITE);
if (!fs.isOpened()) {
cout << "[ SKIPPED ] " << "CV_ArucoMultiDict.serialization"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failure to open file should become test failure. Here and in other places.

* @param dictIndices vector of dictionary indices for each detected marker. Use getDictionaries() to get the
* list of corresponding dictionaries.
*
* Performs marker detection in the input image. Only markers included in the specific dictionaries
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it would be useful to define and test behavior in case when several dictionaries contain same markers.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Added a test that shows off how you can detect three markers for 2 similar dictionaries.

// copy candidates
vector<vector<Point2f>> candidatesCopy = candidates;
vector<vector<Point> > contoursCopy = contours;
candidatesPerDictionarySize[dictionarySizeIndex] = filterTooCloseCandidates(candidatesCopy, contoursCopy, markerSize);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, this function filters candidates using marker size as a hint to estimate distances or something. Could dictionaries with different marker sizes affect detections of each other?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was also my worry and when reading into the code of the function it may affect candidates in some corner cases. This is why I'm making copies of the vectors before filtering, so the order of identifying with multiple dictionaries doesn't matter. The copies are an overhead though that I'd like to avoid.

}

// create at max 4 marker candidate trees for each dictionary size
vector<vector<MarkerCandidateTree>> candidatesPerDictionarySize = {{}, {}, {}, {}};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can dictionary have marker size other than 4, 5, 6, 7? I don't see any checks in Dictionary constructor or other places. If it can, then we have possible out-of-bounds access.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was thinking about the predefined dictionaries only, but custom dictionaries could potentially break this. I made it more flexible that now any size of marker is supported.

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.
@mshabunin
Copy link
Copy Markdown
Contributor

Looks good to me. Please fix Windows warning:

C:\build\precommit_windows64\4.x\opencv\modules\objdetect\src\aruco\aruco_detector.cpp(768): warning C4267: 'argument': conversion from 'size_t' to 'int', possible loss of data [C:\build\precommit_windows64\build\modules\objdetect\opencv_objdetect.vcxproj]

@BenjaminKnecht
Copy link
Copy Markdown
Author

Thank you to everyone for the reviews and the productive discussions. This is my first attempted contribution to OpenCV and I did enjoy the process so far.

@asmorkalov what's the next step? Can we merge the PR as it is now?

@asmorkalov asmorkalov merged commit d9956fc into opencv:4.x Mar 11, 2025
28 checks passed
@asmorkalov asmorkalov mentioned this pull request Mar 11, 2025
@BenjaminKnecht BenjaminKnecht deleted the new_4.x branch March 12, 2025 09:16
@JonasPerolini
Copy link
Copy Markdown
Contributor

Hi @BenjaminKnecht,

While working on #23190 I've tested the Multi-dictionary implementation and noticed that if we use two dictionaries that have markers in common e.g. DICT_4X4_50_DATA and DICT_4X4_100_DATA, those 50 markers will be detected twice.

More generally, every candidate is tested against every dictionary with the same markerSize, even if some candidates were identified.

Is this intended?

If not, when looping over the dictionaries for (const Dictionary& currentDictionary : dictionaries) we can remove the identified candidates from the map candidatesPerDictionarySize.at(currentDictionary.markerSize).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants