PyPatchMatch 2.0.0rc4__tar.gz → 2.0.0rc5__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 (26) hide show
  1. pypatchmatch-2.0.0rc5/CLAUDE.md +436 -0
  2. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/CMakeLists.txt +18 -9
  3. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/PKG-INFO +12 -8
  4. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/README.md +9 -5
  5. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/patchmatch/_lib.py +1 -9
  6. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/patchmatch/_version.py +3 -3
  7. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/patchmatch/csrc/inpaint.cpp +137 -123
  8. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/patchmatch/csrc/inpaint.h +7 -5
  9. pypatchmatch-2.0.0rc5/patchmatch/csrc/masked_image.cpp +175 -0
  10. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/patchmatch/csrc/masked_image.h +17 -34
  11. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/patchmatch/csrc/nnf.cpp +69 -69
  12. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/patchmatch/csrc/nnf.h +25 -16
  13. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/patchmatch/csrc/pyinterface.cpp +14 -49
  14. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/patchmatch/csrc/pyinterface.h +5 -13
  15. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/patchmatch/patch_match.py +15 -6
  16. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/pyproject.toml +26 -5
  17. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/scripts/build_opencv.py +36 -0
  18. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/scripts/evaluate_inpainting.py +85 -17
  19. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/tests/test_e2e.py +28 -12
  20. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/tests/test_patch_match.py +82 -15
  21. pypatchmatch-2.0.0rc5/tests/test_properties.py +95 -0
  22. pypatchmatch-2.0.0rc4/patchmatch/csrc/masked_image.cpp +0 -175
  23. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/LICENSE +0 -0
  24. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/examples/images/forest_pruned.bmp +0 -0
  25. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/patchmatch/__init__.py +0 -0
  26. {pypatchmatch-2.0.0rc4 → pypatchmatch-2.0.0rc5}/patchmatch/py.typed +0 -0
