From b16af518163f482ae42ae7f307f92e142c8b927a Mon Sep 17 00:00:00 2001 From: breken-ai <312387581+breken-ai@users.noreply.github.com> Date: Fri, 25 Sep 2026 01:57:30 -0700 Subject: [PATCH] BUG: Drop find_all duplicates above or left of the best match. find_all removes a match when it overlaps a better one, but the overlap check only looked for boxes whose top-left corner lies inside the better box, that is below and to the right of it. A match one pixel above or to the left of the same element was kept, so find_all returned the same element several times. Compare the distance between the two boxes in both directions. See #50 --- botcity/core/bot.py | 11 +++---- botcity/core/tests/test_find_all.py | 50 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 botcity/core/tests/test_find_all.py diff --git a/botcity/core/bot.py b/botcity/core/bot.py index aa8728d..9bee216 100644 --- a/botcity/core/bot.py +++ b/botcity/core/bot.py @@ -443,17 +443,14 @@ def deduplicate( elems: list[Generator[cv2find.Box, Any, None]] ) -> list[Generator[cv2find.Box, Any, None]]: def find_same(item, items): - x_start = item.left - x_end = item.left + item.width - y_start = item.top - y_end = item.top + item.height similars = [] for itm in items: if itm == item: continue - if (itm.left >= x_start and itm.left < x_end) and ( - itm.top >= y_start and itm.top < y_end - ): + # Boxes overlap no matter which side of `item` they are on. + if abs(itm.left - item.left) < item.width and abs( + itm.top - item.top + ) < item.height: similars.append(itm) continue return similars diff --git a/botcity/core/tests/test_find_all.py b/botcity/core/tests/test_find_all.py new file mode 100644 index 0000000..5090b5a --- /dev/null +++ b/botcity/core/tests/test_find_all.py @@ -0,0 +1,50 @@ +import numpy +from PIL import Image + +from botcity.core import DesktopBot, cv2find + + +def _bot_with_screen(tmp_path, monkeypatch, needle, screen): + needle_path = tmp_path / "needle.png" + Image.fromarray(needle).save(needle_path) + + bot = DesktopBot() + bot.add_image("needle", str(needle_path)) + monkeypatch.setattr(bot, "_fix_display_size", lambda: (200, 100)) + monkeypatch.setattr(bot, "get_screenshot", lambda *a, **k: Image.fromarray(screen)) + monkeypatch.setattr("botcity.core.bot.is_retina", lambda: False) + return bot + + +def test_find_all_drops_overlapping_matches_above_or_left_of_the_best( + tmp_path, monkeypatch +): + rng = numpy.random.default_rng(0) + screen = rng.integers(0, 256, (100, 200, 3), dtype=numpy.uint8) + # A smooth element also scores well one or more pixels away from where + # it really is, in every direction. + needle = numpy.zeros((20, 20, 3), numpy.uint8) + needle[:] = numpy.linspace(0, 255, 20, dtype=numpy.uint8)[None, :, None] + screen[40:60, 50:70] = needle + + bot = _bot_with_screen(tmp_path, monkeypatch, needle, screen) + + found = list(bot.find_all("needle", matching=0.8, waiting_time=1000)) + + assert found == [cv2find.Box(50, 40, 20, 20)] + + +def test_find_all_keeps_elements_that_touch_but_do_not_overlap( + tmp_path, monkeypatch +): + rng = numpy.random.default_rng(1) + screen = rng.integers(0, 256, (100, 200, 3), dtype=numpy.uint8) + needle = rng.integers(0, 256, (10, 10, 3), dtype=numpy.uint8) + screen[20:30, 40:50] = needle + screen[20:30, 50:60] = needle + + bot = _bot_with_screen(tmp_path, monkeypatch, needle, screen) + + found = sorted(bot.find_all("needle", matching=0.9, waiting_time=1000)) + + assert found == [cv2find.Box(40, 20, 10, 10), cv2find.Box(50, 20, 10, 10)]