PyPatchMatch 2.0.0rc5__tar.gz → 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. pypatchmatch-2.0.1/CLAUDE.md +155 -0
  2. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/PKG-INFO +18 -11
  3. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/README.md +13 -6
  4. pypatchmatch-2.0.1/SECURITY.md +20 -0
  5. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/_version.py +3 -3
  6. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/csrc/inpaint.cpp +12 -1
  7. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/pyproject.toml +27 -9
  8. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/scripts/build_opencv.py +25 -10
  9. pypatchmatch-2.0.1/scripts/render_readme_example.py +36 -0
  10. pypatchmatch-2.0.1/tests/conftest.py +6 -0
  11. pypatchmatch-2.0.1/tests/test_properties.py +159 -0
  12. pypatchmatch-2.0.0rc5/CLAUDE.md +0 -436
  13. pypatchmatch-2.0.0rc5/tests/test_properties.py +0 -95
  14. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/CMakeLists.txt +0 -0
  15. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/LICENSE +0 -0
  16. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/examples/images/forest_pruned.bmp +0 -0
  17. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/__init__.py +0 -0
  18. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/_lib.py +0 -0
  19. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/csrc/inpaint.h +0 -0
  20. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/csrc/masked_image.cpp +0 -0
  21. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/csrc/masked_image.h +0 -0
  22. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/csrc/nnf.cpp +0 -0
  23. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/csrc/nnf.h +0 -0
  24. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/csrc/pyinterface.cpp +0 -0
  25. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/csrc/pyinterface.h +0 -0
  26. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/patch_match.py +0 -0
  27. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/patchmatch/py.typed +0 -0
  28. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/scripts/evaluate_inpainting.py +0 -0
  29. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/tests/test_e2e.py +0 -0
  30. {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.1}/tests/test_patch_match.py +0 -0
