diff --git a/src/tapper/helper/_util/image_util.py b/src/tapper/helper/_util/image_util.py index c1a4612..a7c7fab 100644 --- a/src/tapper/helper/_util/image_util.py +++ b/src/tapper/helper/_util/image_util.py @@ -7,7 +7,6 @@ from typing import Union import mss -import numpy import numpy as np import PIL.Image import PIL.ImageGrab @@ -15,6 +14,9 @@ from mss.base import MSSBase from numpy import ndarray from tapper.helper._util import image_fuzz +from tapper.helper.model_types import ImagePathT +from tapper.helper.model_types import ImagePixelMatrixT +from tapper.helper.model_types import ImageT from tapper.model import constants _bbox_pattern = re.compile(r"\(BBOX_-?\d+_-?\d+_-?\d+_-?\d+\)") @@ -28,10 +30,23 @@ def get_mss() -> MSSBase: return mss_instance -@lru_cache -def from_path(pathlike: str) -> ndarray: +@lru_cache(maxsize=5) +def from_path(pathlike: ImagePathT) -> ImagePixelMatrixT: + if isinstance(pathlike, str): + pathlike = os.path.abspath(pathlike) pil_img = PIL.Image.open(pathlike).convert("RGB") - return numpy.asarray(pil_img) + return np.asarray(pil_img) + + +def to_pixel_matrix(image: ImageT | None) -> ImagePixelMatrixT | None: + if image is None: + return None + elif isinstance(image, ndarray): + return image + elif isinstance(image, (str, bytes, os.PathLike)): + return from_path(os.path.abspath(image)) + else: + raise TypeError(f"Unexpected type, {type(image)} of {image}") def normalize( @@ -55,7 +70,7 @@ def normalize( if not bbox and (str_bbox := _bbox_pattern.search(data_in)): sx = str_bbox.group().split("_") bbox = int(sx[1]), int(sx[2]), int(sx[3]), int(sx[4].rstrip(")")) - return from_path(os.path.abspath(data_in)), bbox + return from_path(data_in), bbox raise TypeError(f"Unexpected type, {type(data_in)} of {data_in}") @@ -66,7 +81,7 @@ def get_screenshot_if_none_and_cut( if bbox: return maybe_image[bbox[1] : bbox[3], bbox[0] : bbox[2]] return maybe_image - if bbox: + if bbox is not None: try: sct = get_mss().grab(bbox) except Exception as e: @@ -74,7 +89,7 @@ def get_screenshot_if_none_and_cut( else: sct = get_mss().grab(get_mss().monitors[0]) pil_rgb = PIL.Image.frombytes("RGB", sct.size, sct.bgra, "raw", "BGRX") - return numpy.asarray(pil_rgb) + return np.asarray(pil_rgb) def find_in_image_raw( @@ -90,14 +105,14 @@ def find_in_image_raw( def find_in_image( - inner_image_bbox: tuple[ndarray, tuple[int, int, int, int] | None], + target: ImagePixelMatrixT, + bbox: tuple[int, int, int, int] | None, outer: ndarray | None = None, precision: float = 1.0, ) -> tuple[int, int] | None: - image_arr, bbox = inner_image_bbox x_start, y_start = get_start_coords(outer, bbox) outer = get_screenshot_if_none_and_cut(outer, bbox) - confidence, coords = image_fuzz.find(outer, image_arr) + confidence, coords = image_fuzz.find(outer, target) if confidence < precision: return None @@ -108,7 +123,7 @@ def get_start_coords( outer: ndarray | None, bbox_or_coords: tuple[int, int, int, int] | tuple[int, int] | None, ) -> tuple[int, int]: - if bbox_or_coords: + if bbox_or_coords is not None: return bbox_or_coords[0], bbox_or_coords[1] screenshot_required = outer is None if screenshot_required and sys.platform == constants.OS.win32: @@ -216,7 +231,7 @@ def save_to_disk( def get_pixel_color( coords: tuple[int, int], outer: str | ndarray | None ) -> tuple[int, int, int]: - outer, _ = normalize(outer) # type: ignore + outer = to_pixel_matrix(outer) # type: ignore bbox = coords_to_bbox_1_pixel(coords) outer = get_screenshot_if_none_and_cut(outer, bbox) nd_pixel = outer[0][0] diff --git a/src/tapper/helper/img.py b/src/tapper/helper/img.py index 971908d..81763a2 100644 --- a/src/tapper/helper/img.py +++ b/src/tapper/helper/img.py @@ -8,7 +8,9 @@ import tapper from tapper.helper._util import image_util as _image_util from tapper.helper.model_types import BboxT +from tapper.helper.model_types import ImagePathT from tapper.helper.model_types import ImagePixelMatrixT +from tapper.helper.model_types import ImageT from tapper.helper.model_types import PixelColorT from tapper.helper.model_types import XyCoordsT @@ -37,25 +39,50 @@ """ +def _check_dependencies() -> None: + try: + pass + except ImportError as e: + raise ImportError( + "Looks like you're missing dependencies for tapper img helper." + "Try `pip install tapper[img]` or `pip install tapper[all]`.", + e, + ) + + +def from_path(pathlike: ImagePathT) -> ImagePixelMatrixT: + """Get image from file path.""" + _check_dependencies() + return _image_util.from_path(pathlike) # type: ignore + + def find( - image: SearchableImageT, - outer: str | ImagePixelMatrixT | None = None, + target: ImageT, + bbox: BboxT | None = None, + outer: ImageT | None = None, precision: float = STD_PRECISION, ) -> XyCoordsT | None: """ Search a region of the screen for an image. - :param image: see `SearchableImage`. - :param outer: Optional image in which to find, pathname or numpy array. If not specified, will search on screen. + :param target: what to find. Path to an image, or image object(numpy array). + :param bbox: bounding box of where to search in the outer. + :param outer: Optional image in which to find, path or numpy array. If not specified, will search on screen. :param precision: A number between 0 and 1 to indicate the allowed deviation from the searched image. 0.95 is a difference visible to the eye, and random images can sometimes match up to 0.8. - To calibrate, use get_find_raw method below. + To calibrate, use get_find_raw function. :return: Coordinates X and Y of top-left of the found image relative to the bounding box (if any). If image not found, None is returned. """ - norm_outer = _image_util.normalize(outer)[0] if outer is not None else None # type: ignore - return _image_util.find_in_image(_image_util.normalize(image), norm_outer, precision=precision) # type: ignore + _check_dependencies() + if target is None: + raise ValueError("img.find nees something to search for.") + target_image = _image_util.to_pixel_matrix(target) + outer_image = _image_util.to_pixel_matrix(outer) + return _image_util.find_in_image( + target_image, bbox, outer_image, precision=precision # type: ignore + ) def find_one_of( diff --git a/src/tapper/helper/model_types.py b/src/tapper/helper/model_types.py index 74cdb96..7c267a8 100644 --- a/src/tapper/helper/model_types.py +++ b/src/tapper/helper/model_types.py @@ -4,28 +4,27 @@ from numpy import ndarray -PixelColorT = tuple[int, int, int] +PixelColorT: TypeAlias = tuple[int, int, int] """RGB color of a pixel, 0-255 values.""" -XyCoordsT = tuple[int, int] +XyCoordsT: TypeAlias = tuple[int, int] """ x, y coordinates on an image or screen.""" +BboxT: TypeAlias = tuple[int, int, int, int] +"""Bounding box for an image (rectangle), defined by two points. +x1 y1 x2 y2. Top left is point 1 (x1 y1), bottom right is point 2 (x2 y2).""" + ImagePixelMatrixT: TypeAlias = ndarray """List of lists of pixel colors. -2x2 green pixels: +example, picture with 2x2 green pixels: (not a python's list, but a numpy array) [[[0, 255, 0], [0, 255, 0]], [[0, 255, 0], [0, 255, 0]]] """ -BboxT = tuple[int, int, int, int] -"""Bounding box for an image. -x1 y1 x2 y2. usually top left is x1 y1, and bottom right is x2 y2.""" - - -ImagePathT = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] +ImagePathT: TypeAlias = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] -ImageT = Union[ImagePixelMatrixT, ImagePathT] +ImageT: TypeAlias = Union[ImagePixelMatrixT, ImagePathT] """ Can be: - Image as numpy RGB array. - str or bytes path to an image. diff --git a/tests/manual/tapper_helpers.py b/tests/manual/tapper_helpers.py index e308688..8bc5031 100644 --- a/tests/manual/tapper_helpers.py +++ b/tests/manual/tapper_helpers.py @@ -47,7 +47,7 @@ def helpers() -> None: Group("img").add( { "num1": lambda: print( - img.find(("small_test_img.png", (500, -1080, 600, -900))) + img.find("small_test_img.png", (500, -1080, 600, -900)) ), # open this pic when testing "num2": img.snip(), "num3": lambda: print( diff --git a/tests/tapper/helper/image/img_for_test.py b/tests/tapper/helper/image/img_for_test.py new file mode 100644 index 0000000..571593e --- /dev/null +++ b/tests/tapper/helper/image/img_for_test.py @@ -0,0 +1,22 @@ +import importlib.resources + +import numpy as np +import testresources +from numpy import ndarray +from PIL import Image + + +def from_matrix(matrix: list[list[tuple[int, int, int]]]) -> ndarray: + return np.uint8(matrix) # type: ignore + + +def get_picture(name: str) -> ndarray: + full_name = ( + importlib.resources.files(testresources).joinpath("image").joinpath(name) + ) + pil_img = Image.open(full_name).convert("RGB") + return np.asarray(pil_img) + + +def absolutes() -> ndarray: + return get_picture("absolutes.png") diff --git a/tests/tapper/helper/image/test_img.py b/tests/tapper/helper/image/test_img.py new file mode 100644 index 0000000..d5759db --- /dev/null +++ b/tests/tapper/helper/image/test_img.py @@ -0,0 +1,15 @@ +import img_for_test +from tapper.helper import img + +red = 255, 0, 0 +green = 0, 255, 0 +blue = 0, 0, 255 +black = 0, 0, 0 +white = 255, 255, 255 + + +class TestFind: + def test_simplest(self) -> None: + absolutes = img_for_test.absolutes() + xy = img.find(img_for_test.from_matrix([[red]]), outer=absolutes) + assert xy == (0, 0) diff --git a/tests/testresources/image/absolutes.png b/tests/testresources/image/absolutes.png new file mode 100644 index 0000000..94abdf2 Binary files /dev/null and b/tests/testresources/image/absolutes.png differ