diff --git a/pyproject.toml b/pyproject.toml index 334984c1..fc51a934 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,18 +9,12 @@ dependencies = [ "PyWinCtl >=0.0.42", # py.typed "keyboard @ git+https://github.com/boppreh/keyboard.git", # Fix install on macos and linux-ci https://github.com/boppreh/keyboard/pull/568 "numpy >=2.3", # Windows ARM64 wheels + "opencv-contrib-python-headless >=4.10", # NumPy 2 support "packaging >=20.0", # py.typed # When needed, dev builds can be found at https://download.qt.io/snapshots/ci/pyside/dev?C=M;O=D "PySide6-Essentials", # Let package resolution find the minimum for wheels (>=6.9.0 on Windows ARM64; <6.8.1 on ubuntu-22.04-arm (glibc 2.39)) "tomli-w >=1.1.0", # Typing fixes - # scipy is used for pHash calculation as a smaller, but still fast implementation. - # However, scipy is not available on all environments yet. - # In those cases, we're falling back to opencv-contrib-python's cv2.img_hash - "opencv-contrib-python-headless; platform_machine == 'ARM64' or platform_machine == 'aarch64'", - "opencv-python-headless; platform_machine != 'ARM64' and platform_machine != 'aarch64'", - "scipy >=1.14.1; platform_machine != 'ARM64' and platform_machine != 'aarch64'", # Python 3.13 support - # # Build and compile resources "pyinstaller >=6.14.0", # Mitigate issues with pkg_resources deprecation warning @@ -59,7 +53,6 @@ dev = [ "ruff >=0.11.13", # # Types - "scipy-stubs >=1.14.1.1", "types-PyAutoGUI", "types-PyScreeze; sys_platform == 'linux'", "types-keyboard", diff --git a/src/compare.py b/src/compare.py index e414a1b3..400fea2a 100644 --- a/src/compare.py +++ b/src/compare.py @@ -1,6 +1,5 @@ from collections.abc import Iterable from math import sqrt -from typing import TYPE_CHECKING import cv2 import Levenshtein @@ -22,6 +21,7 @@ RANGES = (0, MAXRANGE, 0, MAXRANGE, 0, MAXRANGE) MASK_SIZE_MULTIPLIER = ColorChannel.Alpha * MAXBYTE * MAXBYTE MAX_VALUE = 1.0 +CV2_PHASH_SIZE = 8 def compare_histograms(source: MatLike, capture: MatLike, mask: MatLike | None = None): @@ -90,41 +90,39 @@ def compare_template(source: MatLike, capture: MatLike, mask: MatLike | None = N return 1 - (min_val / max_error) -try: - from scipy import fft - - def __cv2_scipy_compute_phash(image: MatLike, hash_size: int, highfreq_factor: int = 4): - """Implementation copied from https://github.com/JohannesBuchner/imagehash/blob/38005924fe9be17cfed145bbc6d83b09ef8be025/imagehash/__init__.py#L260 .""" # noqa: E501 - img_size = hash_size * highfreq_factor - image = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY) - image = cv2.resize(image, (img_size, img_size), interpolation=cv2.INTER_AREA) - dct = fft.dct(fft.dct(image, axis=0), axis=1) - dct_low_frequency = dct[:hash_size, :hash_size] - median = np.median(dct_low_frequency) - return dct_low_frequency > median - - def __cv2_phash(source: MatLike, capture: MatLike, hash_size: int = 8): - source_hash = __cv2_scipy_compute_phash(source, hash_size) - capture_hash = __cv2_scipy_compute_phash(capture, hash_size) - hash_diff = np.count_nonzero(source_hash != capture_hash) - return 1 - (hash_diff / 64.0) - -except ModuleNotFoundError: - if not TYPE_CHECKING: # opencv-contrib-python-headless being installed is based on architecture - - def __cv2_phash(source: MatLike, capture: MatLike, hash_size: int = 8): - # OpenCV has its own pHash comparison implementation in `cv2.img_hash`, - # but it requires contrib/extra modules and is inaccurate - # unless we precompute the size with a specific interpolation. - # See: https://github.com/opencv/opencv_contrib/issues/3295#issuecomment-1172878684 - # - phash = cv2.img_hash.PHash.create() - source = cv2.resize(source, (hash_size, hash_size), interpolation=cv2.INTER_AREA) - capture = cv2.resize(capture, (hash_size, hash_size), interpolation=cv2.INTER_AREA) - source_hash = phash.compute(source) - capture_hash = phash.compute(capture) - hash_diff = phash.compare(source_hash, capture_hash) - return 1 - (hash_diff / 64.0) +# The old scipy-based implementation. +# Turns out this cuases an extra 25 MB build compared to opencv-contrib-python-headless +# # from scipy import fft +# def __cv2_scipy_compute_phash(image: MatLike, hash_size: int, highfreq_factor: int = 4): +# """Implementation copied from https://github.com/JohannesBuchner/imagehash/blob/38005924fe9be17cfed145bbc6d83b09ef8be025/imagehash/__init__.py#L260 .""" # noqa: E501 +# img_size = hash_size * highfreq_factor +# image = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY) +# image = cv2.resize(image, (img_size, img_size), interpolation=cv2.INTER_AREA) +# dct = fft.dct(fft.dct(image, axis=0), axis=1) +# dct_low_frequency = dct[:hash_size, :hash_size] +# median = np.median(dct_low_frequency) +# return dct_low_frequency > median +# def __cv2_phash(source: MatLike, capture: MatLike, hash_size: int = 8): +# source_hash = __cv2_scipy_compute_phash(source, hash_size) +# capture_hash = __cv2_scipy_compute_phash(capture, hash_size) +# hash_diff = np.count_nonzero(source_hash != capture_hash) +# return 1 - (hash_diff / 64.0) + + +def __cv2_phash(source: MatLike, capture: MatLike): + """ + OpenCV has its own pHash comparison implementation in `cv2.img_hash`, + but is inaccurate unless we precompute the size with a specific interpolation. + + See: https://github.com/opencv/opencv_contrib/issues/3295#issuecomment-1172878684 + """ + phash = cv2.img_hash.PHash.create() + source = cv2.resize(source, (CV2_PHASH_SIZE, CV2_PHASH_SIZE), interpolation=cv2.INTER_AREA) + capture = cv2.resize(capture, (CV2_PHASH_SIZE, CV2_PHASH_SIZE), interpolation=cv2.INTER_AREA) + source_hash = phash.compute(source) + capture_hash = phash.compute(capture) + hash_diff = phash.compare(source_hash, capture_hash) + return 1 - (hash_diff / 64.0) def compare_phash(source: MatLike, capture: MatLike, mask: MatLike | None = None): diff --git a/uv.lock b/uv.lock index 28f28fe4..64d6df6f 100644 --- a/uv.lock +++ b/uv.lock @@ -36,8 +36,7 @@ dependencies = [ { name = "keyboard", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "levenshtein", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "opencv-contrib-python-headless", marker = "(platform_machine == 'ARM64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'ARM64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'win32')" }, - { name = "opencv-python-headless", marker = "(platform_machine != 'ARM64' and platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and platform_machine != 'aarch64' and sys_platform == 'win32')" }, + { name = "opencv-contrib-python-headless", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "packaging", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pillow", marker = "sys_platform == 'linux'" }, { name = "pyautogui", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -49,7 +48,6 @@ dependencies = [ { name = "python-xlib", marker = "sys_platform == 'linux'" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "pywinctl", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "scipy", marker = "(platform_machine != 'ARM64' and platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and platform_machine != 'aarch64' and sys_platform == 'win32')" }, { name = "tomli-w", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typed-d3dshot", extra = ["numpy"], marker = "sys_platform == 'win32'" }, { name = "winrt-windows-foundation", marker = "sys_platform == 'win32'" }, @@ -69,7 +67,6 @@ dev = [ { name = "pyright", extra = ["nodejs"], marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "qt6-applications", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "ruff", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "scipy-stubs", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "types-keyboard", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "types-pyautogui", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "types-pyinstaller", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -83,8 +80,7 @@ requires-dist = [ { name = "keyboard", git = "https://github.com/boppreh/keyboard.git" }, { name = "levenshtein", specifier = ">=0.25" }, { name = "numpy", specifier = ">=2.3" }, - { name = "opencv-contrib-python-headless", marker = "platform_machine == 'ARM64' or platform_machine == 'aarch64'" }, - { name = "opencv-python-headless", marker = "platform_machine != 'ARM64' and platform_machine != 'aarch64'" }, + { name = "opencv-contrib-python-headless", specifier = ">=4.10" }, { name = "packaging", specifier = ">=20.0" }, { name = "pillow", marker = "sys_platform == 'linux'", specifier = ">=11.0" }, { name = "pyautogui", specifier = ">=0.9.52" }, @@ -95,7 +91,6 @@ requires-dist = [ { name = "python-xlib", marker = "sys_platform == 'linux'", specifier = ">=0.33" }, { name = "pywin32", marker = "sys_platform == 'win32'", specifier = ">=307" }, { name = "pywinctl", specifier = ">=0.0.42" }, - { name = "scipy", marker = "platform_machine != 'ARM64' and platform_machine != 'aarch64'", specifier = ">=1.14.1" }, { name = "tomli-w", specifier = ">=1.1.0" }, { name = "typed-d3dshot", extras = ["numpy"], marker = "sys_platform == 'win32'", specifier = ">=1.0.1" }, { name = "winrt-windows-foundation", marker = "sys_platform == 'win32'", specifier = ">=2.2.0" }, @@ -115,7 +110,6 @@ dev = [ { name = "pyright", extras = ["nodejs"], specifier = ">=1.1.400" }, { name = "qt6-applications", specifier = ">=6.5.0" }, { name = "ruff", specifier = ">=0.11.13" }, - { name = "scipy-stubs", specifier = ">=1.14.1.1" }, { name = "types-keyboard" }, { name = "types-pyautogui" }, { name = "types-pyinstaller" }, @@ -271,30 +265,9 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/53/cc/295e9a4e783ca71ba1b8fbd34e51bc603eba4611afcfc7de1b09b2d6ed8d/opencv-contrib-python-headless-4.11.0.86.tar.gz", hash = "sha256:839319098a73264c580c97cb1ca835f7fce3d30e4fa9fa6d4d0618fff551be0b", size = 150579288, upload-time = "2025-01-16T13:54:11.763Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/ec/b3fb322e8bac7b797f98676c34599827920b3972e4d664bbdf8de84d7fca/opencv_contrib_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8dc2f4109904ffa55967bf9ceb1521ce46d66c333e2f6261dfa1f957a1dbde0", size = 35122073, upload-time = "2025-01-16T13:52:07.72Z" }, -] - -[[package]] -name = "opencv-python-headless" -version = "4.11.0.86" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload-time = "2025-01-16T13:53:40.22Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/be/1438ce43ebe65317344a87e4b150865c5585f4c0db880a34cdae5ac46881/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb", size = 29487060, upload-time = "2025-01-16T13:51:59.625Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5c/c139a7876099916879609372bfa513b7f1257f7f1a908b0bdc1c2328241b/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b", size = 49969856, upload-time = "2025-01-16T13:53:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/95/dd/ed1191c9dc91abcc9f752b499b7928aacabf10567bb2c2535944d848af18/opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b", size = 29324425, upload-time = "2025-01-16T13:52:49.048Z" }, - { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386, upload-time = "2025-01-16T13:52:56.418Z" }, -] - -[[package]] -name = "optype" -version = "0.9.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/32/c2/76a520809f1a5dd970eab9cc7da3aaa0e16e3fb8a0252fecec1a7a23ad73/optype-0.9.2.tar.gz", hash = "sha256:d7487a5b7fe96bacfc5dc8fab6b90c14f1e65e7ba6e7b22a8bef48d29690edac", size = 96126, upload-time = "2025-03-12T22:15:03.571Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/1a/40593f6fb9432fea750c9833e0cff0cd71452b9711b9df83faa251b0fe4e/optype-0.9.2-py3-none-any.whl", hash = "sha256:f9aee29e24794a7af637af80347b9fefbb110dffe012c133079615e551e52ef9", size = 84314, upload-time = "2025-03-12T22:15:01.878Z" }, + { url = "https://files.pythonhosted.org/packages/7a/80/26c4ad9459498fc9213dea7254c8d6cb7717b279306b070588a2781559d4/opencv_contrib_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e7a86bf02e8157a2d9fce26d44eaafd31113fc21fc8b4f44ff56c28ab32fdba", size = 56078660, upload-time = "2025-01-16T13:53:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/c0/38/2ce4259eca6ca356e3757b596d7d583b4ab0be4a482c9f4dacaa3eb688d1/opencv_contrib_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:d2c10564c01f6c308ded345a3b37359714e694361e593e515c148465eda09c2a", size = 35092082, upload-time = "2025-01-16T13:53:06.296Z" }, + { url = "https://files.pythonhosted.org/packages/49/c1/c7600136283a2d4d3327968bdd895ba917a033d5a5498b6c7ffcd78c772c/opencv_contrib_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:2671a828e5c8ec9d237dd8506a9e0268487d37e07625725f1a6de5fa973ea7fa", size = 46095689, upload-time = "2025-01-16T13:53:12.934Z" }, ] [[package]] @@ -603,39 +576,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, ] -[[package]] -name = "scipy" -version = "1.15.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/b9/31ba9cd990e626574baf93fbc1ac61cf9ed54faafd04c479117517661637/scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec", size = 59417316, upload-time = "2025-02-17T00:42:24.791Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/3b/9bda92a85cd93f19f9ed90ade84aa1e51657e29988317fabdd44544f1dd4/scipy-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de9d1416b3d9e7df9923ab23cd2fe714244af10b763975bea9e4f2e81cebd27", size = 35163195, upload-time = "2025-02-17T00:33:15.352Z" }, - { url = "https://files.pythonhosted.org/packages/03/5a/fc34bf1aa14dc7c0e701691fa8685f3faec80e57d816615e3625f28feb43/scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb530e4794fc8ea76a4a21ccb67dea33e5e0e60f07fc38a49e821e1eae3b71a0", size = 37255404, upload-time = "2025-02-17T00:33:22.21Z" }, - { url = "https://files.pythonhosted.org/packages/4a/71/472eac45440cee134c8a180dbe4c01b3ec247e0338b7c759e6cd71f199a7/scipy-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5ea7ed46d437fc52350b028b1d44e002646e28f3e8ddc714011aaf87330f2f32", size = 36860011, upload-time = "2025-02-17T00:33:29.446Z" }, - { url = "https://files.pythonhosted.org/packages/01/b3/21f890f4f42daf20e4d3aaa18182dddb9192771cd47445aaae2e318f6738/scipy-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11e7ad32cf184b74380f43d3c0a706f49358b904fa7d5345f16ddf993609184d", size = 39657406, upload-time = "2025-02-17T00:33:39.019Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/77cf2ac1f2a9cc00c073d49e1e16244e389dd88e2490c91d84e1e3e4d126/scipy-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5080a79dfb9b78b768cebf3c9dcbc7b665c5875793569f48bf0e2b1d7f68f6f", size = 40961243, upload-time = "2025-02-17T00:34:51.024Z" }, - { url = "https://files.pythonhosted.org/packages/b1/53/1cbb148e6e8f1660aacd9f0a9dfa2b05e9ff1cb54b4386fe868477972ac2/scipy-1.15.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd5b77413e1855351cdde594eca99c1f4a588c2d63711388b6a1f1c01f62274", size = 34952867, upload-time = "2025-02-17T00:34:12.928Z" }, - { url = "https://files.pythonhosted.org/packages/2c/23/e0eb7f31a9c13cf2dca083828b97992dd22f8184c6ce4fec5deec0c81fcf/scipy-1.15.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d0194c37037707b2afa7a2f2a924cf7bac3dc292d51b6a925e5fcb89bc5c776", size = 36890009, upload-time = "2025-02-17T00:34:19.55Z" }, - { url = "https://files.pythonhosted.org/packages/03/f3/e699e19cabe96bbac5189c04aaa970718f0105cff03d458dc5e2b6bd1e8c/scipy-1.15.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bae43364d600fdc3ac327db99659dcb79e6e7ecd279a75fe1266669d9a652828", size = 36545159, upload-time = "2025-02-17T00:34:26.724Z" }, - { url = "https://files.pythonhosted.org/packages/af/f5/ab3838e56fe5cc22383d6fcf2336e48c8fe33e944b9037fbf6cbdf5a11f8/scipy-1.15.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f031846580d9acccd0044efd1a90e6f4df3a6e12b4b6bd694a7bc03a89892b28", size = 39136566, upload-time = "2025-02-17T00:34:34.512Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705, upload-time = "2025-02-17T00:34:43.619Z" }, -] - -[[package]] -name = "scipy-stubs" -version = "1.15.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "optype", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/51/3d6dd7233d8fe19063cf0f4280fabbf5a0b0eaf270438808ca96b57eb118/scipy_stubs-1.15.2.1.tar.gz", hash = "sha256:ba9590ef3cd24511dced121ac0a9c8d9d0681ca5ac091128f0f7ba5a936a8693", size = 274081, upload-time = "2025-03-12T13:33:21.085Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/9a/1019c6177105d8a26f9b29ad17b280c4d9495eedcd78b819ef8cee6a7c28/scipy_stubs-1.15.2.1-py3-none-any.whl", hash = "sha256:10977d731ec9b3fbff95dd5868903c9cd1caf8621f4ff8b3b45ef30148337bde", size = 458329, upload-time = "2025-03-12T13:33:19.151Z" }, -] - [[package]] name = "setuptools" version = "80.9.0"