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 0000000..f3fbaf6 Binary files /dev/null and b/tests/resources/image/btn_blue.png differ diff --git a/tests/resources/image/btn_blue_changed.png b/tests/resources/image/btn_blue_changed.png new file mode 100644 index 0000000..7ab3442 Binary files /dev/null and b/tests/resources/image/btn_blue_changed.png differ diff --git a/tests/resources/image/btn_pink.jpg b/tests/resources/image/btn_pink.jpg new file mode 100644 index 0000000..6f22c78 Binary files /dev/null and b/tests/resources/image/btn_pink.jpg differ diff --git a/tests/resources/image/btn_pink.png b/tests/resources/image/btn_pink.png new file mode 100644 index 0000000..3fd6f17 Binary files /dev/null and b/tests/resources/image/btn_pink.png differ diff --git a/tests/resources/image/btn_red.png b/tests/resources/image/btn_red.png new file mode 100644 index 0000000..fc6a7d3 Binary files /dev/null and b/tests/resources/image/btn_red.png differ diff --git a/tests/resources/image/btn_red_less_bright.png b/tests/resources/image/btn_red_less_bright.png new file mode 100644 index 0000000..1d10345 Binary files /dev/null and b/tests/resources/image/btn_red_less_bright.png differ diff --git a/tests/resources/image/btn_yellow.png b/tests/resources/image/btn_yellow.png new file mode 100644 index 0000000..5ae43e7 Binary files /dev/null and b/tests/resources/image/btn_yellow.png differ 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