Skip to content

Commit a8e0249

Browse files
authored
Native Linux ARM64 builds (#328)
1 parent 02ed8d8 commit a8e0249

File tree

6 files changed

+139
-65
lines changed

6 files changed

+139
-65
lines changed

.github/ISSUE_TEMPLATE/bug-or-crash-report.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ If AutoSplit showed an exception traceback, please paste it here:
3939
### Version (please complete the following information)
4040

4141
- OS: [e.g. Windows 10.0.19045]
42+
- Architecture: [x64 or ARM64]
4243
- AutoSplit: [e.g. v2.0.0]
4344

4445
### AutoSplit Profile and Split Images

.github/workflows/lint-and-build.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,13 @@ jobs:
7575
fail-fast: false
7676
# Only the Python version we plan on shipping matters.
7777
matrix:
78-
os: [windows-latest, ubuntu-22.04]
78+
# OpenCV doesn't support windows-11-arm yet https://github.com/opencv/opencv-python/issues/806
79+
os: [windows-latest, ubuntu-22.04, ubuntu-22.04-arm]
7980
python-version: ["3.13"]
8081
steps:
8182
- uses: actions/checkout@v4
82-
# region https://github.com/pyinstaller/pyinstaller/issues/9012
83-
- name: Set up Python for PyInstaller tk issue
83+
# region pyinstaller/pyinstaller#9012 + astral-sh/uv#12906
84+
- name: Set up Python for PyInstaller tk and ARM64 issue
8485
if: ${{ startsWith(matrix.os, 'ubuntu') }}
8586
uses: actions/setup-python@v5
8687
with:

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ To understand how to use AutoSplit and how it works in-depth, please read the [t
4444

4545
### Compatibility
4646

47-
- Windows 10 and 11.
47+
- Windows 10 and 11. (x64 only)
4848
- Linux (still in early development)
49-
- Should work on Ubuntu 20.04+ (Only tested on Ubuntu 22.04)
49+
- Both x64 and ARM64 architectures \* (see [Known Limitations](#known-limitations) for ARM64)
50+
- Should work on Ubuntu 22.04+
5051
- Wayland is not currently supported
5152
- WSL2/WSLg requires an additional Desktop Environment, external X11 server, and/or systemd
5253
- If you'd like to run the project directly in Python from the source code, refer to the [build instructions](/docs/build%20instructions.md).
@@ -73,6 +74,7 @@ See the [installation instructions](https://github.com/Toufool/LiveSplit.AutoSpl
7374
- Linux support is incomplete and we're [looking for contributors](../../issues?q=is%3Aissue+is%3Aopen+label%3A"help+wanted"+label%3ALinux+).
7475
- Incompatible with LiveSplitOne on Linux (see <https://github.com/LiveSplit/LiveSplitOne/issues/1025>)
7576
- Antivirus false positives. Please read <https://github.com/pyinstaller/pyinstaller/blob/develop/.github/ISSUE_TEMPLATE/antivirus.md>
77+
- The Perceptual Hash Comparison Method similarity may differ by 3.125% on ARM64. (this will be solved eventually, we have to use a fallback method for now)
7678

7779
## Resources
7880

pyproject.toml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@ dependencies = [
88
"PyAutoGUI >=0.9.52",
99
"PyWinCtl >=0.0.42", # py.typed
1010
"keyboard @ git+https://github.com/boppreh/keyboard.git", # Fix install on macos and linux-ci https://github.com/boppreh/keyboard/pull/568
11-
"numpy >=2.1", # Python 3.13 support
12-
"opencv-python-headless >=4.10", # NumPy 2 support
11+
"numpy >=2.3", # Windows ARM64 wheels
1312
"packaging >=20.0", # py.typed
1413
# When needed, dev builds can be found at https://download.qt.io/snapshots/ci/pyside/dev?C=M;O=D
15-
"PySide6-Essentials <6.8.1", # Has typing issue with QMessageBox.warning https://bugreports.qt.io/browse/PYSIDE-2939
16-
# "PySide6-Essentials >=6.8.2", # Fixed typing issue with QMessageBox.warning
17-
"scipy >=1.14.1", # Python 3.13 support
14+
"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))
1815
"tomli-w >=1.1.0", # Typing fixes
1916

17+
# scipy is used for pHash calculation as a smaller, but still fast implementation.
18+
# However, scipy is not available on all environments yet.
19+
# In those cases, we're falling back to opencv-contrib-python's cv2.img_hash
20+
"opencv-contrib-python-headless >=4.10; platform_machine == 'aarch64'", # NumPy 2 support
21+
"opencv-python-headless >=4.10; platform_machine != 'aarch64'", # NumPy 2 support
22+
"scipy >=1.14.1; platform_machine != 'aarch64'", # Python 3.13 support
23+
2024
#
2125
# Build and compile resources
2226
"pyinstaller >=6.14.0", # Mitigate issues with pkg_resources deprecation warning
@@ -55,6 +59,7 @@ dev = [
5559
"ruff >=0.11.13",
5660
#
5761
# Types
62+
"opencv-contrib-python-headless",
5863
"scipy-stubs >=1.14.1.1",
5964
"types-PyAutoGUI",
6065
"types-PyScreeze; sys_platform == 'linux'",

src/compare.py

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import Levenshtein
66
import numpy as np
77
from cv2.typing import MatLike
8-
from scipy import fft
98

109
from utils import (
1110
BGRA_CHANNEL_COUNT,
@@ -90,27 +89,40 @@ def compare_template(source: MatLike, capture: MatLike, mask: MatLike | None = N
9089
return 1 - (min_val / max_error)
9190

9291

93-
def __cv2_phash(image: MatLike, hash_size: int = 8, highfreq_factor: int = 4):
94-
"""Implementation copied from https://github.com/JohannesBuchner/imagehash/blob/38005924fe9be17cfed145bbc6d83b09ef8be025/imagehash/__init__.py#L260 .""" # noqa: E501
95-
# OpenCV has its own pHash comparison implementation in `cv2.img_hash`,
96-
# but it requires contrib/extra modules and is inaccurate
97-
# unless we precompute the size with a specific interpolation.
98-
# See: https://github.com/opencv/opencv_contrib/issues/3295#issuecomment-1172878684
99-
#
100-
# pHash = cv2.img_hash.PHash.create()
101-
# source = cv2.resize(source, (8, 8), interpolation=cv2.INTER_AREA)
102-
# capture = cv2.resize(capture, (8, 8), interpolation=cv2.INTER_AREA)
103-
# source_hash = pHash.compute(source)
104-
# capture_hash = pHash.compute(capture)
105-
# hash_diff = pHash.compare(source_hash, capture_hash)
106-
107-
img_size = hash_size * highfreq_factor
108-
image = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY)
109-
image = cv2.resize(image, (img_size, img_size), interpolation=cv2.INTER_AREA)
110-
dct = fft.dct(fft.dct(image, axis=0), axis=1)
111-
dct_low_frequency = dct[:hash_size, :hash_size]
112-
median = np.median(dct_low_frequency)
113-
return dct_low_frequency > median
92+
try:
93+
from scipy import fft
94+
95+
def __cv2_scipy_compute_phash(image: MatLike, hash_size: int, highfreq_factor: int = 4):
96+
"""Implementation copied from https://github.com/JohannesBuchner/imagehash/blob/38005924fe9be17cfed145bbc6d83b09ef8be025/imagehash/__init__.py#L260 .""" # noqa: E501
97+
img_size = hash_size * highfreq_factor
98+
image = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY)
99+
image = cv2.resize(image, (img_size, img_size), interpolation=cv2.INTER_AREA)
100+
dct = fft.dct(fft.dct(image, axis=0), axis=1)
101+
dct_low_frequency = dct[:hash_size, :hash_size]
102+
median = np.median(dct_low_frequency)
103+
return dct_low_frequency > median
104+
105+
def __cv2_phash(source: MatLike, capture: MatLike, hash_size: int = 8): # pyright: ignore[reportRedeclaration]
106+
source_hash = __cv2_scipy_compute_phash(source, hash_size)
107+
capture_hash = __cv2_scipy_compute_phash(capture, hash_size)
108+
hash_diff = np.count_nonzero(source_hash != capture_hash)
109+
return 1 - (hash_diff / 64.0)
110+
111+
except ModuleNotFoundError:
112+
113+
def __cv2_phash(source: MatLike, capture: MatLike, hash_size: int = 8):
114+
# OpenCV has its own pHash comparison implementation in `cv2.img_hash`,
115+
# but it requires contrib/extra modules and is inaccurate
116+
# unless we precompute the size with a specific interpolation.
117+
# See: https://github.com/opencv/opencv_contrib/issues/3295#issuecomment-1172878684
118+
#
119+
phash = cv2.img_hash.PHash.create()
120+
source = cv2.resize(source, (hash_size, hash_size), interpolation=cv2.INTER_AREA)
121+
capture = cv2.resize(capture, (hash_size, hash_size), interpolation=cv2.INTER_AREA)
122+
source_hash = phash.compute(source)
123+
capture_hash = phash.compute(capture)
124+
hash_diff = phash.compare(source_hash, capture_hash)
125+
return 1 - (hash_diff / 64.0)
114126

115127

116128
def compare_phash(source: MatLike, capture: MatLike, mask: MatLike | None = None):
@@ -130,11 +142,7 @@ def compare_phash(source: MatLike, capture: MatLike, mask: MatLike | None = None
130142
source = cv2.bitwise_and(source, source, mask=mask)
131143
capture = cv2.bitwise_and(capture, capture, mask=mask)
132144

133-
source_hash = __cv2_phash(source)
134-
capture_hash = __cv2_phash(capture)
135-
hash_diff = np.count_nonzero(source_hash != capture_hash)
136-
137-
return 1 - (hash_diff / 64.0)
145+
return __cv2_phash(source, capture)
138146

139147

140148
def extract_and_compare_text(capture: MatLike, texts: Iterable[str], methods_index: Iterable[int]):

0 commit comments

Comments
 (0)