Skip to content

Commit

Permalink
img - refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
IGalat committed Sep 28, 2024
1 parent 264cc52 commit 1077a01
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 12 deletions.
162 changes: 162 additions & 0 deletions src/tapper/helper/_util/image/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import os.path
import re
import sys
from functools import lru_cache
from typing import Any
from typing import Callable
from typing import Union

import mss
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 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
from tapper.model import constants


def find_fuzzy_cv2(target: ndarray, outer: ndarray) -> tuple[float, XyCoordsT]:
import cv2

comparison = cv2.matchTemplate(outer, target, cv2.TM_CCORR_NORMED)
_, max_val, _, max_loc = cv2.minMaxLoc(comparison)
return max_val, max_loc # type: ignore


mss_instance: MSSBase


def get_mss() -> MSSBase:
global mss_instance
mss_instance = mss.mss()
return mss_instance


@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 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 get_image_size(image: ImagePixelMatrixT) -> tuple[int, int]:
return image.shape[1], image.shape[0]


def get_screenshot_if_none_and_cut(
maybe_image: ImagePixelMatrixT | None, bbox: BboxT | None
) -> ImagePixelMatrixT:
if maybe_image is not None:
if bbox:
return maybe_image[bbox[1] : bbox[3], bbox[0] : bbox[2]]
return maybe_image
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 np.asarray(pil_rgb)


def check_bbox_smaller_or_eq(
image: ImagePixelMatrixT | None, bbox: BboxT | None
) -> None:
if image is None or bbox is None:
return
bbox_x = abs(bbox[2] - bbox[0])
bbox_y = abs(bbox[3] - bbox[1])
image_x, image_y = get_image_size(image)
if bbox_x > image_x or bbox_y > image_y:
raise ValueError(
f"Bbox should NOT be bigger, but got {bbox_x}x{bbox_y} vs image {image_x}x{image_y}"
)


def check_bbox_bigger_or_eq(
image: ImagePixelMatrixT | None, bbox: BboxT | None
) -> None:
if image is None or bbox is None:
return
bbox_x = abs(bbox[2] - bbox[0])
bbox_y = abs(bbox[3] - bbox[1])
image_x, image_y = get_image_size(image)
if bbox_x < image_x or bbox_y < image_y:
raise ValueError(
f"Bbox should NOT be smaller, but got {bbox_x}x{bbox_y} vs image {image_x}x{image_y}"
)


def get_start_coords(
outer: ndarray | None,
bbox_or_coords: BboxT | XyCoordsT | None,
) -> XyCoordsT:
if bbox_or_coords is not None:
return bbox_or_coords[0], bbox_or_coords[1]
if outer is None and sys.platform == constants.OS.win32:
return win32_coords_start()
return 0, 0


def win32_coords_start() -> XyCoordsT:
"""Win32 may start with negative coords when multiscreen."""
import winput
from win32api import GetSystemMetrics

winput.set_DPI_aware(per_monitor=True)
x = GetSystemMetrics(76)
y = GetSystemMetrics(77)
return x, y


def target_to_image(target: ImageT, bbox: BboxT | None) -> ImagePixelMatrixT:
"""Transform and verify target from API input to workable image-array."""
target_image = to_pixel_matrix(target)
assert target_image is not None # for mypy
if bbox is not None:
bbox_x = abs(bbox[2] - bbox[0])
bbox_y = abs(bbox[3] - bbox[1])
image_x, image_y = get_image_size(target_image)
if bbox_x < image_x or bbox_y < image_y:
raise ValueError(
f"Bbox should NOT be smaller than target, "
f"but got {bbox_x}x{bbox_y} vs target {image_x}x{image_y}"
)
return target_image


def outer_to_image(outer_or_path_maybe: ImageT | None, bbox: BboxT | None) -> ImagePixelMatrixT:
"""Transform and verify target from API input (or screenshot) to workable image-array."""
outer_maybe = to_pixel_matrix(outer_or_path_maybe)
if outer_maybe is not None and bbox is not None:
bbox_x = abs(bbox[2] - bbox[0])
bbox_y = abs(bbox[3] - bbox[1])
image_x, image_y = get_image_size(outer_maybe)
if bbox_x > image_x or bbox_y > image_y:
raise ValueError(
f"Bbox should NOT be bigger than outer, "
f"but got {bbox_x}x{bbox_y} vs outer {image_x}x{image_y}"
)
return get_screenshot_if_none_and_cut(outer_maybe, bbox)
87 changes: 87 additions & 0 deletions src/tapper/helper/_util/image/find_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import os.path
import re
import sys
from functools import lru_cache
from lib2to3.pytree import convert
from sys import prefix
from typing import Any
from typing import Callable
from typing import Union

