From 8be8199c2a8e80a427c55e15e8a0cba87d915eac Mon Sep 17 00:00:00 2001 From: breken-ai <312387581+breken-ai@users.noreply.github.com> Date: Fri, 25 Sep 2026 01:56:47 -0700 Subject: [PATCH] BUG: Order find results by score inside the search region. locate_all_opencv sorted its matches by reading the matchTemplate result at the screen position (x, y). That position already includes the region offset, but the result array is indexed from the region's own origin. A search that starts away from the top-left corner, such as find(label, x=100), therefore read the wrong scores or raised IndexError as soon as it found a match. Sort by the score taken at the match's position inside the result array, then add the offset for the returned box. --- botcity/core/cv2find.py | 15 ++++--- botcity/core/tests/test_cv2find.py | 65 ++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 botcity/core/tests/test_cv2find.py diff --git a/botcity/core/cv2find.py b/botcity/core/cv2find.py index 39f6f98..0417eae 100644 --- a/botcity/core/cv2find.py +++ b/botcity/core/cv2find.py @@ -154,10 +154,15 @@ def locate_all_opencv( # use a generator for API consistency: matchx = matches[1] * step + region[0] # vectorized matchy = matches[0] * step + region[1] - - # Order results before sending back - ordered = sorted( - zip(matchx, matchy), key=lambda p: result[p[1]][p[0]], reverse=True - ) + scores = result[matches] + + # Order results before sending back. Score by the position inside + # `result`, not by the screen position, which includes the region offset. + ordered = [ + (x, y) + for _, x, y in sorted( + zip(scores, matchx, matchy), key=lambda p: p[0], reverse=True + ) + ] for x, y in ordered: yield Box(x, y, needle_width, needle_height) diff --git a/botcity/core/tests/test_cv2find.py b/botcity/core/tests/test_cv2find.py new file mode 100644 index 0000000..204dbaa --- /dev/null +++ b/botcity/core/tests/test_cv2find.py @@ -0,0 +1,65 @@ +import numpy + +from botcity.core import cv2find + + +def _images(): + rng = numpy.random.default_rng(0) + needle = rng.integers(0, 256, (10, 10, 3), dtype=numpy.uint8) + haystack = rng.integers(0, 256, (100, 200, 3), dtype=numpy.uint8) + return needle, haystack + + +def test_locate_all_opencv_with_region_offset_returns_match(): + needle, haystack = _images() + haystack[60:70, 150:160] = needle + + # Search only the right half of the screen, as find(x=100, ...) does. + found = list( + cv2find.locate_all_opencv( + needle, haystack, region=(100, 0, 100, 100), confidence=0.9 + ) + ) + + assert found == [cv2find.Box(150, 60, 10, 10)] + + +def test_locate_all_opencv_with_region_offset_orders_by_score(): + needle, haystack = _images() + # A slightly damaged copy scores lower than the exact copy. + damaged = needle.copy() + damaged[0:3, 0:3] = 255 - damaged[0:3, 0:3] + haystack[10:20, 110:120] = damaged + haystack[60:70, 150:160] = needle + + found = list( + cv2find.locate_all_opencv( + needle, haystack, region=(100, 0, 100, 100), confidence=0.5 + ) + ) + + assert found[0] == cv2find.Box(150, 60, 10, 10) + assert cv2find.Box(110, 10, 10, 10) in found + + +def test_find_with_x_offset_returns_element(tmp_path, monkeypatch): + from PIL import Image + + from botcity.core import DesktopBot + + needle, haystack = _images() + haystack[60:70, 150:160] = needle + needle_path = tmp_path / "needle.png" + Image.fromarray(needle[:, :, ::-1]).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(haystack[:, :, ::-1]) + ) + monkeypatch.setattr("botcity.core.bot.is_retina", lambda: False) + + ele = bot.find("needle", x=100, matching=0.9, waiting_time=1000) + + assert ele == cv2find.Box(150, 60, 10, 10)