diff --git a/scripts/build.ps1 b/scripts/build.ps1 index 94ebd2e9..54c12858 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -17,7 +17,7 @@ $arguments = @( '--exclude=pymsgbox', '--exclude=pytweening', '--exclude=mouseinfo', - # Used by imagehash.whash - '--exclude=pywt') + # Used by D3DShot + '--exclude=PIL') Start-Process -Wait -NoNewWindow pyinstaller -ArgumentList $arguments diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 140af292..299ba51b 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -6,9 +6,9 @@ pip install -r "$PSScriptRoot/requirements$dev.txt" --upgrade # These libraries install extra requirements we don't want # Open suggestion for support in requirements files: https://github.com/pypa/pip/issues/9948 & https://github.com/pypa/pip/pull/10837 # PyAutoGUI: We only use it for hotkeys -# ImageHash: uneeded + broken on Python 3.12 PyWavelets install -# scipy: needed for ImageHash -pip install PyAutoGUI ImageHash scipy --no-deps --upgrade +# D3DShot: Will install Pillow, which we don't use on Windows. +# Even then, PyPI with Pillow>=7.2.0 will install 0.1.3 instead of 0.1.5 +pip install PyAutoGUI "D3DShot>=0.1.5 ; sys_platform == 'win32'" --no-deps --upgrade # Patch libraries so we don't have to install from git @@ -24,11 +24,11 @@ $libPath = python -c 'import pymonctl as _; print(_.__path__[0])' $libPath = python -c 'import pywinbox as _; print(_.__path__[0])' (Get-Content "$libPath/_pywinbox_win.py").replace('ctypes.windll.shcore.SetProcessDpiAwareness(2)', 'pass') | Set-Content "$libPath/_pywinbox_win.py" -# Uninstall optional dependencies if PyAutoGUI was installed outside this script +# Uninstall optional dependencies if PyAutoGUI or D3DShot was installed outside this script # pyscreeze -> pyscreenshot -> mss deps call SetProcessDpiAwareness -# pygetwindow, pymsgbox, pytweening, MouseInfo are picked up by PySide6 +# Pillow, pygetwindow, pymsgbox, pytweening, MouseInfo are picked up by PySide6 # (also --exclude from build script, but more consistent with unfrozen run) -python -m pip uninstall pyscreeze pyscreenshot mss pygetwindow pymsgbox pytweening MouseInfo -y +python -m pip uninstall pyscreeze pyscreenshot mss Pillow pygetwindow pymsgbox pytweening MouseInfo -y # Don't compile resources on the Build CI job as it'll do so in build script diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt index d36e4131..864c2a13 100644 --- a/scripts/requirements-dev.txt +++ b/scripts/requirements-dev.txt @@ -19,7 +19,6 @@ ruff>=0.1.7 # New checks # Must match .pre-commit-config.yaml # Types types-D3DShot ; sys_platform == 'win32' types-keyboard -types-Pillow types-psutil types-PyAutoGUI types-pyinstaller diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 7973b50f..ab9ff83d 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -3,17 +3,16 @@ # Read /docs/build%20instructions.md for more information on how to install, run and build the python code. # # Dependencies: -ImageHash>=4.3.1 ; python_version < '3.12' # Contains type information + setup as package not module # PyWavelets install broken on Python 3.12 git+https://github.com/boppreh/keyboard.git#egg=keyboard # Fix install on macos and linux-ci https://github.com/boppreh/keyboard/pull/568 numpy>=1.26 # Python 3.12 support opencv-python-headless>=4.8.1.78 # Typing fixes packaging -Pillow>=10.0 # Python 3.12 support psutil>=5.9.6 # Python 3.12 fixes -PyAutoGUI +# PyAutoGUI # See install.ps1 PyWinCtl>=0.0.42 # py.typed # When needed, dev builds can be found at https://download.qt.io/snapshots/ci/pyside/dev?C=M;O=D PySide6-Essentials>=6.6.0 # Python 3.12 support +scipy>=1.11.2 # Python 3.12 support toml typing-extensions>=4.4.0 # @override decorator support # @@ -26,4 +25,4 @@ pyinstaller>=5.13 # Python 3.12 support pygrabber>=0.2 ; sys_platform == 'win32' # Completed types pywin32>=301 ; sys_platform == 'win32' winsdk>=1.0.0b10 ; sys_platform == 'win32' # Python 3.12 support -git+https://github.com/ranchen421/D3DShot.git#egg=D3DShot ; sys_platform == 'win32' # D3DShot from PyPI with Pillow>=7.2.0 will install 0.1.3 instead of 0.1.5 +# D3DShot # See install.ps1 diff --git a/src/compare.py b/src/compare.py index 91e5c243..49bec7e6 100644 --- a/src/compare.py +++ b/src/compare.py @@ -1,9 +1,9 @@ from math import sqrt import cv2 -import imagehash +import numpy as np from cv2.typing import MatLike -from PIL import Image +from scipy import fft from utils import BGRA_CHANNEL_COUNT, MAXBYTE, ColorChannel, ImageShape, is_valid_image @@ -80,6 +80,28 @@ def compare_template(source: MatLike, capture: MatLike, mask: MatLike | None = N return 1 - (min_val / max_error) +def __cv2_phash(image: MatLike, hash_size: int = 8, highfreq_factor: int = 4): + """Implementation copied from https://github.com/JohannesBuchner/imagehash/blob/38005924fe9be17cfed145bbc6d83b09ef8be025/imagehash/__init__.py#L260 .""" # noqa: E501 + # OpenCV has its own pHash comparison implementation in `cv2.img_hash`, but it requires contrib/extra modules + # and is innacurate 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, (8, 8), interpolation=cv2.INTER_AREA) + # capture = cv2.resize(capture, (8, 8), interpolation=cv2.INTER_AREA) + # source_hash = pHash.compute(source) + # capture_hash = pHash.compute(capture) + # hash_diff = pHash.compare(source_hash, capture_hash) + + 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 compare_phash(source: MatLike, capture: MatLike, mask: MatLike | None = None): """ Compares the Perceptual Hash of the two given images and returns the similarity between the two. @@ -89,18 +111,18 @@ def compare_phash(source: MatLike, capture: MatLike, mask: MatLike | None = None @param mask: An image matching the dimensions of the source, but 1 channel grayscale @return: The similarity between the hashes of the image as a number 0 to 1. """ - # Since imagehash doesn't have any masking itself, bitwise_and will allow us - # to apply the mask to the source and capture before calculating the pHash for - # each of the images. As a result of this, this function is not going to be very - # helpful for large masks as the images when shrinked down to 8x8 will mostly be - # the same + # Apply the mask to the source and capture before calculating the + # pHash for each of the images. As a result of this, this function + # is not going to be very helpful for large masks as the images + # when shrinked down to 8x8 will mostly be the same. if is_valid_image(mask): source = cv2.bitwise_and(source, source, mask=mask) capture = cv2.bitwise_and(capture, capture, mask=mask) - source_hash = imagehash.phash(Image.fromarray(source)) # pyright: ignore[reportUnknownMemberType] - capture_hash = imagehash.phash(Image.fromarray(capture)) # pyright: ignore[reportUnknownMemberType] - hash_diff = source_hash - capture_hash + source_hash = __cv2_phash(source) + capture_hash = __cv2_phash(capture) + hash_diff = np.count_nonzero(source_hash != capture_hash) + return 1 - (hash_diff / 64.0) diff --git a/src/region_selection.py b/src/region_selection.py index 74ce752a..4ffa7081 100644 --- a/src/region_selection.py +++ b/src/region_selection.py @@ -76,7 +76,7 @@ def callback(async_operation: IAsyncOperation[GraphicsCaptureItem], async_status autosplit.capture_method.reinitialize() picker = GraphicsCapturePicker() - initialize_with_window(picker, int(autosplit.effectiveWinId())) + initialize_with_window(picker, autosplit.effectiveWinId()) async_operation = picker.pick_single_item_async() # None if the selection is canceled if async_operation: diff --git a/typings/scipy/__init__.pyi b/typings/scipy/__init__.pyi new file mode 100644 index 00000000..a684150f --- /dev/null +++ b/typings/scipy/__init__.pyi @@ -0,0 +1,55 @@ + +from numpy.fft import ifft as ifft +from numpy.random import rand as rand, randn as randn +from scipy import ( + cluster, + constants, + datasets, + fft, + fftpack, + integrate, + interpolate, + io, + linalg, + misc, + ndimage, + odr, + optimize, + signal, + sparse, + spatial, + special, + stats, +) +from scipy.__config__ import show as show_config +from scipy._lib._ccallback import LowLevelCallable +from scipy._lib._testutils import PytestTester +from scipy.version import version as __version__ + +__all__ = [ + "cluster", + "constants", + "datasets", + "fft", + "fftpack", + "integrate", + "interpolate", + "io", + "linalg", + "misc", + "ndimage", + "odr", + "optimize", + "signal", + "sparse", + "spatial", + "special", + "stats", + "LowLevelCallable", + "test", + "show_config", + "__version__", +] + +test: PytestTester +def __dir__() -> list[str]: ... diff --git a/typings/scipy/fft/__init__.pyi b/typings/scipy/fft/__init__.pyi new file mode 100644 index 00000000..ea8b8f3d --- /dev/null +++ b/typings/scipy/fft/__init__.pyi @@ -0,0 +1,72 @@ +from numpy.fft import fftfreq, fftshift, ifftshift, rfftfreq +from scipy._lib._testutils import PytestTester +from scipy.fft._backend import register_backend, set_backend, set_global_backend, skip_backend +from scipy.fft._basic import ( + fft, + fft2, + fftn, + hfft, + hfft2, + hfftn, + ifft, + ifft2, + ifftn, + ihfft, + ihfft2, + ihfftn, + irfft, + irfft2, + irfftn, + rfft, + rfft2, + rfftn, +) +from scipy.fft._fftlog import fhtoffset +from scipy.fft._fftlog_multimethods import fht, ifht +from scipy.fft._helper import next_fast_len +from scipy.fft._pocketfft.helper import get_workers, set_workers +from scipy.fft._realtransforms import dct, dctn, dst, dstn, idct, idctn, idst, idstn + +__all__ = [ + "fft", + "ifft", + "fft2", + "ifft2", + "fftn", + "ifftn", + "rfft", + "irfft", + "rfft2", + "irfft2", + "rfftn", + "irfftn", + "hfft", + "ihfft", + "hfft2", + "ihfft2", + "hfftn", + "ihfftn", + "fftfreq", + "rfftfreq", + "fftshift", + "ifftshift", + "next_fast_len", + "dct", + "idct", + "dst", + "idst", + "dctn", + "idctn", + "dstn", + "idstn", + "fht", + "ifht", + "fhtoffset", + "set_backend", + "skip_backend", + "set_global_backend", + "register_backend", + "get_workers", + "set_workers", +] +test: PytestTester diff --git a/typings/scipy/fft/_realtransforms.pyi b/typings/scipy/fft/_realtransforms.pyi new file mode 100644 index 00000000..bbddf386 --- /dev/null +++ b/typings/scipy/fft/_realtransforms.pyi @@ -0,0 +1,102 @@ +from _typeshed import Incomplete +from numpy import float64, generic +from numpy.typing import NDArray + +__all__ = ["dct", "idct", "dst", "idst", "dctn", "idctn", "dstn", "idstn"] + + +def dctn( + x, + type=2, + s=None, + axes=None, + norm=None, + overwrite_x=False, + workers=None, + *, + orthogonalize=None, +): ... + + +def idctn( + x, + type=2, + s=None, + axes=None, + norm=None, + overwrite_x=False, + workers=None, + orthogonalize=None, +): ... + + +def dstn( + x, + type=2, + s=None, + axes=None, + norm=None, + overwrite_x=False, + workers=None, + orthogonalize=None, +): ... + + +def idstn( + x, + type=2, + s=None, + axes=None, + norm=None, + overwrite_x=False, + workers=None, + orthogonalize=None, +): ... + + +def dct( + x: NDArray[generic], + type: int = 2, + n: Incomplete | None = None, + axis: int = -1, + norm: Incomplete | None = None, + overwrite_x: bool = False, + workers: Incomplete | None = None, + orthogonalize: Incomplete | None = None, +) -> NDArray[float64]: ... + + +def idct( + x, + type=2, + n=None, + axis=-1, + norm=None, + overwrite_x=False, + workers=None, + orthogonalize=None, +): ... + + +def dst( + x, + type=2, + n=None, + axis=-1, + norm=None, + overwrite_x=False, + workers=None, + orthogonalize=None, +): ... + + +def idst( + x, + type=2, + n=None, + axis=-1, + norm=None, + overwrite_x=False, + workers=None, + orthogonalize=None, +): ... diff --git a/typings/scipy/py.typed b/typings/scipy/py.typed new file mode 100644 index 00000000..b648ac92 --- /dev/null +++ b/typings/scipy/py.typed @@ -0,0 +1 @@ +partial