Skip to content

Commit

Permalink
feat: allow all img args to be image data
Browse files Browse the repository at this point in the history
Expand the API to allow `img` args to be image data. This feature
targets performance inside loops. Before, images had to be paths and
were read in every loop iteration. Now, images can be read outside loops
to minimize repeated work. This is expected to make loop iterations
about 1ms faster per image read avoided on average.
  • Loading branch information
aentwist committed Sep 9, 2024
1 parent 52cd6b3 commit fe46177
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 31 deletions.
21 changes: 8 additions & 13 deletions gamedriver/_locate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


def locate(
img: str,
img: str | cv.typing.MatLike,
*,
bounding_box: Box = None,
convert_to_grayscale=True,
Expand All @@ -27,7 +27,8 @@ def locate(
:py:func:`gamedriver.get_img_path`.
Args:
img (str): Relative path to the needle image, with no file extension
img (str | cv.typing.MatLike): Image or relative path to the image to
search for with no file extension
Returns:
Box | None
Expand All @@ -37,12 +38,9 @@ def locate(
box = gd.locate("buttons/confirm")
"""
screen = (
get_screen(grayscale=is_grayscale) if is_grayscale is not None else get_screen()
)
return match_template(
screen,
get_img_path(img),
get_screen(grayscale=is_grayscale),
get_img_path(img) if isinstance(img, str) else img,
bounding_box=bounding_box,
convert_to_grayscale=convert_to_grayscale,
is_grayscale=is_grayscale,
Expand All @@ -52,7 +50,7 @@ def locate(


def locate_all(
img: str,
img: str | cv.typing.MatLike,
*,
bounding_box: Box = None,
convert_to_grayscale=True,
Expand All @@ -69,12 +67,9 @@ def locate_all(
Yields:
Iterator[Box]
"""
screen = (
get_screen(grayscale=is_grayscale) if is_grayscale is not None else get_screen()
)
return match_template_all(
screen,
get_img_path(img),
get_screen(grayscale=is_grayscale),
get_img_path(img) if isinstance(img, str) else img,
bounding_box=bounding_box,
convert_to_grayscale=convert_to_grayscale,
is_grayscale=is_grayscale,
Expand Down
32 changes: 18 additions & 14 deletions gamedriver/_tap_img.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

from gamedriver._geometry import Box, tap_box
from gamedriver._locate import locate
from gamedriver._util import wait
from gamedriver._util import get_img_path, open_img, wait
from gamedriver.logger import logger
from gamedriver.settings import settings


def wait_until_img_visible(
img: str,
img: str | cv.typing.MatLike,
*,
bounding_box: Box = None,
convert_to_grayscale=True,
Expand All @@ -33,11 +33,12 @@ def wait_until_img_visible(
Box | None: The match, or None if there is no match within `timeout_s`.
"""
polling_interval_s = settings["refresh_rate_ms"] / 1_000
img_bgr = open_img(get_img_path(img)) if isinstance(img, str) else img

box = None
for i in range(math.floor(timeout_s / polling_interval_s)):
box = locate(
img,
img_bgr,
bounding_box=bounding_box,
convert_to_grayscale=convert_to_grayscale,
is_grayscale=is_grayscale,
Expand All @@ -55,7 +56,7 @@ def wait_until_img_visible(


def tap_img(
img: str,
img: str | cv.typing.MatLike,
*,
bounding_box: Box = None,
convert_to_grayscale=True,
Expand Down Expand Up @@ -86,7 +87,7 @@ def tap_img(

# Makes us seem a little more human, if you're into that ;) (at the expense of speed)
def tap_img_when_visible_after_wait(
img: str,
img: str | cv.typing.MatLike,
*,
bounding_box: Box = None,
convert_to_grayscale=True,
Expand Down Expand Up @@ -121,7 +122,7 @@ def tap_img_when_visible_after_wait(


def tap_img_when_visible(
img: str,
img: str | cv.typing.MatLike,
*,
bounding_box: Box = None,
convert_to_grayscale=True,
Expand All @@ -148,8 +149,8 @@ def tap_img_when_visible(


def tap_img_while_other_visible(
img: str,
other: str,
img: str | cv.typing.MatLike,
other: str | cv.typing.MatLike,
*,
bounding_box: Box = None,
other_bounding_box: Box = None,
Expand All @@ -166,8 +167,8 @@ def tap_img_while_other_visible(
:py:func:`gamedriver.wait_until_img_visible`.
Args:
img (str): Image to tap
other (str): Image to check the visibility of
img (str | cv.typing.MatLike): Image to tap
other (str | cv.typing.MatLike): Image to check the visibility of
frequency_s (int, optional): How often the image to tap should be
tapped, in seconds. Defaults to 1.
Expand All @@ -176,10 +177,13 @@ def tap_img_while_other_visible(
tapping was stopped, or False if the image to check remained after
the timeout and tapping was continued until the end.
"""
img_bgr = open_img(get_img_path(img)) if isinstance(img, str) else img
other_bgr = open_img(get_img_path(other)) if isinstance(other, str) else other

tap_count = math.floor(timeout_s / frequency_s)
for _ in range(tap_count):
if not locate(
other,
other_bgr,
bounding_box=other_bounding_box,
convert_to_grayscale=convert_to_grayscale,
is_grayscale=is_grayscale,
Expand All @@ -188,7 +192,7 @@ def tap_img_while_other_visible(
):
break
tap_img(
img,
img_bgr,
bounding_box=bounding_box,
convert_to_grayscale=convert_to_grayscale,
is_grayscale=is_grayscale,
Expand All @@ -207,7 +211,7 @@ def tap_img_while_other_visible(


def tap_img_while_visible(
img: str,
img: str | cv.typing.MatLike,
*,
bounding_box: Box = None,
convert_to_grayscale=True,
Expand All @@ -222,7 +226,7 @@ def tap_img_while_visible(
See :py:func:`tap_img_while_other_visible`.
Args:
img (str): Image to check the visibility of and tap
img (str | cv.typing.MatLike): Image to check the visibility of and tap
"""
# Touching an image while it is visible is a special case of taping it
# while an arbitrary image is visible
Expand Down
26 changes: 22 additions & 4 deletions tests/test_tap_img.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import Mock, patch
from unittest.mock import patch

import cv2 as cv
import pytest
Expand All @@ -7,6 +7,20 @@
from tests.util import get_test_ss_path as ss, get_test_templ_path as templ


@pytest.fixture
def locate_campaign_begin():
with patch("gamedriver._tap_img.locate") as locate:
locate.return_value = gd.Box(left=320, top=1602, right=760, bottom=1729)
yield locate


@pytest.fixture
def locate_not_found():
with patch("gamedriver._tap_img.locate") as locate:
locate.return_value = None
yield locate


# TODO: Some of this goes into locate and should be split out. locate itself
# should be mocked here.

Expand All @@ -24,7 +38,7 @@ def get_screen():

@pytest.fixture
def get_img_path():
with patch("gamedriver._locate.get_img_path") as get_img_path:
with patch("gamedriver._tap_img.get_img_path") as get_img_path:
get_img_path.side_effect = templ
yield get_img_path

Expand Down Expand Up @@ -56,12 +70,16 @@ def tap_box():
yield tap_box


def test_tap_img(get_screen, get_img_path, tap_box):
def test_tap_img(locate_campaign_begin, tap_box):
assert gd.tap_img("begin")
locate_campaign_begin.assert_called_once()
tap_box.assert_called_once()


def test_tap_img_not_found(get_screen, get_img_path, tap_box):
def test_tap_img_not_found(locate_not_found, tap_box):
assert not gd.tap_img("dispatch-brown")
locate_not_found.assert_called_once()
tap_box.assert_not_called()


def test_tap_img_when_visible_after_wait(get_screen, get_img_path, tap_box):
Expand Down

0 comments on commit fe46177

Please sign in to comment.