CARVIEW |
Navigation Menu
-
-
Notifications
You must be signed in to change notification settings - Fork 56.2k
Extend ArUcoDetector to run multiple dictionaries in an efficient manner. #26934
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
manner. * Add constructor for multiple dictionaries * Add get/set/remove/add functions for multiple dictionaries * Add unit tests TESTED=unit tests
There was a problem hiding this 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
For Python compatibility, please add opencv/modules/python/src2/cv2.cpp Line 47 in 6092499
|
@@ -1114,7 +1151,7 @@ void ArucoDetector::refineDetectedMarkers(InputArray _image, const Board& _board | |||
InputOutputArrayOfArrays _rejectedCorners, InputArray _cameraMatrix, | |||
InputArray _distCoeffs, OutputArray _recoveredIdxs) const { | |||
DetectorParameters& detectorParams = arucoDetectorImpl->detectorParams; | |||
const Dictionary& dictionary = arucoDetectorImpl->dictionary; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
Warning in all Windows builds:
|
@dkurt @AleksandrPanov @mshabunin What do you think? |
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. |
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
In my opinion, methods
get(set)Dictionary
,add(remove)Dictionary
are redundant. It is enough to haveget(set)Dictionaries
. Perhaps it would be better to preserve oldget(set)Dictionary
, so that they would return the first one. -
Is it safe to return reference to internal vector in method
getDictionaries
? Maybe it would be better to return a copy. -
What about corner cases: empty vector, duplicates?
-
Methods should be documented.
There was a problem hiding this comment.
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.
- 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. - As long as it's
const
it's safe. You can always get a copy, rather than a view of the insides by doingvector<Dictionary> copy = detector.getDictionaries()
rather thanvector<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. - Empty vector isn't allowed. I have a check in the constructor and the
removeDictionary()
function for that, but I forgot to check in thesetDictionaries()
. 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 anunsorted_set
internally that automatically ignores duplicates, but the interface would remain a vector. This would also mean thatgetDictionaries()
would return a copy for sure. Even then you could do nonsensical stuff like runningDICT_4X4_50
and thenDICT_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. - 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Old get/setDictionary API would work, yes. But ABI compatibility would be broken. Can we avoid it?
- 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.
- Each access function must be tested for possible errors then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Alright, I've updated the PR to keep the behavior of the set/get functions and the ABI the same.
- Yeah, good point with the iterators. I make it return a copy now.
- 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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Debug test failed:
|
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. |
@mshabunin Could you take a look again. I believe the patch is ready for integration. |
}); | ||
} | ||
} else if (DictionaryMode::Multi == dictMode) { | ||
unordered_set<int> uniqueMarkerSizes; |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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:
opencv/modules/features2d/test/test_matchers_algorithmic.cpp
Lines 591 to 595 in dbd3ef9
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(); |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 = {{}, {}, {}, {}}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Looks good to me. Please fix Windows warning:
|
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? |
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 |
TESTED=unit tests
Pull Request Readiness Checklist
See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request
Patch to opencv_extra has the same branch name.