Skip to content

Commit

Permalink
Re-implement pHash comparison using cv2. Drop imagehash+Pillow (
Browse files Browse the repository at this point in the history
#266)

- Re-implement `pHash` comparison using `cv2`.
- Drop `imagehash` and `Pillow` as dependencies
- Reduce bundle size by 1.93MB (139.004 --> 137.074)
- ~1.6x speed improvement for pHash comparison
- Reduce install/build time considerably by not installing D3DShot from GitHub
- Testing suggests some images might differ in comparison from old implementation by at most 2/64 (3.125%)
  • Loading branch information
Avasam authored Dec 19, 2023
1 parent 63b48aa commit f2e6372
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 24 deletions.
4 changes: 2 additions & 2 deletions scripts/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 6 additions & 6 deletions scripts/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion scripts/requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions scripts/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand All @@ -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
42 changes: 32 additions & 10 deletions src/compare.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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)


Expand Down
2 changes: 1 addition & 1 deletion src/region_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
55 changes: 55 additions & 0 deletions typings/scipy/__init__.pyi
Original file line number Diff line number Diff line change
@@ -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]: ...
72 changes: 72 additions & 0 deletions typings/scipy/fft/__init__.pyi
Original file line number Diff line number Diff line change
@@ -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
102 changes: 102 additions & 0 deletions typings/scipy/fft/_realtransforms.pyi
Original file line number Diff line number Diff line change
@@ -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,
): ...
1 change: 1 addition & 0 deletions typings/scipy/py.typed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
partial

0 comments on commit f2e6372

Please sign in to comment.