@@ -0,0 +1,436 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in
4
+ this repository.
5
+
6
+ ## Project
7
+
8
+ PyPatchMatch implements PatchMatch based image inpainting (Barnes et al., SIGGRAPH 2009):
9
+ a C++17 library on top of OpenCV core, and a thin Python package that loads it through
10
+ `ctypes`. It is published on PyPI as `PyPatchMatch` (import name `patchmatch`) and used
11
+ by InvokeAI. This repository (`mauwii/PyPatchMatch`) is the maintained fork of
12
+ `invoke-ai/PyPatchMatch`, which derives from `vacancy/PyPatchMatch` and
13
+ `younesse-cv/PatchMatch`.
14
+
15
+ ## Commands
16
+
17
+ The project is managed with uv (`.python-version` pins 3.12 for the local venv). Building
18
+ needs a C++17 compiler, CMake >= 3.21 and an OpenCV that `find_package(OpenCV)` finds:
19
+ Homebrew `opencv`, `libopencv-dev`, or an installation prefix in `OpenCV_ROOT`.
20
+
21
+ ```sh
22
+ uv sync # .venv, editable install, compiles the library
23
+ uv run pre-commit install
24
+ uv run pytest # all tests, with the coverage gate
25
+ uv run pytest -m "not e2e" # fast unit tests only
26
+ uv run pytest -m e2e --no-cov # end-to-end tests only
27
+ uv run pytest tests/test_patch_match.py::test_inpaint_global_mask --no-cov
28
+ uv run pytest -k regularity --no-cov
29
+ uv run mypy # strict, also a pre-commit hook
30
+ uv run pre-commit run --all-files # what the CI lint job runs
31
+ uv run ruff check --fix . && uv run ruff format .
32
+ uv build # sdist and wheel into dist/
33
+ ```
34
+
35
+ - Pass `--no-cov` for any partial run: `addopts` always enables coverage, and
36
+ `fail_under = 95` (branch coverage, subprocesses included through
37
+ `patch = ["subprocess"]`) fails the run otherwise.
38
+ - Use `uv run pytest`, not `python -m pytest`. The latter puts the repository root on
39
+ `sys.path`, which imports the source tree `patchmatch/` without the compiled library;
40
+ the symptoms are the warning `patchmatch failed to load: libpatchmatch.* not found`
41
+ and `RuntimeError: the patchmatch native library is not available` in every test.
42
+ - `--cov=patchmatch` measures the source tree, so coverage only works with the editable
43
+ install that `uv sync` creates. With a non-editable install (`uv sync --no-editable`,
44
+ `uv pip install .`) it reports 0 % and the gate fails.
45
+ - `uv sync` rebuilds the library only when a `tool.uv.cache-keys` entry changes
46
+ (`CMakeLists.txt`, `patchmatch/csrc/*`, `pyproject.toml`, git commit or tags). Force it
47
+ with `uv sync --reinstall-package pypatchmatch`.
48
+ - CI builds the library with warnings as errors. Locally:
49
+ `SKBUILD_CMAKE_DEFINE=CMAKE_COMPILE_WARNING_AS_ERROR=ON uv sync --reinstall-package pypatchmatch`.
50
+
51
+ Build against the same static, core-only OpenCV as the release wheels:
52
+
53
+ ```sh
54
+ export OpenCV_ROOT=/tmp/opencv
55
+ uv run --no-project python scripts/build_opencv.py # pinned version and SHA-256
56
+ uv sync --reinstall-package pypatchmatch
57
+ ```
58
+
59
+ ASan and UBSan, like the CI job `sanitizers` (Linux):
60
+
61
+ ```sh
62
+ SKBUILD_CMAKE_DEFINE=PATCHMATCH_SANITIZE=ON uv sync --reinstall-package pypatchmatch
63
+ ASAN="$(gcc -print-file-name=libasan.so) $(gcc -print-file-name=libstdc++.so)"
64
+ ASAN_OPTIONS=detect_leaks=0 uv run env LD_PRELOAD="$ASAN" pytest --no-cov --capture=sys
65
+ uv sync --reinstall-package pypatchmatch # back to the normal library
66
+ ```
67
+
68
+ - Python is not instrumented, so the ASan runtime has to be loaded at startup; without
69
+ it every test aborts with `Interceptors are not working`. On Linux `libstdc++` has to
70
+ follow it: ASan looks up the real `__cxa_throw` at startup, and without it the first
71
+ C++ exception aborts with `CHECK failed: ... real___cxa_throw`. The leak check would
72
+ report the interpreter's own allocations.
73
+ - `--capture=sys` is required: pytest's default capture of the file descriptors keeps
74
+ the report of an abort, and only `Fatal Python error: Aborted` reaches the terminal.
75
+ - On macOS, inject the runtime with
76
+ `DYLD_INSERT_LIBRARIES="$(xcrun clang -print-resource-dir)/lib/darwin/libclang_rt.asan_osx_dynamic.dylib"`,
77
+ add `strip_env=0` to `ASAN_OPTIONS` (ASan removes the variable, and the tests that
78
+ start a subprocess abort), and start pytest as `.venv/bin/python .venv/bin/pytest`:
79
+ SIP drops the variable for `/usr/bin/env` and `/bin/sh`, which uv uses as the
80
+ shebang of scripts in long paths.
81
+ - The run takes about 80 s locally. Calling `PM_inpaint` through ctypes with a shape
82
+ larger than the buffer (ASan) or a `patch_size` of `2**31 - 1` (UBSan, `nnf.cpp`)
83
+ shows that both report and abort.
84
+
85
+ C++ coverage, like the CI job `coverage` (gcovr comes from the `coverage` group):
86
+
87
+ ```sh
88
+ SKBUILD_CMAKE_DEFINE=PATCHMATCH_COVERAGE=ON uv sync --reinstall-package pypatchmatch
89
+ find build -name '*.gcda' -delete # the counts add up across runs
90
+ uv run --group coverage pytest --no-cov
91
+ uv run --group coverage gcovr --filter patchmatch/csrc/ --print-summary \
92
+ --exclude-throw-branches --exclude-unreachable-branches \
93
+ --gcov-suspicious-hits-threshold 0
94
+ uv sync --reinstall-package pypatchmatch # back to the normal library
95
+ ```
96
+
97
+ - The Python tests are the C++ tests: there is no C++ test framework, the coverage of
98
+ the native code is measured while they run.
99
+ - The build uses `-Og` with atomic counters (a test inpaints in several threads at once);
100
+ the run takes about 90 s locally. `-O0` reports one more line (the masked branch of
101
+ `MaskedImage::upsample`, which `-Og` merges) but triples the time. The distance loop
102
+ runs more than 2**32 times, which gcovr rejects as a gcov bug without
103
+ `--gcov-suspicious-hits-threshold 0`.
104
+ - On macOS add `--gcov-executable "xcrun llvm-cov gcov"`. Clang's counts are not
105
+ reliable: they report a closing brace after a `return` as a missed line, and hundreds
106
+ of millions of returns through the clamp at the end of `distance_masked_images`,
107
+ which recomputing the distances showed to be unreachable. Judge by GCC (the CI job,
108
+ or Docker with `ubuntu:24.04`).
109
+ - Missed on purpose: the `catch (...)` blocks of `guarded()` and `set_last_error()`,
110
+ the `clone()` of a non-continuous result in `_cv2_to_py`, the clamps at the end of
111
+ `distance_masked_images` and for targets outside the image in
112
+ `RegularityGuidedPatchDistanceMetricV2`, and the masked branch of
113
+ `MaskedImage::upsample` (only called on targets, which have no holes). GCC also
114
+ reports the closing braces of the functions that return a `MaskedImage`, where it
115
+ puts the cleanup for exceptions.
116
+ - GCC 13 (Docker arm64, `-Og`): 97.0 % of the lines, 95.2 % of the branches. The gates
117
+ are these values rounded down to 5 % with at least two points of margin.
118
+
119
+ Fill quality (`scripts/evaluate_inpainting.py`, its docstring explains the columns):
120
+
121
+ ```sh
122
+ uv run --no-sync python scripts/evaluate_inpainting.py --baseline <library of main>
123
+ uv run --no-sync python scripts/evaluate_inpainting.py --patch-size 15 --seeds 1 \
124
+ --cases forest,brick
125
+ ```
126
+
127
+ - It cuts holes into `forest.bmp` and the CC0 images of `examples/images` (sources in
128
+ `SOURCES.md`) and adds `forest_pruned.bmp` and every `*_pruned.*` of the git-ignored
129
+ `examples/images/local/` as object removals without a ground truth. That folder is
130
+ for your own images; never commit them.
131
+ - For `--baseline`, build the library of `main` outside the checkout:
132
+
133
+ ```sh
134
+ mkdir -p /tmp/main-lib
135
+ git archive main CMakeLists.txt patchmatch/csrc | tar -x -C /tmp/main-lib
136
+ cmake -S /tmp/main-lib -B /tmp/main-lib/build -DCMAKE_BUILD_TYPE=Release
137
+ cmake --build /tmp/main-lib/build # libpatchmatch.so, .dylib or patchmatch.dll
138
+ ```
139
+
140
+ - Judge changes on all cases and look at the fills in `examples/images/evaluation.html`
141
+ (git-ignored, it embeds the images of `examples/images/local/` too): fixes for one
142
+ image regressed others before.
143
+
144
+ Other entry points:
145
+
146
+ - `examples/cpp_example_run.sh` builds the library and `examples/cpp_example.cpp` with
147
+ CMake into `build/cpp` and runs it. It needs OpenCV `imgcodecs`, so a full OpenCV, not
148
+ the core-only build of the script.
149
+ - `uv run python examples/py_example.py` (and `py_example_global_mask.py`) write
150
+ `examples/images/forest_recovered.bmp`, which is git-ignored.
151
+ - CMake options: `PATCHMATCH_BUILD_EXAMPLES` (OFF), `PATCHMATCH_FAST_MATH` (ON,
152
+ `-ffast-math` or `/fp:fast`), `PATCHMATCH_SANITIZE` (OFF, ASan and UBSan with
153
+ `float-cast-overflow`, every finding aborts; turns off fast math, which lets the
154
+ compiler drop checks; GCC and Clang only), `PATCHMATCH_COVERAGE` (OFF, gcov counters
155
+ at `-Og`; GCC and Clang only).
156
+ - The fallback wheel of the CI job `sdist-fallback`:
157
+ `uv build --wheel dist/<sdist>.tar.gz --out-dir fallback -C cmake.define.CMAKE_DISABLE_FIND_PACKAGE_OpenCV=ON`
158
+ must produce a `py3-none-any` wheel with the Python package but without the library.
159
+
160
+ ## Architecture
161
+
162
+ ### Layers, in the call order of `patchmatch.inpaint`
163
+
164
+ 1. `patchmatch/__init__.py` re-exports the public API of `patch_match.py` and
165
+ `__version__` from the generated `_version.py`. `test_top_level_exports` checks that
166
+ every name in `patch_match.__all__` is re-exported.
167
+ 2. `patchmatch/patch_match.py` is the public API. It validates and canonicalizes the
168
+ inputs (`_prepare_inputs`, `_check_patch_size`, the `ijmap` and `guide_weight`
169
+ checks), picks the entry point (`PM_inpaint`, `PM_inpaint2` with a global mask, and
170
+ their `_regularity` variants) and runs it through `_call`. It loads the library at
171
+ import time; on `OSError` it logs a warning and sets `patchmatch_available = False`,
172
+ and every function then raises `RuntimeError` through `_get_lib()`.
173
+ 3. `patchmatch/_lib.py` is the ctypes layer: the `CShapeT`/`CMatT` structures, the dtype
174
+ table, `np_to_pymat`/`pymat_to_np`, `find_library` (looks for `libpatchmatch.so`,
175
+ `libpatchmatch.dylib` or `patchmatch.dll` in every directory of `patchmatch.__path__`)
176
+ and `load_library` (argtypes and restype of every `PM_*` function).
177
+ 4. `patchmatch/csrc/pyinterface.{h,cpp}` is the `extern "C"` API: the `PM_*` functions,
178
+ the conversion between `PM_mat_t` and `cv::Mat`, the global settings and the error
179
+ reporting.
180
+ 5. `csrc/inpaint.{h,cpp}`: `Inpainting`, the image pyramid and the EM loop.
181
+ 6. `csrc/nnf.{h,cpp}`: `NearestNeighborField` (propagation and random search) and the
182
+ distance metrics `PatchSSDDistanceMetric` and `RegularityGuidedPatchDistanceMetricV2`.
183
+ 7. `csrc/masked_image.{h,cpp}`: `MaskedImage`, an image with its hole mask and optional
184
+ global mask, down- and upsampling, gradients.
185
+
186
+ The C++ headers double as the C++ API: `examples/cpp_example.cpp` uses `Inpainting` and
187
+ `PatchSSDDistanceMetric` directly.
188
+
189
+ ### ctypes instead of a CPython extension
190
+
191
+ The library does not use the CPython ABI, so the wheels are tagged
192
+ `py3-none-<platform>` (`wheel.py-api = "py3"`): one wheel per platform serves all Python
193
+ versions, and cibuildwheel only builds `cp310-*`. Keep `csrc` free of `Python.h`,
194
+ pybind11 or nanobind. The C++ sources are excluded from the wheel (`wheel.exclude`);
195
+ CMake installs the shared library into the package directory.
196
+
197
+ ### The C interface contract
198
+
199
+ A change on one side has to be mirrored on the other:
200
+
201
+ - `PM_mat_t`/`PM_shape_t` in `pyinterface.h` and `CMatT`/`CShapeT` in `_lib.py` (field
202
+ order and types).
203
+ - The order of `PM_dtype_e` and of the `_PYMAT_DTYPES` list: uint8 and float32, the
204
+ only types the native code reads; `_py_to_cv2` rejects others.
205
+ - Every `PM_*` function needs its `argtypes` and `restype` in `load_library()`; without
206
+ them ctypes silently assumes `int`.
207
+ - Symbols are exported by `extern "C"` and `CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS`, there are
208
+ no export macros.
209
+
210
+ A new entry point is declared in `pyinterface.h`, implemented in `pyinterface.cpp`
211
+ inside `guarded(...)`, registered in `load_library()` and called through `_call()`.
212
+
213
+ Memory ownership: `np_to_pymat` wraps a C-contiguous array without copying it, so
214
+ callers convert with `np.ascontiguousarray` first and keep the array alive during the
215
+ call. `_py_to_cv2` clones it into a `cv::Mat`, so the inputs are never modified.
216
+ `_cv2_to_py` hands a heap buffer (a released `std::unique_ptr<unsigned char[]>`) to
217
+ Python; `pymat_to_np` copies it, and `_call` always returns it through `PM_free_pymat` in
218
+ a `finally`. It allocates at least one byte, because a null `data_ptr` signals failure.
219
+
220
+ Errors: a C++ exception that reaches ctypes aborts the process. `guarded()` catches
221
+ everything, stores the message in a thread-local string and returns a null `data_ptr`;
222
+ `_call` turns that into `RuntimeError("patchmatch failed: <PM_last_error()>")`. Most
223
+ input validation lives in Python because the native code crashes or hangs on bad input:
224
+ `patch_size` 0 crashes, negative sizes never terminate, sizes >= 2**31 wrap to negative
225
+ C ints, negative guide weights index outside the similarity table, NaN or infinity in the
226
+ `ijmap` produce out-of-range distances. `RegularityGuidedPatchDistanceMetricV2` also
227
+ throws on a negative weight, for C++ callers.
228
+
229
+ ### Threading and determinism
230
+
231
+ ctypes releases the GIL during native calls, so inpaintings run truly concurrently
232
+ (`test_concurrent_inpaint_is_deterministic`). Therefore:
233
+
234
+ - The seed and the verbose flag are `std::atomic` globals, read when a call starts.
235
+ - The random generator is a function-local `thread_local std::mt19937`, seeded at the
236
+ start of every `Inpainting::run`, so each call is reproducible from the seed on any
237
+ thread.
238
+ - `random_int` uses `engine() % n` instead of `std::uniform_int_distribution` to keep
239
+ the sequence identical on all platforms and standard libraries. Keep it that way.
240
+ - Lookup tables are function-local statics (thread-safe initialization). Do not add
241
+ mutable shared state.
242
+
243
+ ### Algorithm
244
+
245
+ - `patch_size` is a radius: patches span `(2 * patch_size + 1)` pixels in each
246
+ direction. The pyramid halves the image until one side is <= `patch_size`.
247
+ - From the coarsest level to the finest, two nearest-neighbor fields are kept:
248
+ source to target (completeness) and target to source (coherence). They start random
249
+ at the coarsest level and are upscaled from the previous level afterwards, keeping the
250
+ offset within the coarse pixel so that neighbors still point to neighbors.
251
+ - Each level runs `1 + 2 * level` EM iterations with `min(7, 1 + level)` NNF passes
252
+ (propagation and random search), an expectation step (votes of both fields, weighted
253
+ through the `distance2similarity` table) and a maximization step, which sets each
254
+ pixel to the weighted mean of its votes. On levels 0 and 1 the known pixels keep their
255
+ input values instead; without that the votes change the pixels around the holes, and a
256
+ seam appears when `run()` restores them. On coarser levels the known pixels next to
257
+ the holes are averages of partially masked kernels, and keeping them made fills copy
258
+ smooth regions into textured ones (57 % instead of 17 % of the fill in one case). The
259
+ last iteration of a level votes directly into the upsampled image of the next level,
260
+ which is less blurry than upsampling the result. The last iteration at level 0 keeps
261
+ only the heaviest vote of each pixel instead of the mean, which blurs.
262
+ - The coarse levels mark a pixel as a hole only if its whole downsampling kernel is
263
+ one, so the holes shrink quickly and are filled smoothly from their border; the finer
264
+ levels add the texture. Marking every pixel that touches a hole instead was tried and
265
+ made the fills worse: it leaves the coarsest levels without valid source patches,
266
+ because the similarity table gives zero weight to patches with more than about 10 %
267
+ masked or border pixels.
268
+ - A hole, and later each thin part of it, therefore has a level on which it appears for
269
+ the first time. The patches around it arrive there with the link of a patch without
270
+ holes, the one to itself, which now points into the hole. `_initialize_field_from`
271
+ runs a random search around such a patch before the first pass. Otherwise the first
272
+ propagation replaces the link with the one of a neighbor, the random search goes on
273
+ around that link, and with a radius of half the smaller image side it does not find
274
+ the way back to the surroundings of the patch: the stem of `forest_pruned.bmp` was
275
+ filled with forest instead of meadow in 31 of 200 seeds (36 of 48 with `patch_size`
276
+ 7), and the `meadow` case had an error above 12 in 9 of 96 seeds, now at most 9.7
277
+ (`test_inpaint_thin_hole_takes_the_colors_beside_it`). Tried and rejected: searching
278
+ before propagating in every pass, which no longer refines a propagated link, the way
279
+ a structure is followed along a hole (brick error +9 %); and a search radius of the
280
+ larger image side, as in the paper, which blurs the fills (coffee detail 0.30 instead
281
+ of 0.53).
282
+ - Distances are ints in `[0, PatchDistanceMetric::kDistanceScale]` (65535): an SSD over
283
+ the colors and the x/y gradients, where masked pixels and pixels at the border count
284
+ as maximal. The distance indexes `distance2similarity`, so every metric has to clamp
285
+ its result to that range.
286
+ - Mask semantics: `mask` non-zero marks the holes (by default all pure white pixels,
287
+ computed in Python by `_default_mask`); `global_mask` marks pixels that are neither
288
+ filled nor used as a patch source, and whose colors must not influence the fill: the
289
+ downsampling kernel skips them, and gradients next to them are neutral
290
+ (`test_colors_under_global_mask_do_not_matter`). At the end of `run()` every pixel
291
+ that is not a hole, including the globally masked ones, is copied back from the input,
292
+ so only the holes change. The tests assert this invariant (`assert_filled`,
293
+ `assert_plausible_fill`).
294
+
295
+ ### OpenCV usage and library size
296
+
297
+ The release wheels link a static, core-only OpenCV (`scripts/build_opencv.py`:
298
+ `BUILD_LIST=core`, every optional backend off). For changes to the C++ code:
299
+
300
+ - The library links only `opencv_core`; `imgcodecs` is only used by the example.
301
+ - Use plain pixel loops instead of `cv::Mat` matrix expressions such as `a == b` or
302
+ `m1 | m2`: they pull OpenCV's expression module, and with it `FileStorage`, PCA and
303
+ `SparseMat` code, into the static library, which grew by about 40 % before commit
304
+ e50c613 replaced them.
305
+ - On Windows the static OpenCV uses the static MSVC runtime (`BUILD_WITH_STATIC_CRT`),
306
+ so the library does too (`MSVC_RUNTIME_LIBRARY` in `CMakeLists.txt`). Its stderr is
307
+ then not captured by pytest's `capfd`, so tests of native output run in a subprocess
308
+ (`test_set_verbose_prints_progress`).
309
+ - With a static OpenCV, CMake installs its licenses, which the build script collects in
310
+ `<prefix>/licenses`, into `.dist-info/licenses/opencv` of the wheel, and the CycloneDX
311
+ SBOM it writes to `<prefix>/sboms/opencv.cdx.json` into `.dist-info/sboms` (PEP 770).
312
+ The static library hides OpenCV from scanners; grype matches the SBOM through its CPE.
313
+ - The SBOM leaves out OpenCV's bundled zlib: only `FileStorage` uses it, and none of it
314
+ is linked into the library. The wheel job checks that every wheel has the SBOM and,
315
+ except on Windows, that `nm` finds no zlib symbol in the library. `write_sbom` fails
316
+ if the build bundles another 3rdparty library; add a new one to the SBOM.
317
+ - To update OpenCV, change `OPENCV_VERSION` and `OPENCV_SHA256` together. The CI cache of
318
+ the OpenCV build is keyed on the hash of the script.
319
+
320
+ ### Packaging and versioning
321
+
322
+ - Build backend: scikit-build-core with setuptools-scm. The version comes from the git
323
+ tags; `patchmatch/_version.py` is generated and git-ignored. Without git metadata the
324
+ version falls back to `0.0.0`, so CI checks out with `fetch-depth: 0`.
325
+ - The release wheels are built from the sdist, not from the checkout, and cibuildwheel
326
+ runs `pytest {project}/tests --no-cov` against the sdist contents. Everything the tests
327
+ need has to be in the sdist: `sdist.include` adds `examples/images/forest_pruned.bmp`,
328
+ the rest of `examples/` is excluded. New test data has to be added there as well.
329
+ - Fallback: if a build from the sdist fails (e.g. no compiler, CMake or OpenCV on a
330
+ platform without a wheel), the `tool.scikit-build.overrides` entry builds a pure wheel
331
+ without the library, so the installation succeeds with `patchmatch_available` False and
332
+ the error message of `find_library` explains how to reinstall. Builds from the source
333
+ tree still fail. The CI job `sdist-fallback` and `test_import_without_native_library`
334
+ keep this path working.
335
+ - Wheels: manylinux x86_64 and aarch64, macOS arm64 and x86_64 (deployment target 11.0),
336
+ Windows x64. musllinux is skipped.
337
+
338
+ ## CI
339
+
340
+ - `ci.yml`: `lint` runs `pre-commit run --all-files`; `test` runs `uv run pytest` on
341
+ Ubuntu, macOS and Windows with Python 3.10 and 3.14, after building (or restoring) the
342
+ OpenCV of the build script.
343
+ - pre-commit runs, besides the file checks, ruff and uv-lock: clang-format
344
+ (`.clang-format`), shellcheck, markdownlint-cli2 (`.markdownlint.yaml`, 88 columns),
345
+ typos (fixes in place), actionlint (checks `run:` scripts with shellcheck only if it
346
+ is on `PATH`) and zizmor (offline, default persona), and mypy as a local hook
347
+ (`uv run --locked --no-build --only-group dev mypy`, like the `lint` job, so the
348
+ library is not built; numpy and pillow are in the `dev` group for their types).
349
+ MegaLinter was considered and rejected: it duplicates pre-commit in a multi-GB Docker
350
+ image and bundles the linter versions instead of pinning each hook for Dependabot.
351
+ - Dependabot waits 7 days after a release (`cooldown`, required by zizmor).
352
+ - `test` sets `SKBUILD_CMAKE_DEFINE=CMAKE_COMPILE_WARNING_AS_ERROR=ON`, so the warning
353
+ flags of `CMakeLists.txt` (`-Wall -Wextra -Wpedantic -Wshadow`, `/W4` on MSVC) fail the
354
+ build. Only there: builds from the sdist and the wheels must not break when a newer
355
+ compiler adds a warning. The OpenCV headers are system includes and do not count.
356
+ - `sanitizers` builds with `PATCHMATCH_SANITIZE=ON` on Ubuntu and runs the tests with
357
+ the preloaded ASan runtime (see Commands). It reuses the checkout and OpenCV steps of
358
+ `test` through YAML anchors, so change them there.
359
+ - `coverage` builds with `PATCHMATCH_COVERAGE=ON` on Ubuntu, runs the tests and fails
360
+ below 95 % of the C++ lines or 90 % of the branches (gcovr, see Commands).
361
+ It reuses the same anchors. It also writes `coverage.xml` (Python) and
362
+ `coverage-cpp.xml` (`gcovr --sonarqube`) and exports `build/compile_commands.json`,
363
+ and then runs the SonarCloud scan (`sonar-project.properties`, `SONAR_TOKEN`, skipped
364
+ without the secret, e.g. for Dependabot and forks). Automatic analysis in SonarCloud
365
+ is disabled; it cannot import coverage and would conflict with the CI scan. `scripts/`
366
+ has no tests and is excluded from the coverage on new code (80 % in "Sonar way").
367
+ - The `uv run` steps pass `--locked` and `--no-build` (SonarCloud S8544, S8541).
368
+ `--no-build` only forbids building dependencies from source; uv still builds the
369
+ project itself. The OpenCV step runs with `--only-group dev`: the project cannot be
370
+ built before OpenCV is installed, and `--no-project` would lock nothing.
371
+ - `system-opencv` installs `libopencv-dev` on `ubuntu-24.04` (OpenCV 4.6) instead of
372
+ running the build script and runs the tests, because builds from the sdist link
373
+ whatever OpenCV the system has. The runner is pinned so that the OpenCV version only
374
+ changes on purpose.
375
+ - `wheels.yml`: sdist, then cibuildwheel on five runners, then `sdist-fallback`;
376
+ `publish` attests the build provenance of all files (`actions/attest`) and uploads
377
+ them to PyPI with trusted publishing (environment `pypi`) only for a published GitHub
378
+ release of `mauwii/PyPatchMatch`. The wheel jobs cache the OpenCV prefix, keyed on
379
+ `build_opencv.py` and `pyproject.toml` from the sdist (there is no checkout); on Linux
380
+ a volume makes `/tmp/opencv` of the container visible to the cache step. Runs on a
381
+ tag, and therefore releases, skip the cache and build OpenCV from scratch.
382
+ - `fill-quality.yml` runs for pull requests that change `CMakeLists.txt`,
383
+ `patchmatch/csrc` or the evaluation script: it builds the library of the base commit,
384
+ runs `scripts/evaluate_inpainting.py --baseline` against it (patch size 3, 3 seeds),
385
+ writes the table to the job summary (`--markdown`) and uploads `evaluation.html` as the
386
+ artifact `fill-quality`. It is a report, not a gate: the measures vary over the seeds,
387
+ so it is not in the `needs` of `ci-ok`. It repeats the OpenCV steps of `ci.yml` with
388
+ the same cache key, because anchors do not reach across files.
389
+ - `codeql.yml` analyzes actions, C/C++ and Python.
390
+ - `ci-ok` and `wheels-ok` (re-actors/alls-green) are the required status checks; add new
391
+ jobs to their `needs`.
392
+ - Actions are pinned to full commit SHAs with a `# vX.Y.Z` comment, and checkouts use
393
+ `persist-credentials: false`. Dependabot updates actions, uv and pre-commit weekly in
394
+ groups.
395
+
396
+ ## Conventions
397
+
398
+ - Python >= 3.10: keep `from __future__ import annotations`. Ruff targets py310 with the
399
+ rules B, C4, E, F, I, RUF, SIM, UP and W, and formats at 88 columns. `patchmatch/csrc`
400
+ is excluded from ruff. C++ is formatted by clang-format (Microsoft style, 120 columns,
401
+ includes grouped as own header, `<...>`, `"..."`); naming is not checked: `m_`
402
+ members, `k` constants. Commit a reformat separately and add its hash to
403
+ `.git-blame-ignore-revs`.
404
+ - The package ships `py.typed`; public functions are fully annotated. mypy checks
405
+ `patchmatch` and `tests` in strict mode (`[tool.mypy]`); the tests have to use the
406
+ API correctly but need no annotations of their own. A deliberately wrong argument
407
+ goes through a test parameter instead of a `# type: ignore`.
408
+ - Backwards compatibility: `from patchmatch import patch_match` and
409
+ `patch_match.CShapeT`/`CMatT` keep working (tested).
410
+ - Comments explain why: which crash a check prevents, why a construct is avoided.
411
+ - No `NOSONAR` markers: change the code so that the SonarCloud rule is satisfied, and ask
412
+ when that seems impossible. The only exception is `nnf.cpp`: cpp:S2245 flags every
413
+ declaration of `std::mt19937`, which only drives the randomized search.
414
+ - Tests check invariants instead of golden images: holes filled, all other pixels
415
+ unchanged, determinism with `set_random_seed`, fills that blend in with their
416
+ surroundings. `test_patch_match.py` and `test_e2e.py` seed through an autouse fixture.
417
+ Unit tests use a small synthetic image; the `e2e` tests use
418
+ `examples/images/forest_pruned.bmp` and mirror the workflows of the README and the
419
+ examples. `test_properties.py` checks the invariants on random inputs with Hypothesis
420
+ (in the `test` group, so the wheel tests run it too); it seeds inside the tests,
421
+ because Hypothesis rejects function-scoped fixtures, and has no deadline, because
422
+ the sanitizer and coverage builds are much slower.
423
+ - The README documents the behavior (mask semantics, `patch_size` as a radius, the
424
+ fallback installation); update it together with API or installation changes.
425
+ - Commit messages: imperative subject in sentence case without a prefix (e.g. "Reject
426
+ invalid patch sizes and empty regularity maps"), and a body that explains the reason,
427
+ with measurements where relevant. Changes go through PRs from branches such as
428
+ `fix/<topic>`.
429
+
430
+ ## Git and GitHub
431
+
432
+ - `origin` is `mauwii/PyPatchMatch`, where PRs and releases go; `upstream` is
433
+ `invoke-ai/PyPatchMatch`. `gh` resolves the default repository to upstream, so pass
434
+ `-R mauwii/PyPatchMatch`.
435
+ - Releases: creating a GitHub release with a tag like `v2.0.0` (or `v2.0.0rc2` for
436
+ pre-releases) builds the wheels and publishes them to PyPI.
@@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.21)
2
2
  project(patchmatch LANGUAGES CXX)
