Skip to content

Identify ArUco markers based on threshold to reduce false positives#28289

Merged
asmorkalov merged 12 commits intoopencv:4.xfrom
JonasPerolini:pr-aruco-identification
Mar 4, 2026
Merged

Identify ArUco markers based on threshold to reduce false positives#28289
asmorkalov merged 12 commits intoopencv:4.xfrom
JonasPerolini:pr-aruco-identification

Conversation

@JonasPerolini
Copy link
Copy Markdown
Contributor

Goal: parametrize the current marker identification process (pixel-based majority count) to reduce the number of false positives while maintaining high recall. Useful in high risk scenarios in which false positives are not acceptable.

Context: This PR builds on top of #23190 in which we've introduced a pixel-based confidence in the marker detection.

Solution: Include a new parameter: validBitIdThreshold used to identify markers based on the pixel count of each cell. Set the parameter default either to 50% which is equivalent to the current majority count implementation or to 49% which already singnificantly reduces the number of false positives (see details below).

Test coverage:

  • Unit tests: CV_ArucoDetectionThreshold, CV_InvertedArucoDetectionThreshold
  • The impact of validBitIdThreshold on false positives was also tested using the benchmark dataset: MIRFLICKR-25k https://www.kaggle.com/datasets/skfrost19/mirflickr25k which contains random images without any markers. Every marker detection is a false positive.

Example of images in the dataset:

im2048

im17627

Results: A threshold of 49% already allows to significantly reduce the number of false positives for the dict DICT_4X4_1000:

  • 5942 false positives for validBitIdThreshold = 0.5
  • 629 false positives for validBitIdThreshold = 0.49 and 0.46
    • number of false positives divided by 9.5 when compared to validBitIdThreshold = 0.5
  • 139 false positives for validBitIdThreshold = 0.43 and 0.4
    • number of false positives divided by 42 when compared to validBitIdThreshold = 0.5

Dicts with a higher number of cells are not as impacted since it's much harder to obtain false positives. However, the less cells in a marker the further away it can be reliably detected, so the dict DICT_4X4_1000 is commonly used.

false_positive_image_rate

In the image attached, the values of validBitIdThreshold tested are: 0.10f, 0.20f, 0.30f, 0.40f, 0.43f, 0.46f, 0.49f, 0.50f, 0.53f, 0.56f, 0.60f, 0.70f, 0.80f, 0.90f

Summary of the results: summary.csv

Note that we can also analyse the number of false positives per marker id. For example, here's the histogram for the dict DICT_4X4_1000. (The CSV attached contains all the results)

false_positive_ids_DICT_4X4_1000_thr0 50

For example, the marker id 17 is detected 252 times with validBitIdThreshold = 0.5 and only 34 times with validBitIdThreshold = 0.49. Looking at marker 17 (see below), we understand that this simple pattern randomly occurs in images.

Marker17

Results for every dict and every validBitIdThreshold per_id.csv

Missing coverage: there is no labeled dataset with images containing markers to analyse the impact of on the recall (i.e. look at the true positive rate). For my specific use case (drones) any threshold above 0.4 allows to maintain a high recall in all conditions.

Pull Request Readiness Checklist

  • 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

@asmorkalov
Copy link
Copy Markdown
Contributor

Mac OS ARM:

Discovering python tests from: /Users/opencv-cn/GHA-OCV-2/_work/opencv/opencv/build

    found 0 tests
======================================================================
Discovering python tests from: /Users/opencv-cn/GHA-OCV-2/_work/opencv/opencv/opencv/modules/python/test
FAIL: test_multi_dict_arucodetector (test_objdetect_aruco.aruco_objdetect_test)
    found 136 tests
----------------------------------------------------------------------
Discovering python tests from: /Users/opencv-cn/GHA-OCV-2/_work/opencv/opencv/opencv/modules/flann/misc/python/test
Traceback (most recent call last):
    found 1 tests
  File "/Users/opencv-cn/GHA-OCV-2/_work/opencv/opencv/opencv/modules/objdetect/misc/python/test/test_objdetect_aruco.py", line 479, in test_multi_dict_arucodetector
Discovering python tests from: /Users/opencv-cn/GHA-OCV-2/_work/opencv/opencv/opencv/modules/imgproc/misc/python/test
    self.assertEqual(2, len(ids))
    found 1 tests