import mss
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.image import base
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
from tapper.model import constants


def find_raw(
target: ImagePixelMatrixT,
outer: ImagePixelMatrixT,
) -> tuple[float, XyCoordsT]:
"""Doesn't account for the starting coords of the search."""
confidence, coords = base.find_fuzzy_cv2(target, outer)
target_size_x, target_size_y = base.get_image_size(target)
return confidence, (coords[0] + target_size_x // 2, coords[1] + target_size_y // 2)


def api_find_raw(
image: ImagePixelMatrixT,
bbox: BboxT | None,
outer_maybe: ImagePixelMatrixT | None = None,
) -> tuple[float, XyCoordsT]:
x_start, y_start = base.get_start_coords(outer_maybe, bbox)
outer = base.outer_to_image(outer_maybe, bbox)
confidence, xy = find_raw(image, outer)
return confidence, (x_start + xy[0], y_start + xy[1])


def find(
target: ImageT,
outer: ImageT,
precision: float,
) -> XyCoordsT | None:
"""Doesn't account for the starting coords of the search."""
confidence, xy = find_raw(target, outer)
if confidence < precision:
return None
return xy


def api_find(
target: ImageT,
bbox: BboxT | None,
outer_or_path_maybe: ImageT | None = None,
precision: float = 1.0,
) -> XyCoordsT | None:
if target is None:
raise ValueError("image_find nees something to search for.")
target_image = base.target_to_image(target, bbox)
outer = base.outer_to_image(outer_or_path_maybe, bbox)
found = find(target_image, outer, precision)
if found is None:
return None
x_start, y_start = base.get_start_coords(outer_or_path_maybe, bbox)
return x_start + found[0], y_start + found[1]


def find_one_of() -> None:
pass


def wait_for() -> None:
pass


def wait_for_one_of() -> None:
pass
11 changes: 8 additions & 3 deletions src/tapper/helper/img.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import tapper
from tapper.helper._util import image_util as _image_util
from tapper.helper._util.image import base as _base_util
from tapper.helper._util.image import find_util as _find_util
from tapper.helper.model_types import BboxT
from tapper.helper.model_types import ImagePathT
from tapper.helper.model_types import ImagePixelMatrixT
Expand Down Expand Up @@ -41,7 +43,10 @@

def _check_dependencies() -> None:
try:
pass
import PIL
import mss
import cv2
import numpy
except ImportError as e:
raise ImportError(
"Looks like you're missing dependencies for tapper img helper."
Expand All @@ -53,7 +58,7 @@ def _check_dependencies() -> None:
def from_path(pathlike: ImagePathT) -> ImagePixelMatrixT:
"""Get image from file path."""
_check_dependencies()
return _image_util.from_path(pathlike) # type: ignore
return _base_util.from_path(pathlike) # type: ignore


# todo return middle of searched area, not top left
Expand All @@ -78,7 +83,7 @@ def find(
If image not found, None is returned.
"""
_check_dependencies()
return _image_util.find(target, bbox, outer, precision=precision) # type: ignore
return _find_util.api_find(target, bbox, outer, precision=precision) # type: ignore


def find_one_of(
Expand Down
10 changes: 1 addition & 9 deletions tests/tapper/helper/image/test_img.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def test_screenshot_not_outer(self) -> None:
"""Touches internals to simulate taking screenshot."""
sct = img_test_util.btn_all()
with patch(
"tapper.helper._util.image_util" ".get_screenshot_if_none_and_cut"
"tapper.helper._util.image.base.get_screenshot_if_none_and_cut"
) as mock_get_sct:
mock_get_sct.return_value = sct
xy = img.find(img_test_util.btn_red(), precision=0.999)
Expand Down Expand Up @@ -115,11 +115,3 @@ def test_jpg(self) -> None:
img_test_util.btn_pink(), outer=img_test_util.btn_all(), precision=0.98
)
assert xy_jpg == xy_png == pytest.approx(btn_pink_xy, abs=10)


class TestSnip:
def test_simplest(self) -> None:
pass

def test_saved_image_same_as_on_disk(self) -> None:
pass
7 changes: 7 additions & 0 deletions tests/tapper/helper/image/test_snip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

class TestSnip:
def test_simplest(self) -> None:
pass

def test_saved_image_same_as_on_disk(self) -> None:
pass

0 comments on commit 1077a01

Please sign in to comment.