From 95041c9ff6e256fcd5683a994538deb66f0f2810 Mon Sep 17 00:00:00 2001 From: Illia Halatiuk Date: Sat, 28 Sep 2024 20:59:41 +0700 Subject: [PATCH] img - more tests, refactors, fixes --- src/tapper/helper/_util/image_util.py | 43 +++++++-- src/tapper/helper/img.py | 15 +--- .../image/{test-buttons.png => btn_all.png} | Bin tests/resources/image/btn_blue.png | Bin 0 -> 5174 bytes tests/resources/image/btn_blue_changed.png | Bin 0 -> 4492 bytes tests/resources/image/btn_pink.jpg | Bin 0 -> 1867 bytes tests/resources/image/btn_pink.png | Bin 0 -> 4344 bytes tests/resources/image/btn_red.png | Bin 0 -> 5193 bytes tests/resources/image/btn_red_less_bright.png | Bin 0 -> 4138 bytes tests/resources/image/btn_yellow.png | Bin 0 -> 4465 bytes .../{img_for_test.py => img_test_util.py} | 20 +++++ tests/tapper/helper/image/test_img.py | 84 ++++++++++++++---- tests/tapper/helper/image/test_pixel.py | 12 ++- 13 files changed, 137 insertions(+), 37 deletions(-) rename tests/resources/image/{test-buttons.png => btn_all.png} (100%) create mode 100644 tests/resources/image/btn_blue.png create mode 100644 tests/resources/image/btn_blue_changed.png create mode 100644 tests/resources/image/btn_pink.jpg create mode 100644 tests/resources/image/btn_pink.png create mode 100644 tests/resources/image/btn_red.png create mode 100644 tests/resources/image/btn_red_less_bright.png create mode 100644 tests/resources/image/btn_yellow.png rename tests/tapper/helper/image/{img_for_test.py => img_test_util.py} (64%) diff --git a/src/tapper/helper/_util/image_util.py b/src/tapper/helper/_util/image_util.py index 749240a..21c86e3 100644 --- a/src/tapper/helper/_util/image_util.py +++ b/src/tapper/helper/_util/image_util.py @@ -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 @@ -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, @@ -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( diff --git a/src/tapper/helper/img.py b/src/tapper/helper/img.py index 7eabf50..ac2ddd0 100644 --- a/src/tapper/helper/img.py +++ b/src/tapper/helper/img.py @@ -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( @@ -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, @@ -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. diff --git a/tests/resources/image/test-buttons.png b/tests/resources/image/btn_all.png similarity index 100% rename from tests/resources/image/test-buttons.png rename to tests/resources/image/btn_all.png diff --git a/tests/resources/image/btn_blue.png b/tests/resources/image/btn_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..f3fbaf6de4504b50dc029372066dfc20eca5bea0 GIT binary patch literal 5174 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 literal 0 HcmV?d00001 diff --git a/tests/resources/image/btn_blue_changed.png b/tests/resources/image/btn_blue_changed.png new file mode 100644 index 0000000000000000000000000000000000000000..7ab3442b4804d4fc43c620d6e66909206c9b685f GIT binary patch literal 4492 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 literal 0 HcmV?d00001 diff --git a/tests/resources/image/btn_yellow.png b/tests/resources/image/btn_yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..5ae43e729f757ed64a1aaa822f86e03a73bba6c9 GIT binary patch literal 4465 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 DzVnvq literal 0 HcmV?d00001 diff --git a/tests/tapper/helper/image/img_for_test.py b/tests/tapper/helper/image/img_test_util.py similarity index 64% rename from tests/tapper/helper/image/img_for_test.py rename to tests/tapper/helper/image/img_test_util.py index 99581a8..16d2d78 100644 --- a/tests/tapper/helper/image/img_for_test.py +++ b/tests/tapper/helper/image/img_test_util.py @@ -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") diff --git a/tests/tapper/helper/image/test_img.py b/tests/tapper/helper/image/test_img.py index b23c3b7..7d81494 100644 --- a/tests/tapper/helper/image/test_img.py +++ b/tests/tapper/helper/image/test_img.py @@ -1,6 +1,10 @@ -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 @@ -8,65 +12,109 @@ 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: diff --git a/tests/tapper/helper/image/test_pixel.py b/tests/tapper/helper/image/test_pixel.py index f3f8148..11e541b 100644 --- a/tests/tapper/helper/image/test_pixel.py +++ b/tests/tapper/helper/image/test_pixel.py @@ -1,4 +1,4 @@ -import img_for_test +import img_test_util from tapper.helper import img red = 255, 0, 0 @@ -8,7 +8,7 @@ white = 255, 255, 255 gray = 128, 128, 128 -absolutes = img_for_test.absolutes() +absolutes = img_test_util.absolutes() class TestFind: @@ -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