3
3
 
4
4
  option(PATCHMATCH_BUILD_EXAMPLES "Build the C++ example (requires OpenCV imgcodecs)" OFF)
5
- option(PATCHMATCH_WITH_HIGHGUI "Enable the debug visualization of Inpainting::run (requires OpenCV highgui)" OFF)
6
5
  option(PATCHMATCH_FAST_MATH "Compile with fast-math optimizations" ON)
7
6
  option(PATCHMATCH_SANITIZE "Build with AddressSanitizer and UndefinedBehaviorSanitizer, without fast math (GCC, Clang)" OFF)
7
+ option(PATCHMATCH_COVERAGE "Build with gcov coverage counters at -Og (GCC, Clang)" OFF)
8
8
 
9
9
  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
10
10
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
@@ -16,9 +16,6 @@ set(CMAKE_CXX_EXTENSIONS OFF)
16
16
  set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
17
17
 
18
18
  set(PATCHMATCH_OPENCV_COMPONENTS core)
19
- if(PATCHMATCH_WITH_HIGHGUI)
20
- list(APPEND PATCHMATCH_OPENCV_COMPONENTS imgproc highgui)
21
- endif()
22
19
  if(PATCHMATCH_BUILD_EXAMPLES)
23
20
  list(APPEND PATCHMATCH_OPENCV_COMPONENTS imgcodecs)
