Skip to content

Commit

Permalink
img - more tests, refactors, fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
IGalat committed Sep 28, 2024
1 parent 7a1cdbd commit 95041c9
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 37 deletions.
43 changes: 37 additions & 6 deletions src/tapper/helper/_util/image_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ def from_path(pathlike: ImagePathT) -> ImagePixelMatrixT:
return np.asarray(pil_img)


def get_image_size(image: ImagePixelMatrixT) -> tuple[int, int]:
return image.shape[1], image.shape[0]


def to_pixel_matrix(image: ImageT | None) -> ImagePixelMatrixT | None:
if image is None:
return None
Expand Down Expand Up @@ -105,7 +109,31 @@ def find_in_image_raw(
return confidence, (x_start + coords[0], y_start + coords[1])


def image_find(
def check_bbox_smaller_or_eq(image: ImagePixelMatrixT, 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, 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 find(
target: ImageT,
bbox: tuple[int, int, int, int] | None,
outer: ImageT | None = None,
Expand All @@ -115,15 +143,18 @@ def image_find(
raise ValueError("image_find nees something to search for.")
target_image = to_pixel_matrix(target)
assert target_image is not None # for mypy
check_bbox_bigger_or_eq(target_image, bbox)

outer_image = to_pixel_matrix(outer)
# has to be before screenshot is taken, for Windows multi-monitor case
x_start, y_start = get_start_coords(outer_image, bbox)
certain_outer = get_screenshot_if_none_and_cut(outer_image, bbox)
confidence, coords = image_fuzz.find(certain_outer, target_image)
check_bbox_smaller_or_eq(outer_image, bbox)
outer_certain = get_screenshot_if_none_and_cut(outer_image, bbox)

confidence, coords = image_fuzz.find(outer_certain, target_image)
if confidence < precision:
return None
return x_start + coords[0], y_start + coords[1]
target_x, target_y = get_image_size(target_image)
x_start, y_start = get_start_coords(outer_image, bbox)
return x_start + coords[0] + target_x // 2, y_start + coords[1] + target_y // 2


def find_in_image(
Expand Down
15 changes: 4 additions & 11 deletions src/tapper/helper/img.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,7 @@ def find(
If image not found, None is returned.
"""
_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
)
return _image_util.find(target, bbox, outer, precision=precision) # type: ignore


def find_one_of(
Expand Down Expand Up @@ -201,6 +195,7 @@ def get_find_raw(
return _image_util.find_in_image_raw(_image_util.normalize(image), _image_util.normalize(outer)[0]) # type: ignore


# todo add param bool to overwrite existing on save to disk / add (2) etc
def snip(
prefix: str | None = "snip",
bbox_to_name: bool = True,
Expand Down Expand Up @@ -298,10 +293,8 @@ def pixel_str(
return lambda: callback(_image_util.pixel_str(tapper.mouse.get_pos(), outer))


# todo method to get color #FFFFFF?
def pixel_get_color(
coords: XyCoordsT, outer: str | ImagePixelMatrixT | None = None
) -> PixelColorT:
# todo method to get color #FFFFFF? or remove this, and use format on pixel_str
def pixel_get_color(coords: XyCoordsT, outer: ImageT | None = None) -> PixelColorT:
"""
Get pixel color.
Expand Down
File renamed without changes
Binary file added tests/resources/image/btn_blue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/resources/image/btn_blue_changed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/resources/image/btn_pink.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/resources/image/btn_pink.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/resources/image/btn_red.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/resources/image/btn_red_less_bright.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/resources/image/btn_yellow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,23 @@ def get_picture(name: str) -> ndarray:

def absolutes() -> ndarray:
return get_picture("absolutes.png")


def btn_all() -> ndarray:
return get_picture("btn_all.png")


def btn_red() -> ndarray:
return get_picture("btn_red.png")


def btn_yellow() -> ndarray:
return get_picture("btn_yellow.png")


def btn_blue() -> ndarray:
return get_picture("btn_blue.png")


def btn_pink() -> ndarray:
return get_picture("btn_pink.png")
84 changes: 66 additions & 18 deletions tests/tapper/helper/image/test_img.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,120 @@
import img_for_test
from unittest.mock import patch

import img_test_util
import pytest
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
gray = 128, 128, 128

absolutes = img_for_test.absolutes()
absolutes = img_test_util.absolutes()

btn_red_xy = 60, 60
btn_yellow_xy = 310, 60
btn_blue_xy = 310, 180
btn_pink_xy = 310, 305


class TestFind:
def test_simplest(self) -> None:
xy = img.find(img_for_test.from_matrix([[blue]]), outer=absolutes)
xy = img.find(img_test_util.from_matrix([[blue]]), outer=absolutes)
assert xy == (2, 0)

def test_target_is_path(self) -> None:
pic_path = img_for_test.get_image_path("absolutes.png")
xy = img.find(img_for_test.from_matrix([[green]]), outer=pic_path)
pic_path = img_test_util.get_image_path("absolutes.png")
xy = img.find(img_test_util.from_matrix([[green]]), outer=pic_path)
assert xy == (1, 0)

def test_corner_case_black_not_found(self) -> None:
"""This is technically a bug of tapper, but caused by openCV
"""This is technically a bug in tapper, but caused by openCV
algorithm that's searching. All-black picture will not be found.
This is corner case where all target pixels are same, in which case
user should do pixel_find instead."""
xy = img.find(img_for_test.from_matrix([[black]]), outer=absolutes)
xy = img.find(img_test_util.from_matrix([[black]]), outer=absolutes)
assert xy is None

def test_corner_case_gray_is_white(self) -> None:
"""This is technically a bug of tapper, but caused by openCV
"""This is technically a bug in tapper, but caused by openCV
algorithm that's searching. Picture of gray pixel is found
on white pixel, with 100% match.
This is corner case where all target pixels are same, in which case
user should do pixel_find instead."""
xy = img.find(img_for_test.from_matrix([[gray]]), outer=absolutes)
xy = img.find(img_test_util.from_matrix([[gray]]), outer=absolutes)
assert xy == (0, 4)

def test_dependencies_not_installed(self) -> None:
pass

def test_with_bbox(self) -> None:
xy = img.find(img_for_test.from_matrix([[white]]), (2, 2, 3, 5), absolutes)
xy = img.find(img_test_util.from_matrix([[white]]), (2, 2, 3, 5), absolutes)
assert xy == (2, 4)

def test_not_found(self) -> None:
xy = img.find(img_for_test.from_matrix([[(100, 150, 50)]]), outer=absolutes)
xy = img.find(img_test_util.from_matrix([[(100, 150, 50)]]), outer=absolutes)
assert xy is None

def test_bbox_larger_than_outer(self) -> None:
# with pytest.raises(ValueError):
# img.find(img_for_test.from_matrix([[green]]), (0, 0, 100, 100), absolutes)
pass
with pytest.raises(ValueError):
img.find(img_test_util.from_matrix([[green]]), (0, 0, 5, 3), absolutes)

def test_bbox_smaller_than_target(self) -> None:
with pytest.raises(ValueError):
img.find(absolutes, (0, 0, 2, 2), absolutes)

def test_target_larger_than_outer(self) -> None:
pass
with pytest.raises(ValueError):
img.find(absolutes, (0, 0, 1, 1), img_test_util.from_matrix([[black]]))

def test_screenshot_not_outer(self) -> None:
pass
"""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"
) as mock_get_sct:
mock_get_sct.return_value = sct
xy = img.find(img_test_util.btn_red(), precision=0.999)
assert xy == pytest.approx(btn_red_xy, abs=10)

def test_target_wrong_type(self) -> None:
with pytest.raises(TypeError):
img.find(1) # noqa


class TestFindFuzz:
def test_precise_find(self) -> None:
xy = img.find(
img_test_util.btn_yellow(), outer=img_test_util.btn_all(), precision=0.999
)
assert xy == pytest.approx(btn_yellow_xy, abs=10)

def test_precise_not_found__approximate_found(self) -> None:
pass
target = img_test_util.get_picture("btn_red_less_bright.png")
xy = img.find(target, outer=img_test_util.btn_all(), precision=0.999)
assert xy is None
xy = img.find(target, outer=img_test_util.btn_all(), precision=0.95)
assert xy == pytest.approx(btn_red_xy, abs=10)

def test_several_similar_targets(self) -> None:
pass
target = img_test_util.get_picture("btn_blue_changed.png")
xy = img.find(target, outer=img_test_util.btn_all(), precision=0.95)
assert xy == pytest.approx(btn_blue_xy, abs=10)
xy = img.find( # exclude third column with actual blue button
target, bbox=(0, 0, 250, 361), outer=img_test_util.btn_all(), precision=0.95
)
assert xy is not None

def test_jpg(self) -> None:
target_jpg = img_test_util.get_picture("btn_pink.jpg")
xy_jpg = img.find(target_jpg, outer=img_test_util.btn_all(), precision=0.98)
xy_png = img.find(
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:
Expand Down
12 changes: 10 additions & 2 deletions tests/tapper/helper/image/test_pixel.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import img_for_test
import img_test_util
from tapper.helper import img

red = 255, 0, 0
Expand All @@ -8,7 +8,7 @@
white = 255, 255, 255
gray = 128, 128, 128

absolutes = img_for_test.absolutes()
absolutes = img_test_util.absolutes()


class TestFind:
Expand All @@ -24,3 +24,11 @@ def test_precise_not_found__approximate_found(self) -> None:

def test_pixel_color_out_of_bounds(self) -> None:
pass


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

def test_hexagonal(self) -> None:
pass

0 comments on commit 95041c9

Please sign in to comment.