@@ -0,0 +1,155 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in
4
+ this repository. The details of the native code, the C interface, packaging and CI are
5
+ path-scoped rules in `.claude/rules/`, which load when a matching file is read or
6
+ edited; read the rule before changing such a file through Bash. The sanitizer, C++
7
+ coverage and fill-quality runs are skills in `.claude/skills/`.
8
+
9
+ ## Project
10
+
11
+ PyPatchMatch implements PatchMatch based image inpainting (Barnes et al., SIGGRAPH 2009):
12
+ a C++17 library on top of OpenCV core, and a thin Python package that loads it through
13
+ `ctypes`. It is published on PyPI as `PyPatchMatch` (import name `patchmatch`) and used
14
+ by InvokeAI. This repository (`mauwii/PyPatchMatch`) is the maintained fork of
15
+ `invoke-ai/PyPatchMatch`, which derives from `vacancy/PyPatchMatch` and
16
+ `younesse-cv/PatchMatch`.
17
+
18
+ ## Commands
19
+
20
+ The project is managed with uv (`.python-version` pins 3.12 for the local venv). Building
21
+ needs a C++17 compiler, CMake >= 3.21 and an OpenCV that `find_package(OpenCV)` finds:
22
+ Homebrew `opencv`, `libopencv-dev`, or an installation prefix in `OpenCV_ROOT`.
23
+
24
+ ```sh
25
+ uv sync # .venv, editable install, compiles the library
26
+ uv run pre-commit install
27
+ uv run pytest # all tests, with the coverage gate
28
+ uv run pytest -m "not e2e" # fast unit tests only
29
+ uv run pytest -m e2e --no-cov # end-to-end tests only
30
+ uv run pytest tests/test_patch_match.py::test_inpaint_global_mask --no-cov
31
+ uv run pytest -k regularity --no-cov
32
+ uv run mypy # strict, also a pre-commit hook
33
+ uv run pre-commit run --all-files # what the CI lint job runs
34
+ uv run ruff check --fix . && uv run ruff format .
35
+ uv build # sdist and wheel into dist/
36
+ ```
37
+
38
+ - Pass `--no-cov` for any partial run: `addopts` always enables coverage, and
39
+ `fail_under = 95` (branch coverage, subprocesses included through
40
+ `patch = ["subprocess"]`) fails the run otherwise.
41
+ - Use `uv run pytest`, not `python -m pytest`. The latter puts the repository root on
42
+ `sys.path`, which imports the source tree `patchmatch/` without the compiled library;
43
+ the symptoms are the warning `patchmatch failed to load: libpatchmatch.* not found`
44
+ and `RuntimeError: the patchmatch native library is not available` in every test.
45
+ - `--cov=patchmatch` measures the source tree, so coverage only works with the editable
46
+ install that `uv sync` creates. With a non-editable install (`uv sync --no-editable`,
47
+ `uv pip install .`) it reports 0 % and the gate fails.
48
+ - `uv sync` rebuilds the library only when a `tool.uv.cache-keys` entry changes
49
+ (`CMakeLists.txt`, `patchmatch/csrc/*`, `pyproject.toml`, git commit or tags). Force it
50
+ with `uv sync --reinstall-package pypatchmatch`.
51
+ - CI builds the library with warnings as errors. Locally:
52
+ `SKBUILD_CMAKE_DEFINE=CMAKE_COMPILE_WARNING_AS_ERROR=ON uv sync --reinstall-package pypatchmatch`.
53
+
54
+ Build against the same static, core-only OpenCV as the release wheels:
55
+
56
+ ```sh
57
+ export OpenCV_ROOT=/tmp/opencv
58
+ uv run --no-project python scripts/build_opencv.py # pinned version and SHA-256
59
+ uv sync --reinstall-package pypatchmatch
60
+ ```
61
+
62
+ Other entry points:
63
+
64
+ - `examples/cpp_example_run.sh` builds the library and `examples/cpp_example.cpp` with
65
+ CMake into `build/cpp` and runs it. It needs OpenCV `imgcodecs`, so a full OpenCV, not
66
+ the core-only build of the script.
67
+ - `uv run python examples/py_example.py` (and `py_example_global_mask.py`) write
68
+ `examples/images/forest_recovered.bmp`, which is git-ignored.
69
+ - `uv run python scripts/render_readme_example.py` runs `py_example.py` and renders the
70
+ picture at the top of the README, `examples/images/readme_example.jpg`; rerun it and
71
+ commit the picture after changes that affect the fills.
72
+ - CMake options: `PATCHMATCH_BUILD_EXAMPLES` (OFF), `PATCHMATCH_FAST_MATH` (ON,
73
+ `-ffast-math` or `/fp:fast`), `PATCHMATCH_SANITIZE` (OFF, ASan and UBSan with
74
+ `float-cast-overflow`, every finding aborts; turns off fast math, which lets the
75
+ compiler drop checks; GCC and Clang only), `PATCHMATCH_COVERAGE` (OFF, gcov counters
76
+ at `-Og`; GCC and Clang only).
77
+
78
+ ## Architecture
79
+
80
+ ### Layers, in the call order of `patchmatch.inpaint`
81
+
82
+ 1. `patchmatch/__init__.py` re-exports the public API of `patch_match.py` and
83
+ `__version__` from the generated `_version.py`. `test_top_level_exports` checks that
84
+ every name in `patch_match.__all__` is re-exported.
85
+ 2. `patchmatch/patch_match.py` is the public API. It validates and canonicalizes the
86
+ inputs (`_prepare_inputs`, `_check_patch_size`, the `ijmap` and `guide_weight`
87
+ checks), picks the entry point (`PM_inpaint`, `PM_inpaint2` with a global mask, and
88
+ their `_regularity` variants) and runs it through `_call`. It loads the library at
89
+ import time; on `OSError` it logs a warning and sets `patchmatch_available = False`,
90
+ and every function then raises `RuntimeError` through `_get_lib()`.
91
+ 3. `patchmatch/_lib.py` is the ctypes layer: the `CShapeT`/`CMatT` structures, the dtype
92
+ table, `np_to_pymat`/`pymat_to_np`, `find_library` (looks for `libpatchmatch.so`,
93
+ `libpatchmatch.dylib` or `patchmatch.dll` in every directory of `patchmatch.__path__`)
94
+ and `load_library` (argtypes and restype of every `PM_*` function).
95
+ 4. `patchmatch/csrc/pyinterface.{h,cpp}` is the `extern "C"` API: the `PM_*` functions,
96
+ the conversion between `PM_mat_t` and `cv::Mat`, the global settings and the error
97
+ reporting.
98
+ 5. `csrc/inpaint.{h,cpp}`: `Inpainting`, the image pyramid and the EM loop.
99
+ 6. `csrc/nnf.{h,cpp}`: `NearestNeighborField` (propagation and random search) and the
100
+ distance metrics `PatchSSDDistanceMetric` and `RegularityGuidedPatchDistanceMetricV2`.
101
+ 7. `csrc/masked_image.{h,cpp}`: `MaskedImage`, an image with its hole mask and optional
102
+ global mask, down- and upsampling, gradients.
103
+
104
+ The C++ headers double as the C++ API: `examples/cpp_example.cpp` uses `Inpainting` and
105
+ `PatchSSDDistanceMetric` directly.
106
+
107
+ ### ctypes instead of a CPython extension
108
+
109
+ The library does not use the CPython ABI, so the wheels are tagged
110
+ `py3-none-<platform>` (`wheel.py-api = "py3"`): one wheel per platform serves all Python
111
+ versions, and cibuildwheel only builds `cp311-*`. Keep `csrc` free of `Python.h`,
112
+ pybind11 or nanobind. The C++ sources are excluded from the wheel (`wheel.exclude`);
113
+ CMake installs the shared library into the package directory.
114
+
115
+ ## Conventions
116
+
117
+ - Python >= 3.11: keep `from __future__ import annotations`. Ruff targets py311 with the
118
+ rules B, C4, E, F, I, RUF, SIM, UP and W, and formats at 88 columns. `patchmatch/csrc`
119
+ is excluded from ruff. C++ is formatted by clang-format (Microsoft style, 120 columns,
120
+ includes grouped as own header, `<...>`, `"..."`); naming is not checked: `m_`
121
+ members, `k` constants. Commit a reformat separately and add its hash to
122
+ `.git-blame-ignore-revs`.
123
+ - The package ships `py.typed`; public functions are fully annotated. mypy checks
124
+ `patchmatch` and `tests` in strict mode (`[tool.mypy]`); the tests have to use the
125
+ API correctly but need no annotations of their own. A deliberately wrong argument
126
+ goes through a test parameter instead of a `# type: ignore`.
127
+ - Backwards compatibility: `from patchmatch import patch_match` and
128
+ `patch_match.CShapeT`/`CMatT` keep working (tested).
129
+ - Comments explain why: which crash a check prevents, why a construct is avoided.
130
+ - No `NOSONAR` markers: change the code so that the SonarCloud rule is satisfied, and ask
131
+ when that seems impossible. The only exception is `nnf.cpp`: cpp:S2245 flags every
132
+ declaration of `std::mt19937`, which only drives the randomized search.
133
+ - Tests check invariants instead of golden images: holes filled, all other pixels
134
+ unchanged, determinism with `set_random_seed`, fills that blend in with their
135
+ surroundings. `test_patch_match.py` and `test_e2e.py` seed through an autouse fixture.
136
+ Unit tests use a small synthetic image; the `e2e` tests use
137
+ `examples/images/forest_pruned.bmp` and mirror the workflows of the README and the
138
+ examples. `test_properties.py` checks the invariants on random inputs with Hypothesis
139
+ (in the `test` group, so the wheel tests run it too); it seeds inside the tests,
140
+ because Hypothesis rejects function-scoped fixtures, and has no deadline, because
141
+ the sanitizer and coverage builds are much slower.
142
+ - The README documents the behavior (mask semantics, `patch_size` as a radius, the
143
+ fallback installation); update it together with API or installation changes.
144
+ - Commit messages: imperative subject in sentence case without a prefix (e.g. "Reject
145
+ invalid patch sizes and empty regularity maps"), and a body that explains the reason,
146
+ with measurements where relevant. Changes go through PRs from branches such as
147
+ `fix/<topic>`.
148
+
149
+ ## Git and GitHub
150
+
151
+ - `origin` is `mauwii/PyPatchMatch`, where PRs and releases go; `upstream` is
152
+ `invoke-ai/PyPatchMatch`. `gh` resolves the default repository to upstream, so pass
153
+ `-R mauwii/PyPatchMatch`.
154
+ - Releases: creating a GitHub release with a tag like `v2.0.0` (or `v2.0.0rc2` for
155
+ pre-releases) builds the wheels and publishes them to PyPI.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPatchMatch
3
- Version: 2.0.0rc5
3
+ Version: 2.0.1
4
4
  Summary: This library implements the PatchMatch based inpainting algorithm.
5
5
  Keywords: inpainting,patchmatch,image-processing,opencv
6
6
  Author-Email: The InvokeAI Project <lincoln.stein@gmail.com>, Jiayuan Mao <maojiayuan@gmail.com>, Kyle Schouviller <kyle0654@hotmail.com>, Matthias Wild <mauwii@outlook.de>, Younesse ANDAM <younesse.andam@gmail.com>
@@ -12,20 +12,20 @@ Classifier: Operating System :: Microsoft :: Windows
12
12
  Classifier: Operating System :: POSIX :: Linux
13
13
  Classifier: Programming Language :: C++
14
14
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
19
18
  Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
20
20
  Classifier: Topic :: Multimedia :: Graphics
21
21
  Classifier: Topic :: Scientific/Engineering :: Image Processing
22
22
  Classifier: Typing :: Typed
23
23
  Project-URL: Source Code, https://github.com/mauwii/PyPatchMatch
24
24
  Project-URL: Issues, https://github.com/mauwii/PyPatchMatch/issues
25
25
  Project-URL: Release Notes, https://github.com/mauwii/PyPatchMatch/releases
26
- Requires-Python: >=3.10
27
- Requires-Dist: numpy>=1.21.6
28
- Requires-Dist: pillow>=8.4.0
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: numpy>=1.23.2
28
+ Requires-Dist: pillow>=9.2.0
29
29
  Description-Content-Type: text/markdown
30
30
 
31
31
  # PatchMatch based Inpainting
@@ -42,6 +42,12 @@ and Python interfaces. This implementation is heavily based on the implementatio
42
42
  Younesse ANDAM: [younesse-cv/PatchMatch](https://github.com/younesse-cv/PatchMatch),
43
43
  with some bug fixes, and updates.
44
44
 
45
+ ![Birdhouses removed from a photo by patchmatch.inpaint](https://raw.githubusercontent.com/mauwii/PyPatchMatch/v2.0.1/examples/images/readme_example.jpg)
46
+
47
+ From left to right: the photo, the input of
48
+ [examples/py_example.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example.py)
49
+ with the birdhouses painted out in white, and its result.
50
+
45
51
  ## Installation
46
52
 
47
53
  ```sh
@@ -78,12 +84,13 @@ result = patchmatch.inpaint(image, mask, patch_size=3)
78
84
 
79
85
  The mask must have the same height and width as the image. If `mask` is omitted, all
80
86
  pure white pixels are treated as holes. Only the holes are filled; all other pixels keep
81
- their values. Holes need hard edges: an anti-aliased brush leaves light pixels along
82
- the edge of its strokes that are not pure white, so they count as known and the fill
83
- continues them. Paint without anti-aliasing or pass a mask that covers those pixels as
84
- well. The optional keyword argument `global_mask`, in the same format, marks
85
- pixels that are neither filled nor used as a source; they keep their values as well, and
86
- their colors have no influence on the fill (see
87
+ their values, and the colors under the holes have no influence on the fill. Holes need
88
+ hard edges: an anti-aliased brush leaves light pixels along the edge of its strokes that
89
+ are not pure white, so they count as known and the fill continues them. Paint without
90
+ anti-aliasing or pass a mask that covers those pixels as well. The optional keyword
91
+ argument `global_mask`, in the same format, marks pixels that are neither filled nor
92
+ used as a source; they keep their values as well, and their colors have no influence on
93
+ the fill (see
87
94
  [examples/py_example_global_mask.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example_global_mask.py)).
88
95
  `patchmatch.patchmatch_available` tells whether the native library could be loaded.
89
96
  The previous import path `from patchmatch import patch_match` keeps working.
@@ -12,6 +12,12 @@ and Python interfaces. This implementation is heavily based on the implementatio
12
12
  Younesse ANDAM: [younesse-cv/PatchMatch](https://github.com/younesse-cv/PatchMatch),
13
13
  with some bug fixes, and updates.
14
14
 
15
+ ![Birdhouses removed from a photo by patchmatch.inpaint](examples/images/readme_example.jpg)
16
+
17
+ From left to right: the photo, the input of
18
+ [examples/py_example.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example.py)
19
+ with the birdhouses painted out in white, and its result.
20
+
15
21
  ## Installation
16
22
 
17
23
  ```sh
@@ -48,12 +54,13 @@ result = patchmatch.inpaint(image, mask, patch_size=3)
48
54
 
49
55
  The mask must have the same height and width as the image. If `mask` is omitted, all
50
56
  pure white pixels are treated as holes. Only the holes are filled; all other pixels keep
51
- their values. Holes need hard edges: an anti-aliased brush leaves light pixels along
52
- the edge of its strokes that are not pure white, so they count as known and the fill
53
- continues them. Paint without anti-aliasing or pass a mask that covers those pixels as
54
- well. The optional keyword argument `global_mask`, in the same format, marks
55
- pixels that are neither filled nor used as a source; they keep their values as well, and
56
- their colors have no influence on the fill (see
57
+ their values, and the colors under the holes have no influence on the fill. Holes need
58
+ hard edges: an anti-aliased brush leaves light pixels along the edge of its strokes that
59
+ are not pure white, so they count as known and the fill continues them. Paint without
60
+ anti-aliasing or pass a mask that covers those pixels as well. The optional keyword
61
+ argument `global_mask`, in the same format, marks pixels that are neither filled nor
62
+ used as a source; they keep their values as well, and their colors have no influence on
63
+ the fill (see
57
64
  [examples/py_example_global_mask.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example_global_mask.py)).
58
65
  `patchmatch.patchmatch_available` tells whether the native library could be loaded.
59
66
  The previous import path `from patchmatch import patch_match` keeps working.
@@ -0,0 +1,20 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Security fixes are released as a new version of PyPatchMatch on PyPI. Only the latest
6
+ release is supported; older versions are not updated.
7
+
8
+ ## Reporting a Vulnerability
9
+
10
+ Please report vulnerabilities privately through GitHub's
11
+ [vulnerability reporting](https://github.com/mauwii/PyPatchMatch/security/advisories/new),
12
+ not in a public issue.
13
+
14
+ ## OpenCV
15
+
16
+ The wheels on PyPI contain a statically linked, core-only build of OpenCV; its version
17
+ is listed in the CycloneDX SBOM in `sboms/opencv.cdx.json` of each wheel's
18
+ `.dist-info`. A vulnerability in OpenCV core is fixed by a new PyPatchMatch release
19
+ with an updated OpenCV. Installations built from the source distribution link the
20
+ OpenCV of the system and get its fixes from there.
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.0.0rc5'
22
- __version_tuple__ = version_tuple = (2, 0, 0, 'rc5')
21
+ __version__ = version = '2.0.1'
22
+ __version_tuple__ = version_tuple = (2, 0, 1)
23
23
 
24
- __commit_id__ = commit_id = 'g3dcd7aa74'
24
+ __commit_id__ = commit_id = 'g995b76749'
@@ -164,7 +164,18 @@ Inpainting::Inpainting(
164
164
 
165
165
  void Inpainting::_initialize_pyramid()
166
166
  {
167
- auto source = m_initial;
167
+ // The colors under the holes would reach the fill through the gradients of their
168
+ // neighbors and the target of a single-level pyramid. Black, like on coarser levels.
169
+ auto source = m_initial.clone();
170
+ const auto size = source.size();
171
+ for (int y = 0; y < size.height; ++y)
172
+ {
173
+ for (int x = 0; x < size.width; ++x)
174
+ {
175
+ if (source.is_masked(y, x))
176
+ std::fill_n(source.get_mutable_image(y, x), 3, 0);
177
+ }
178
+ }
168
179
  m_pyramid.push_back(source);
169
180
  while (source.size().height > m_distance_metric->patch_size() &&
170
181
  source.size().width > m_distance_metric->patch_size())
@@ -1,11 +1,10 @@
1
1
  [build-system]
2
2
  build-backend = "scikit_build_core.build"
3
- requires = ["scikit-build-core>=0.11", "setuptools-scm>=8"]
3
+ requires = ["scikit-build-core>=0.11.2", "setuptools-scm>=8"]
4
4
 
5
5
  [project]
6
6
  name = "PyPatchMatch"
7
7
  description = "This library implements the PatchMatch based inpainting algorithm."
8
- readme = "README.md"
9
8
  license = "MIT"
10
9
  license-files = ["LICENSE"]
11
10
  authors = [
@@ -15,7 +14,7 @@ authors = [
15
14
  { name = "Matthias Wild", email = "mauwii@outlook.de" },
16
15
  { name = "Younesse ANDAM", email = "younesse.andam@gmail.com" },
17
16
  ]
18
- requires-python = ">=3.10"
17
+ requires-python = ">=3.11"
19
18
  keywords = ["inpainting", "patchmatch", "image-processing", "opencv"]
20
19
  classifiers = [
21
20
  "Development Status :: 5 - Production/Stable",
@@ -24,17 +23,17 @@ classifiers = [
24
23
  "Operating System :: POSIX :: Linux",
25
24
  "Programming Language :: C++",
26
25
  "Programming Language :: Python :: 3",
27
- "Programming Language :: Python :: 3.10",
28
26
  "Programming Language :: Python :: 3.11",
29
27
  "Programming Language :: Python :: 3.12",
30
28
  "Programming Language :: Python :: 3.13",
31
29
  "Programming Language :: Python :: 3.14",
30
+ "Programming Language :: Python :: 3.15",
32
31
  "Topic :: Multimedia :: Graphics",
33
32
  "Topic :: Scientific/Engineering :: Image Processing",
34
33
  "Typing :: Typed",
35
34
  ]
36
- dependencies = ["numpy>=1.21.6", "pillow>=8.4.0"]
37
- dynamic = ["version"]
35
+ dependencies = ["numpy>=1.23.2", "pillow>=9.2.0"]
36
+ dynamic = ["version", "readme"]
38
37
 
39
38
  [project.urls]
40
39
  "Source Code" = "https://github.com/mauwii/PyPatchMatch"
@@ -56,6 +55,7 @@ wheel.py-api = "py3"
56
55
  wheel.packages = ["patchmatch"]
57
56
  wheel.exclude = ["patchmatch/csrc/**"]
58
57
  metadata.version.provider = "scikit_build_core.metadata.setuptools_scm"
58
+ metadata.readme.provider = "scikit_build_core.metadata.fancy_pypi_readme"
59
59
  # The release wheels are built from the sdist, so it has to contain the tests
60
60
  # and their input image; _version.py is generated and therefore git-ignored.
61
61
  sdist.include = ["examples/images/forest_pruned.bmp", "patchmatch/_version.py"]
@@ -70,6 +70,17 @@ if.from-sdist = true
70
70
  if.failed = true
71
71
  wheel.cmake = false
72
72
 
73
+ [tool.hatch.metadata.hooks.fancy-pypi-readme]
74
+ content-type = "text/markdown"
75
+
76
+ [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
77
+ path = "README.md"
78
+
79
+ # PyPI does not resolve relative image paths
80
+ [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
81
+ pattern = '!\[([^\]]*)\]\((?!https?:)([^)]+)\)'
82
+ replacement = '![\1](https://raw.githubusercontent.com/mauwii/PyPatchMatch/v$HFPR_VERSION/\2)'
83
+
73
84
  [tool.setuptools_scm]
74
85
  version_file = "patchmatch/_version.py"
75
86
  # used when building without git metadata, e.g. from a GitHub source archive
@@ -78,10 +89,13 @@ fallback_version = "0.0.0"
78
89
  [tool.cibuildwheel]
79
90
  # The wheels are Python-version independent (py3-none-<platform>), so each platform
80
91
  # is built and tested once; the CI workflow covers the other Python versions.
81
- build = "cp310-*"
92
+ build = "cp311-*"
82
93
  skip = "*-musllinux_*"
83
94
  # OpenCV is built once per job in before-all, only for the runner's native arch
84
95
  archs = ["native"]
96
+ # uv installs the build and test environments, so that UV_EXCLUDE_NEWER of wheels.yml
97
+ # applies to them
98
+ build-frontend = "build[uv]"
85
99
  test-groups = ["test"]
86
100
  test-command = "pytest {project}/tests --no-cov"
87
101
  before-all = "python {project}/scripts/build_opencv.py"
@@ -89,6 +103,7 @@ environment = { OpenCV_ROOT = "/tmp/opencv" }
89
103
 
90
104
  [tool.cibuildwheel.linux]
91
105
  before-all = "/opt/python/cp312-cp312/bin/python {project}/scripts/build_opencv.py"
106
+ environment-pass = ["UV_EXCLUDE_NEWER"]
92
107
 
93
108
  [tool.cibuildwheel.macos]
94
109
  environment = { OpenCV_ROOT = "/tmp/opencv", MACOSX_DEPLOYMENT_TARGET = "11.0" }
@@ -108,7 +123,7 @@ cache-keys = [
108
123
  [tool.ruff]
109
124
  extend-exclude = ["patchmatch/csrc"]
110
125
  line-length = 88
111
- target-version = "py310"
126
+ target-version = "py311"
112
127
 
113
128
  [tool.ruff.lint]
114
129
  select = ["B", "C4", "E", "F", "I", "RUF", "SIM", "UP", "W"]
@@ -138,7 +153,10 @@ warn_return_any = false
138
153
 
139
154
  [tool.pytest.ini_options]
140
155
  addopts = ["--cov-branch", "--cov-report=term:skip-covered", "--cov=patchmatch"]
156
+ # shows which test hangs before the job timeout kills the run
157
+ faulthandler_timeout = 300
141
158
  markers = ["e2e: end-to-end tests of the user workflows (deselect with -m 'not e2e')"]
142
- minversion = "6.0"
159
+ minversion = "9.0"
143
160
  python_files = ["test_*.py"]
161
+ strict = true
144
162
  testpaths = ["tests"]
@@ -4,6 +4,9 @@ The patchmatch library only needs opencv_core. Linking it statically keeps the
4
4
  wheels small and free of OpenCV's GUI/codec dependencies.
5
5
 
6
6
  Usage: python build_opencv.py (installs into $OpenCV_ROOT)
7
+
8
+ PATCHMATCH_OPENCV_VERSION selects another pinned version, e.g. 4.14.0 to rebuild the
9
+ wheels of PyPatchMatch 2.0.0.
7
10
  """
8
11
 
9
12
  import hashlib
@@ -17,13 +20,20 @@ import tempfile
17
20
  import urllib.request
18
21
  from pathlib import Path
19
22
 
20
- OPENCV_VERSION = "4.14.0"
23
+ # The wheels link this code statically, so every archive is pinned by its SHA-256,
24
+ # e.g. from `sha256sum` or `shasum -a 256`.
25
+ OPENCV_SHA256S = {
26
+ "4.14.0": "ee8fb9b30eb60850431b4656447080e3737b56e45719c92b67f245950609f86e",
27
+ "5.0.0": "b0528f5a1d379d59d4701cb28c36e22214cc51cf64594e5b56f2d3e6c0233095",
28
+ }
29
+ # the version of the release wheels
30
+ OPENCV_VERSION = os.environ.get("PATCHMATCH_OPENCV_VERSION") or "5.0.0"
31
+ if OPENCV_VERSION not in OPENCV_SHA256S:
32
+ sys.exit(f"OpenCV {OPENCV_VERSION} is not pinned, known: {sorted(OPENCV_SHA256S)}")
33
+ OPENCV_SHA256 = OPENCV_SHA256S[OPENCV_VERSION]
21
34
  OPENCV_URL = (
22
35
  f"https://github.com/opencv/opencv/archive/refs/tags/{OPENCV_VERSION}.tar.gz"
23
36
  )
24
- # The wheels link this code statically, so the archive is pinned by its SHA-256.
25
- # Update it together with the version, e.g. with `sha256sum` or `shasum -a 256`.
26
- OPENCV_SHA256 = "ee8fb9b30eb60850431b4656447080e3737b56e45719c92b67f245950609f86e"
27
37
 
28
38
  CMAKE_OPTIONS = {
29
39
  "CMAKE_BUILD_TYPE": "Release",
@@ -63,6 +73,8 @@ CMAKE_OPTIONS = {
63
73
  "WITH_QT": "OFF",
64
74
  "WITH_TIFF": "OFF",
65
75
  "WITH_V4L": "OFF",
76
+ # an installed VTK adds OpenGL to the link interface of core (OpenCV 5)
77
+ "WITH_VTK": "OFF",
66
78
  "WITH_WEBP": "OFF",
67
79
  "WITH_IPP": "OFF",
68
80
  "WITH_ITT": "OFF",
@@ -80,6 +92,8 @@ WINDOWS_CMAKE_OPTIONS = {
80
92
  "OPENCV_CONFIG_INSTALL_PATH": "cmake",
81
93
  "OPENCV_INSTALL_BINARIES_PREFIX": "",
82
94
  "OPENCV_SKIP_CMAKE_ROOT_CONFIG": "ON",
95
+ # skips OpenCV 5's assembler check: MinGW's would set MINGW and link pthread
96
+ "CMAKE_ASM_COMPILER": "CMAKE_ASM_COMPILER-NOTFOUND",
83
97
  }
84
98
 
85
99
 
@@ -149,21 +163,22 @@ def main() -> None:
149
163
  return
150
164
 
151
165
  cmake = find_cmake()
152
- with tempfile.TemporaryDirectory() as tmp:
153
- archive = Path(tmp) / "opencv.tar.gz"
166
+ with tempfile.TemporaryDirectory() as tmp_name:
167
+ # OpenCV 5 compares paths as strings, which fails with 8.3 names (RUNNER~1)
168
+ tmp = Path(tmp_name).resolve()
169
+ archive = tmp / "opencv.tar.gz"
154
170
  print(f"Downloading {OPENCV_URL}", flush=True)
155
171
  urllib.request.urlretrieve(OPENCV_URL, archive)
156
172
  verify_archive(archive)
157
173
  with tarfile.open(archive) as tar:
158
- # extraction filters are missing on older patch releases, e.g. the
159
- # last Windows installer of Python 3.10 (3.10.11)
174
+ # extraction filters are missing on older patch releases (3.11 < 3.11.4)
160
175
  if hasattr(tarfile, "data_filter"):
161
176
  tar.extractall(tmp, filter="data")
162
177
  else:
163
178
  tar.extractall(tmp)
164
179
 
165
- source = Path(tmp) / f"opencv-{OPENCV_VERSION}"
166
- build = Path(tmp) / "build"
180
+ source = tmp / f"opencv-{OPENCV_VERSION}"
181
+ build = tmp / "build"
167
182
  options = {**CMAKE_OPTIONS, "CMAKE_INSTALL_PREFIX": prefix.as_posix()}
168
183
  if sys.platform == "win32":
169
184
  options.update(WINDOWS_CMAKE_OPTIONS)
@@ -0,0 +1,36 @@
1
+ """Render examples/images/readme_example.jpg, the picture at the top of the README.
2
+
3
+ It runs examples/py_example.py and shows, from left to right, the photo forest.bmp,
4
+ the input forest_pruned.bmp with the birdhouses painted out in white, and the result.
5
+ The PyPI description links the picture at the release tag, so rerun the script and
6
+ commit the picture after changes that affect the fills.
7
+
8
+ Usage:
9
+ uv run python scripts/render_readme_example.py
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import runpy
15
+ from pathlib import Path
16
+
17
+ import numpy as np
18
+ from PIL import Image
19
+
20
+ import patchmatch
21
+
22
+ EXAMPLES = Path(__file__).resolve().parents[1] / "examples"
23
+ IMAGES = EXAMPLES / "images"
24
+
25
+ if __name__ == "__main__":
26
+ patchmatch.set_random_seed(0)
27
+ runpy.run_path(str(EXAMPLES / "py_example.py"), run_name="__main__")
28
+ panels = [
29
+ np.asarray(Image.open(IMAGES / f"forest{suffix}.bmp"))
30
+ for suffix in ("", "_pruned", "_recovered")
31
+ ]
32
+ gap = np.full((panels[0].shape[0], 8, 3), 255, dtype=np.uint8)
33
+ picture = np.hstack([panels[0], gap, panels[1], gap, panels[2]])
34
+ Image.fromarray(picture).save(
35
+ IMAGES / "readme_example.jpg", quality=85, optimize=True
36
+ )
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from hypothesis import settings
4
+
5
+ # weekly CI; print_blob shows how to reproduce a failure locally
6
+ settings.register_profile("deep", max_examples=5000, print_blob=True)