24
21
  endif()
@@ -65,9 +62,13 @@ if(PATCHMATCH_SANITIZE)
65
62
  target_link_options(patchmatch PRIVATE ${PATCHMATCH_SANITIZERS})
66
63
  endif()
67
64
 
68
- if(PATCHMATCH_WITH_HIGHGUI)
69
- target_compile_definitions(patchmatch PRIVATE PATCHMATCH_WITH_HIGHGUI)
70
- target_link_libraries(patchmatch PRIVATE opencv_imgproc opencv_highgui)
65
+ if(PATCHMATCH_COVERAGE)
66
+ if(MSVC)
67
+ message(FATAL_ERROR "PATCHMATCH_COVERAGE needs GCC or Clang")
68
+ endif()
69
+ # atomic counters, since a test inpaints in parallel; -O0 would triple the test time
70
+ target_compile_options(patchmatch PRIVATE --coverage -fprofile-update=atomic -Og)
71
+ target_link_options(patchmatch PRIVATE --coverage)
71
72
  endif()
72
73
 
73
74
  if(PATCHMATCH_BUILD_EXAMPLES)
@@ -81,8 +82,9 @@ install(TARGETS patchmatch
81
82
  RUNTIME DESTINATION patchmatch
82
83
  )
83
84
 
84
- # A static OpenCV becomes part of the library, so a wheel has to ship its licenses,
85
- # which scripts/build_opencv.py collects in <prefix>/licenses.
85
+ # A static OpenCV becomes part of the library, so a wheel has to ship its licenses and
86
+ # an SBOM (PEP 770), which scripts/build_opencv.py writes to <prefix>/licenses and
87
+ # <prefix>/sboms.
86
88
  if(DEFINED SKBUILD_METADATA_DIR AND NOT OpenCV_SHARED)