AssertionError: 2 != 3
Discovering python tests from: /Users/opencv-cn/GHA-OCV-2/_work/opencv/opencv/opencv/modules/ml/misc/python/test

    found 4 tests
----------------------------------------------------------------------
Discovering python tests from: /Users/opencv-cn/GHA-OCV-2/_work/opencv/opencv/opencv/modules/dnn/misc/python/test
Ran 283 tests in 50.794s
    found 16 tests

Discovering python tests from: /Users/opencv-cn/GHA-OCV-2/_work/opencv/opencv/opencv/modules/features2d/misc/python/test
FAILED (failures=1, skipped=18)

@JonasPerolini
Copy link
Copy Markdown
Contributor Author

Thank you for the heads up @asmorkalov. The test test_multi_dict_arucodetector fails because validBitIdThreshold is set to 0.5 by default and it leads to a false positive detection of marker 103 from the 4x4 dict. A similar issue happens with TEST(Charuco, issue_14014).

Why does validBitIdThreshold = 0.5 behave differently than the previous implementation?

Previous implementation: if(nZ > square.total() / 2) bits.at<unsigned char>(y, x) = 1; the key here is >. We were defaulting to 0 (black cell) if nZ == square.total() / 2. So we were arbitrarily giving "priority" to black cells:

  • if the ground truth cell was black --> no error.
  • if the ground truth cell was white --> error.

New implementation:

if(fabs(onlyCellPixelRatio.at<float>(i, j) - static_cast<float>(bitsRot.at<float>(i, j))) > validBitIdThreshold){
       currentHamming++;
}

If we set validBitIdThreshold = 0.5 , all nZ == square.total() / 2 will be considered as correct by default which will lead to an increase of detections. If we set validBitIdThreshold = 0.49, all nZ == square.total() / 2 will be considered as incorrect.

My preferred solution is to reduce the number of false positive detections by setting the default validBitIdThreshold to 0.49 instead of 0.5. I've updated the default in 6bb5090

What do you think @asmorkalov ?

Note that setting validBitIdThreshold = 0.49 leads to failures in the tests below but I'm not too concerned because it only affects a minor part of the detections e.g. 11/12 still detected:

  • CV_ArucoTutorial.can_find_diamondmarkers to fail by detecting 11 instead of 12 markers, so for this specific test I set detectorParams.validBitIdThreshold = 0.5f
  • CV_ArucoBoardPose and CV_Aruco3BoardPose 8 markers detected instead of 9
  • ArucoThreading 1 marker not detected

diamondmarkers

@asmorkalov asmorkalov modified the milestones: 4.13.0, 4.14.0 Dec 31, 2025
@JonasPerolini
Copy link
Copy Markdown
Contributor Author

Hi @asmorkalov, the default check is failing in Compare ABI dumps. In this PR, we include a new entry: validBitIdThreshold inside struct CV_EXPORTS_W_SIMPLE DetectorParameters . How can we proceed?

@JonasPerolini
Copy link
Copy Markdown
Contributor Author

Hi @asmorkalov, I'm still interested in this PR to reduce the chance of false-positive detections. Is there a path forward despite the new parameter?

Comment on lines +23 to +24
static constexpr float kDefaultValidBitIdThreshold{0.49f};

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 do you need it as dedicated constant? I propose to remove it from API and just stay it as field initializer.

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 do you use 0.49 by default, but force 0.5 in tests? M.b. better to have 0.5 by default and do not touch tests (cover the default params in the tests).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi,

Why do you need it as dedicated constant?

I need it to make sure that the legacy Dictionary::identify function uses the same default as the default DetectorParameters

I propose to remove it from API and just stay it as field initializer.

I'm not sure to understand, could you please provide more details?

Why do you use 0.49 by default, but force 0.5 in tests?

Please see #28289 (comment), which explains the reasons behind 0.49 vs 0.5. Note that in both cases some tests will fail.

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.

Let's rename the constant with capital letters LIKE_THIS. It's common OpenCV style.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated, what about the 0.49 vs 0.5?

@asmorkalov asmorkalov merged commit 5e91b46 into opencv:4.x Mar 4, 2026
44 of 51 checks passed
@asmorkalov asmorkalov mentioned this pull request Mar 19, 2026
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.

2 participants