Skip to content

Commit

Permalink
refactor util usage and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
IGalat committed Sep 27, 2024
1 parent a8388e1 commit ee7b410
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 58 deletions.
22 changes: 11 additions & 11 deletions src/tapper/helper/_util/image_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def from_path(pathlike: str) -> ndarray:
return numpy.asarray(pil_img)


def _normalize(
def normalize(
data_in: Union[
None,
str,
Expand Down Expand Up @@ -77,7 +77,7 @@ def get_screenshot_if_none_and_cut(
return numpy.asarray(pil_rgb)


def _find_in_image_raw(
def find_in_image_raw(
inner_image_bbox: tuple[ndarray, tuple[int, int, int, int] | None],
outer: ndarray | None = None,
) -> tuple[float, tuple[int, int]]:
Expand All @@ -89,7 +89,7 @@ def _find_in_image_raw(
return confidence, (x_start + coords[0], y_start + coords[1])


def _find_in_image(
def find_in_image(
inner_image_bbox: tuple[ndarray, tuple[int, int, int, int] | None],
outer: ndarray | None = None,
precision: float = 1.0,
Expand Down Expand Up @@ -130,7 +130,7 @@ def win32_coords_start() -> tuple[int, int]:
snip_start_coords: tuple[int, int] | None = None


def _toggle_snip(
def toggle_snip(
prefix: str | None = None,
bbox_to_name: bool = True,
bbox_callback: Callable[[int, int, int, int], Any] | None = None,
Expand Down Expand Up @@ -163,14 +163,14 @@ def finish_snip_with_callback(
bbox_callback: Callable[[int, int, int, int], Any] | None = None,
picture_callback: Callable[[ndarray], Any] | None = None,
) -> None:
nd_sct, bbox = _finish_snip(prefix, bbox, bbox_to_name)
nd_sct, bbox = finish_snip(prefix, bbox, bbox_to_name)
if bbox and bbox_callback:
bbox_callback(*bbox)
if picture_callback:
picture_callback(nd_sct)


def _finish_snip(
def finish_snip(
prefix: str | None = None,
bbox: tuple[int, int, int, int] | None = None,
bbox_to_name: bool = True,
Expand Down Expand Up @@ -213,18 +213,18 @@ def save_to_disk(
)


def _get_pixel_color(
def get_pixel_color(
coords: tuple[int, int], outer: str | ndarray | None
) -> tuple[int, int, int]:
outer, _ = _normalize(outer) # type: ignore
outer, _ = normalize(outer) # type: ignore
bbox = coords_to_bbox_1_pixel(coords)
outer = get_screenshot_if_none_and_cut(outer, bbox)
nd_pixel = outer[0][0]
return tuple(c for c in nd_pixel) # type: ignore


def _pixel_str(coords: tuple[int, int], outer: str | ndarray | None) -> str:
color = _get_pixel_color(coords, outer)
def pixel_str(coords: tuple[int, int], outer: str | ndarray | None) -> str:
color = get_pixel_color(coords, outer)
return f"({color[0]}, {color[1]}, {color[2]}), ({coords[0]}, {coords[1]})"


Expand All @@ -243,7 +243,7 @@ def _pixel_str(coords: tuple[int, int], outer: str | ndarray | None) -> str:
)


def _pixel_find(
def pixel_find(
color: tuple[int, int, int],
bbox_or_coords: tuple[int, int, int, int] | tuple[int, int] | None,
outer: ndarray | None,
Expand Down
71 changes: 35 additions & 36 deletions src/tapper/helper/img.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import re
import time
from functools import partial
from typing import Any
Expand All @@ -7,43 +6,36 @@
from typing import Union

import tapper
from tapper.helper._util.image_util import _find_in_image
from tapper.helper._util.image_util import _find_in_image_raw
from tapper.helper._util.image_util import _finish_snip
from tapper.helper._util.image_util import _get_pixel_color
from tapper.helper._util.image_util import _normalize
from tapper.helper._util.image_util import _pixel_find
from tapper.helper._util.image_util import _pixel_str
from tapper.helper._util.image_util import _toggle_snip
from tapper.helper._util import image_util as _image_util
from tapper.helper.model_types import BboxT
from tapper.helper.model_types import ImagePixelMatrixT
from tapper.helper.model_types import PixelColorT
from tapper.helper.model_types import XyCoordsT

STD_PRECISION = 0.999

SearchableImageT = Union[
str,
tuple[str, BboxT],
tuple[ImagePixelMatrixT, BboxT],
ImagePixelMatrixT,
]
SearchableImageT = (
Union[ # todo remove and use ImageT in methods, with bbox as separate arg
str,
tuple[str, BboxT],
tuple[ImagePixelMatrixT, BboxT],
ImagePixelMatrixT,
]
)
"""
Image to be searched for. May be:
- numpy array: pimg = numpy.array(PIL.Image.open('Image.jpg')). RGB is expected.
- numpy array with bounding box
- file path, absolute or relative: pic_name = "my_button.png"
- file path with bounding box: (pic_name, (0, 0, 200, 50))
- file path with bounding box in the name: pic_name = "my_button(BBOX_100_213_156_412).png"
- file path with bounding box in the name. Example: "my_button(BBOX_100_213_156_412).png"
Pattern is: (BBOX_{int}_{int}_{int}_{int})
If bounding box is specified separately, BBOX in the name will be ignored.
Coordinates of bounding box may be negative on win32 with multiple screens.
If bounding box is not specified as tuple or in the name, the whole screen will be searched.
For performance, it's highly recommended to specify bounding box: searching smaller area is faster.
"""

_bbox_pattern = re.compile(r"\(BBOX_-?\d+_-?\d+_-?\d+_-?\d+\)")


def find(
image: SearchableImageT,
Expand All @@ -62,8 +54,8 @@ def find(
: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 = _normalize(outer)[0] if outer is not None else None # type: ignore
return _find_in_image(_normalize(image), norm_outer, precision=precision) # type: ignore
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


def find_one_of(
Expand All @@ -81,9 +73,9 @@ def find_one_of(
Will return object supplied in the list if it finds corresponding image.
In case many images are present, first found in the `images` list will be returned.
"""
normalized = [_normalize(image) for image in images] # type: ignore
normalized = [_image_util.normalize(image) for image in images] # type: ignore
for i in range(len(normalized)):
if _find_in_image(normalized[i], outer, precision=precision): # type: ignore
if _image_util.find_in_image(normalized[i], outer, precision=precision): # type: ignore
return images[i]
return None

Expand All @@ -107,9 +99,9 @@ def wait_for(
:return: Coordinates X and Y of top-left of the found image relative to the bounding box (if any).
"""
finish_time = time.perf_counter() + timeout
normalized = _normalize(image) # type: ignore
normalized = _image_util.normalize(image) # type: ignore
while time.perf_counter() < finish_time:
if found := _find_in_image(normalized, precision=precision): # type: ignore
if found := _image_util.find_in_image(normalized, precision=precision): # type: ignore
return found
time.sleep(interval)
return None
Expand Down Expand Up @@ -153,10 +145,10 @@ def wait_for_one_of(
raise ValueError
"""
finish_time = time.perf_counter() + timeout
normalized = [_normalize(image) for image in images] # type: ignore
normalized = [_image_util.normalize(image) for image in images] # type: ignore
while time.perf_counter() < finish_time:
for i in range(len(normalized)):
if _find_in_image(normalized[i], precision=precision): # type: ignore
if _image_util.find_in_image(normalized[i], precision=precision): # type: ignore
return images[i]
time.sleep(interval)
return None
Expand All @@ -175,7 +167,7 @@ def get_find_raw(
:return: Match precision, and coordinates.
"""

return _find_in_image_raw(_normalize(image), _normalize(outer)[0]) # type: ignore
return _image_util.find_in_image_raw(_image_util.normalize(image), _image_util.normalize(outer)[0]) # type: ignore


def snip(
Expand Down Expand Up @@ -206,7 +198,9 @@ def snip(
Same procedure to get an image, but this will be called "image.png" without bounding box in the name,
instead it will be copied to your clipboard. Package pyperclip if required for this.
"""
return partial(_toggle_snip, prefix, bbox_to_name, bbox_callback, picture_callback)
return partial(
_image_util.toggle_snip, prefix, bbox_to_name, bbox_callback, picture_callback
)


def get_snip(
Expand All @@ -222,16 +216,15 @@ def get_snip(
:param bbox: Bounding box of the screenshot or image. If None, the whole screen or image is snipped.
:param prefix: Optional name, may be a path of image to save, without extension. If not specified,
will not be saved to disk.
:param bbox_in_name: If true, will include in the name -(BBOX_{x1}_{y1}_{x2}_{y2}), with actual coordinates.
This is useful for precise-position search with `find` and `wait_for` methods.
:param bbox_in_name: If true, will append to the name -(BBOX_{x1}_{y1}_{x2}_{y2}), with corner coordinates.
:return: Resulting image RGB, transformed to numpy array.
Usage:
my_pic = img.snip_bbox((100, 100, 200, 400))
my_pic = img.get_snip(bbox=(100, 100, 200, 400))
...
img.wait_for(my_pic)
"""
return _finish_snip(prefix, bbox, bbox_in_name)[0]
return _image_util.finish_snip(prefix, bbox, bbox_in_name)[0]


def pixel_info(
Expand All @@ -253,6 +246,7 @@ def pixel_info(
)


# todo custom format? like format=r"({r}, {g}, {b}), ({x}, {y})"
def pixel_str(
callback: Callable[[str], Any], outer: str | ImagePixelMatrixT | None = None
) -> Callable[[], Any]:
Expand All @@ -270,7 +264,7 @@ def pixel_str(
... then press "a" to get pixel, and paste it into another script:
img.pixel_find((255, 255, 255), (1919, 1079))
"""
return lambda: callback(_pixel_str(tapper.mouse.get_pos(), outer))
return lambda: callback(_image_util.pixel_str(tapper.mouse.get_pos(), outer))


def pixel_get_color(
Expand All @@ -285,7 +279,7 @@ def pixel_get_color(
:param outer: Optional image, pathname or numpy array. If not specified, will get color from screen.
:return: Decimal values of Red, Green, and Blue components of the pixel color.
"""
return _get_pixel_color(coords, outer)
return _image_util.get_pixel_color(coords, outer)


def pixel_find(
Expand All @@ -301,13 +295,17 @@ def pixel_find(
:param bbox_or_coords: Bounding box of the screenshot or image. If None, the whole screen or image is snipped.
If coordinates are supplied, only one pixel at those coordinates will be checked.
:param variation: Allowed number of shades of variation in either direction for the intensity of the
red, green, and blue components, 0-255. For example, if 2 is specified and color is (10, 10, 10),
red, green, and blue components, 0-255.
For example, if 2 is specified and color is (10, 10, 10),
any color from (8, 8, 8) to (12, 12, 12) will be considered a match.
This parameter is helpful if the color sought is not always exactly the same shade.
If you specify 255 shades of variation, all colors will match.
:return: Coordinates X and Y of the first pixel that matches, or None if no match.
"""
return _pixel_find(color, bbox_or_coords, _normalize(outer)[0], variation)
return _image_util.pixel_find(
color, bbox_or_coords, _image_util.normalize(outer)[0], variation
)


def pixel_wait_for(
Expand Down Expand Up @@ -358,6 +356,7 @@ def pixel_wait_for_one_of(
:param variation: see `pixel_find` param.
:return: tuple(color, coords) that was found, else None.
"""
# Todo rework: colors_coords now: (color, coords | None), to: color | (color, coords)
finish_time = time.perf_counter() + timeout
while True:
for _, color_coords in enumerate(colors_coords):
Expand Down
15 changes: 14 additions & 1 deletion src/tapper/helper/model_types.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import os
from typing import TypeAlias
from typing import Union

from numpy import ndarray

PixelColorT = tuple[int, int, int]
Expand All @@ -6,7 +10,7 @@
XyCoordsT = tuple[int, int]
""" x, y coordinates on an image or screen."""

ImagePixelMatrixT = ndarray
ImagePixelMatrixT: TypeAlias = ndarray
"""List of lists of pixel colors.
2x2 green pixels:
Expand All @@ -17,3 +21,12 @@
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]"]

ImageT = Union[ImagePixelMatrixT, ImagePathT]
""" Can be:
- Image as numpy RGB array.
- str or bytes path to an image.
"""
11 changes: 6 additions & 5 deletions src/tapper/helper/recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
from typing import Callable

from tapper import send
from tapper.helper._util import record_util
from tapper.helper._util import record_util as _record_util
from tapper.helper.model import RecordConfig


last_recorded: str = ""


Expand All @@ -32,7 +31,7 @@ def start() -> Callable[[], None]:
:return: callable start, to be set into a Tap
"""
return record_util.start_recording
return _record_util.start_recording


def stop(
Expand All @@ -48,7 +47,7 @@ def stop(
:param config: config settings.
:return: callable stop, to be set into a Tap.
"""
return partial(record_util.stop_recording, [_set_new_recording, callback], config)
return partial(_record_util.stop_recording, [_set_new_recording, callback], config)


def toggle(
Expand All @@ -65,4 +64,6 @@ def toggle(
:param config: config settings.
:return: callable toggle, to be set into a Tap.
"""
return partial(record_util.toggle_recording, [_set_new_recording, callback], config)
return partial(
_record_util.toggle_recording, [_set_new_recording, callback], config
)
10 changes: 5 additions & 5 deletions src/tapper/helper/repeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Any
from typing import Callable

from tapper.helper._util import repeat_util
from tapper.helper._util import repeat_util as _repeat_util
from tapper.helper.model import Repeatable


Expand All @@ -26,7 +26,7 @@ def while_fn(
:return: callable toggle, to be set into a Tap.
"""
return partial(
repeat_util.run_action,
_repeat_util.run_action,
Repeatable(
condition=condition,
action=action,
Expand All @@ -50,9 +50,9 @@ def while_pressed(
See :func:`while_fn` for other docs.
"""
return partial(
repeat_util.run_action,
_repeat_util.run_action,
Repeatable(
condition=repeat_util.to_pressed_condition(symbol),
condition=_repeat_util.to_pressed_condition(symbol),
action=action,
interval=interval,
max_repeats=max_repeats,
Expand All @@ -74,7 +74,7 @@ def toggle(
See :func:`while_fn` for docs.
"""
return partial(
repeat_util.toggle_run,
_repeat_util.toggle_run,
Repeatable(
condition=condition if condition else lambda: True,
action=action,
Expand Down
Loading

0 comments on commit ee7b410

Please sign in to comment.