87
89
  set(PATCHMATCH_OPENCV_LICENSES "${OpenCV_INSTALL_PATH}/licenses")
88
90
  if(EXISTS "${PATCHMATCH_OPENCV_LICENSES}/opencv-LICENSE")
@@ -92,4 +94,11 @@ if(DEFINED SKBUILD_METADATA_DIR AND NOT OpenCV_SHARED)
92
94
  message(WARNING "No OpenCV licenses in ${PATCHMATCH_OPENCV_LICENSES}, "
93
95
  "the wheel will not include them")
94
96
  endif()
97
+ set(PATCHMATCH_OPENCV_SBOM "${OpenCV_INSTALL_PATH}/sboms/opencv.cdx.json")
98
+ if(EXISTS "${PATCHMATCH_OPENCV_SBOM}")
99
+ install(FILES "${PATCHMATCH_OPENCV_SBOM}" DESTINATION "${SKBUILD_METADATA_DIR}/sboms")
100
+ else()
101
+ message(WARNING "No OpenCV SBOM at ${PATCHMATCH_OPENCV_SBOM}, "
102
+ "the wheel will not include it")
103
+ endif()
95
104
  endif()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPatchMatch
3
- Version: 2.0.0rc4
3
+ Version: 2.0.0rc5
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>
@@ -24,8 +24,8 @@ 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
26
  Requires-Python: >=3.10
