Skip to content

Commit

Permalink
img - first test
Browse files Browse the repository at this point in the history
  • Loading branch information
IGalat committed Sep 28, 2024
1 parent ee7b410 commit f2410d2
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 30 deletions.
39 changes: 27 additions & 12 deletions src/tapper/helper/_util/image_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
from typing import Union

import mss
import numpy
import numpy as np
import PIL.Image
import PIL.ImageGrab
import tapper
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+\)")
Expand All @@ -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(
Expand All @@ -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}")


Expand All @@ -66,15 +81,15 @@ 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:
raise e
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(
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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]
Expand Down
41 changes: 34 additions & 7 deletions src/tapper/helper/img.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down
19 changes: 9 additions & 10 deletions src/tapper/helper/model_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion tests/manual/tapper_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
22 changes: 22 additions & 0 deletions tests/tapper/helper/image/img_for_test.py
Original file line number Diff line number Diff line change
@@ -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")
15 changes: 15 additions & 0 deletions tests/tapper/helper/image/test_img.py
Original file line number Diff line number Diff line change
@@ -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)
Binary file added tests/testresources/image/absolutes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit f2410d2

Please sign in to comment.