27
- Requires-Dist: numpy
28
- Requires-Dist: pillow
27
+ Requires-Dist: numpy>=1.21.6
28
+ Requires-Dist: pillow>=8.4.0
29
29
  Description-Content-Type: text/markdown
30
30
 
31
31
  # PatchMatch based Inpainting
@@ -82,7 +82,8 @@ their values. Holes need hard edges: an anti-aliased brush leaves light pixels a
82
82
  the edge of its strokes that are not pure white, so they count as known and the fill
83
83
  continues them. Paint without anti-aliasing or pass a mask that covers those pixels as
84
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 (see
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
86
87
  [examples/py_example_global_mask.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example_global_mask.py)).
87
88
  `patchmatch.patchmatch_available` tells whether the native library could be loaded.
88
89
  The previous import path `from patchmatch import patch_match` keeps working.
@@ -114,8 +115,7 @@ int main() {
114
115
  }
115
116
  ```
116
117
 
117
- The library is built with CMake; `PATCHMATCH_BUILD_EXAMPLES` builds the example and
118
- `PATCHMATCH_WITH_HIGHGUI` enables the debug visualization of `Inpainting::run`.
118
+ The library is built with CMake; `PATCHMATCH_BUILD_EXAMPLES` builds the example.
119
119
 
120
120
  ## Development
121
121
 
@@ -133,9 +133,12 @@ The evaluation cuts holes into the images of `examples/images`, compares the fil
133
133
  the original content and writes `examples/images/evaluation.html`, which shows them side
134
134
  by side. `--baseline` evaluates another build of the library as well, e.g. of the main
135
135
  branch; the docstring of the script explains the columns.
136
+ Pull requests that change the C++ code get this comparison with their base commit in the
137
+ summary of the workflow "Fill quality", and the HTML report as its artifact.
136
138
 
137
139
  Releases are published to PyPI by creating a GitHub release; the version is taken from
138
- its tag (e.g. `v2.0.0`).
140
+ its tag (e.g. `v2.0.0`). Every released wheel and sdist carries a build provenance
141
+ attestation: `gh attestation verify <file> -R mauwii/PyPatchMatch`.
139
142
 
140
143
  ## License
141
144
 
@@ -143,7 +146,8 @@ PyPatchMatch is released under the
143
146
  [MIT License](https://github.com/mauwii/PyPatchMatch/blob/main/LICENSE). The wheels
144
147
  contain a statically linked build of OpenCV core (Apache-2.0) and its bundled
145
148
  third-party code; their licenses are included in the `licenses/opencv` directory of the
146
- wheel's `.dist-info`.
149
+ wheel's `.dist-info`, and a CycloneDX SBOM of OpenCV (PEP 770) in
150
+ `sboms/opencv.cdx.json`, so that vulnerability scanners can find it.
147
151
 
148
152
  ## README and COPYRIGHT by Younesse ANDAM
149
153
 
@@ -52,7 +52,8 @@ their values. Holes need hard edges: an anti-aliased brush leaves light pixels a
52
52
  the edge of its strokes that are not pure white, so they count as known and the fill
53
53
  continues them. Paint without anti-aliasing or pass a mask that covers those pixels as
54
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 (see
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
56
57
  [examples/py_example_global_mask.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example_global_mask.py)).
57
58
  `patchmatch.patchmatch_available` tells whether the native library could be loaded.
58
59
  The previous import path `from patchmatch import patch_match` keeps working.
@@ -84,8 +85,7 @@ int main() {
84
85
  }
85
86
  ```
86
87
 
87
- The library is built with CMake; `PATCHMATCH_BUILD_EXAMPLES` builds the example and
88
- `PATCHMATCH_WITH_HIGHGUI` enables the debug visualization of `Inpainting::run`.
88
+ The library is built with CMake; `PATCHMATCH_BUILD_EXAMPLES` builds the example.
89
89
 
90
90
  ## Development
91
91
 
@@ -103,9 +103,12 @@ The evaluation cuts holes into the images of `examples/images`, compares the fil
103
103
  the original content and writes `examples/images/evaluation.html`, which shows them side
104
104
  by side. `--baseline` evaluates another build of the library as well, e.g. of the main
105
105
  branch; the docstring of the script explains the columns.
106
+ Pull requests that change the C++ code get this comparison with their base commit in the
107
+ summary of the workflow "Fill quality", and the HTML report as its artifact.
106
108
 
107
109
  Releases are published to PyPI by creating a GitHub release; the version is taken from
108
- its tag (e.g. `v2.0.0`).
110
+ its tag (e.g. `v2.0.0`). Every released wheel and sdist carries a build provenance
111
+ attestation: `gh attestation verify <file> -R mauwii/PyPatchMatch`.
109
112
 
110
113
  ## License
111
114
 
@@ -113,7 +116,8 @@ PyPatchMatch is released under the
113
116
  [MIT License](https://github.com/mauwii/PyPatchMatch/blob/main/LICENSE). The wheels
114
117
  contain a statically linked build of OpenCV core (Apache-2.0) and its bundled
115
118
  third-party code; their licenses are included in the `licenses/opencv` directory of the
116
- wheel's `.dist-info`.
119
+ wheel's `.dist-info`, and a CycloneDX SBOM of OpenCV (PEP 770) in
120
+ `sboms/opencv.cdx.json`, so that vulnerability scanners can find it.
117
121
 
118
122
  ## README and COPYRIGHT by Younesse ANDAM
119
123
 
@@ -33,15 +33,7 @@ class CMatT(ctypes.Structure):
33
33
 
34
34
 
35
35
  # Order matches the PM_dtype_e enum in csrc/pyinterface.h.
36
- _PYMAT_DTYPES = [
37
- np.dtype(np.uint8),
38
- np.dtype(np.int8),
39
- np.dtype(np.uint16),
40
- np.dtype(np.int16),
41
- np.dtype(np.int32),
42
- np.dtype(np.float32),
43
- np.dtype(np.float64),
44
- ]
36
+ _PYMAT_DTYPES = [np.dtype(np.uint8), np.dtype(np.float32)]
45
37
  _PYMAT_DTYPE_IDS = {dtype: i for i, dtype in enumerate(_PYMAT_DTYPES)}
46
38
 
47
39
 
@@ -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.0rc4'
22
- __version_tuple__ = version_tuple = (2, 0, 0, 'rc4')
21
+ __version__ = version = '2.0.0rc5'
22
+ __version_tuple__ = version_tuple = (2, 0, 0, 'rc5')
23
23
 
24
- __commit_id__ = commit_id = 'g6f35e1d41'
24
+ __commit_id__ = commit_id = 'g3dcd7aa74'