PyPatchMatch 2.0.0rc6__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.
- pypatchmatch-2.0.1/CLAUDE.md +155 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/PKG-INFO +7 -1
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/README.md +6 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/_version.py +3 -3
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/pyproject.toml +14 -3
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/scripts/build_opencv.py +13 -7
- pypatchmatch-2.0.1/scripts/render_readme_example.py +36 -0
- pypatchmatch-2.0.0rc6/CLAUDE.md +0 -479
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/CMakeLists.txt +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/LICENSE +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/SECURITY.md +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/examples/images/forest_pruned.bmp +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/__init__.py +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/_lib.py +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/csrc/inpaint.cpp +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/csrc/inpaint.h +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/csrc/masked_image.cpp +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/csrc/masked_image.h +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/csrc/nnf.cpp +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/csrc/nnf.h +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/csrc/pyinterface.cpp +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/csrc/pyinterface.h +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/patch_match.py +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/patchmatch/py.typed +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/scripts/evaluate_inpainting.py +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/tests/conftest.py +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/tests/test_e2e.py +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/tests/test_patch_match.py +0 -0
- {pypatchmatch-2.0.0rc6 → pypatchmatch-2.0.1}/tests/test_properties.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.
|
|
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>
|
|
@@ -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
|
+

|
|
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
|
|
@@ -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
|
+

|
|
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
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 0,
|
|
21
|
+
__version__ = version = '2.0.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 0, 1)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g995b76749'
|
|
@@ -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 = [
|
|
@@ -34,7 +33,7 @@ classifiers = [
|
|
|
34
33
|
"Typing :: Typed",
|
|
35
34
|
]
|
|
36
35
|
dependencies = ["numpy>=1.23.2", "pillow>=9.2.0"]
|
|
37
|
-
dynamic = ["version"]
|
|
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 = ''
|
|
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
|
|
@@ -5,8 +5,8 @@ wheels small and free of OpenCV's GUI/codec dependencies.
|
|
|
5
5
|
|
|
6
6
|
Usage: python build_opencv.py (installs into $OpenCV_ROOT)
|
|
7
7
|
|
|
8
|
-
PATCHMATCH_OPENCV_VERSION selects another pinned version,
|
|
9
|
-
|
|
8
|
+
PATCHMATCH_OPENCV_VERSION selects another pinned version, e.g. 4.14.0 to rebuild the
|
|
9
|
+
wheels of PyPatchMatch 2.0.0.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import hashlib
|
|
@@ -27,7 +27,7 @@ OPENCV_SHA256S = {
|
|
|
27
27
|
"5.0.0": "b0528f5a1d379d59d4701cb28c36e22214cc51cf64594e5b56f2d3e6c0233095",
|
|
28
28
|
}
|
|
29
29
|
# the version of the release wheels
|
|
30
|
-
OPENCV_VERSION = os.environ.get("PATCHMATCH_OPENCV_VERSION") or "
|
|
30
|
+
OPENCV_VERSION = os.environ.get("PATCHMATCH_OPENCV_VERSION") or "5.0.0"
|
|
31
31
|
if OPENCV_VERSION not in OPENCV_SHA256S:
|
|
32
32
|
sys.exit(f"OpenCV {OPENCV_VERSION} is not pinned, known: {sorted(OPENCV_SHA256S)}")
|
|
33
33
|
OPENCV_SHA256 = OPENCV_SHA256S[OPENCV_VERSION]
|
|
@@ -73,6 +73,8 @@ CMAKE_OPTIONS = {
|
|
|
73
73
|
"WITH_QT": "OFF",
|
|
74
74
|
"WITH_TIFF": "OFF",
|
|
75
75
|
"WITH_V4L": "OFF",
|
|
76
|
+
# an installed VTK adds OpenGL to the link interface of core (OpenCV 5)
|
|
77
|
+
"WITH_VTK": "OFF",
|
|
76
78
|
"WITH_WEBP": "OFF",
|
|
77
79
|
"WITH_IPP": "OFF",
|
|
78
80
|
"WITH_ITT": "OFF",
|
|
@@ -90,6 +92,8 @@ WINDOWS_CMAKE_OPTIONS = {
|
|
|
90
92
|
"OPENCV_CONFIG_INSTALL_PATH": "cmake",
|
|
91
93
|
"OPENCV_INSTALL_BINARIES_PREFIX": "",
|
|
92
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",
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
|
|
@@ -159,8 +163,10 @@ def main() -> None:
|
|
|
159
163
|
return
|
|
160
164
|
|
|
161
165
|
cmake = find_cmake()
|
|
162
|
-
with tempfile.TemporaryDirectory() as
|
|
163
|
-
|
|
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"
|
|
164
170
|
print(f"Downloading {OPENCV_URL}", flush=True)
|
|
165
171
|
urllib.request.urlretrieve(OPENCV_URL, archive)
|
|
166
172
|
verify_archive(archive)
|
|
@@ -171,8 +177,8 @@ def main() -> None:
|
|
|
171
177
|
else:
|
|
172
178
|
tar.extractall(tmp)
|
|
173
179
|
|
|
174
|
-
source =
|
|
175
|
-
build =
|
|
180
|
+
source = tmp / f"opencv-{OPENCV_VERSION}"
|
|
181
|
+
build = tmp / "build"
|
|
176
182
|
options = {**CMAKE_OPTIONS, "CMAKE_INSTALL_PREFIX": prefix.as_posix()}
|
|
177
183
|
if sys.platform == "win32":
|
|
178
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
|
+
)
|
pypatchmatch-2.0.0rc6/CLAUDE.md
DELETED
|
@@ -1,479 +0,0 @@
|
|
|
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 `cp311-*`. 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`); `_initialize_pyramid` makes them black on
|
|
288
|
-
level 0 as on the coarser levels, so their colors do not matter
|
|
289
|
-
(`test_colors_under_the_holes_do_not_matter`); `global_mask` marks pixels that are
|
|
290
|
-
neither filled nor used as a patch source, and whose colors must not influence the
|
|
291
|
-
fill: the downsampling kernel skips them, and gradients next to them are neutral
|
|
292
|
-
(`test_colors_under_global_mask_do_not_matter`). At the end of `run()` every pixel
|
|
293
|
-
that is not a hole, including the globally masked ones, is copied back from the input,
|
|
294
|
-
so only the holes change. The tests assert this invariant (`assert_filled`,
|
|
295
|
-
`assert_plausible_fill`).
|
|
296
|
-
|
|
297
|
-
### OpenCV usage and library size
|
|
298
|
-
|
|
299
|
-
The release wheels link a static, core-only OpenCV (`scripts/build_opencv.py`:
|
|
300
|
-
`BUILD_LIST=core`, every optional backend off). For changes to the C++ code:
|
|
301
|
-
|
|
302
|
-
- The library links only `opencv_core`; `imgcodecs` is only used by the example.
|
|
303
|
-
- Use plain pixel loops instead of `cv::Mat` matrix expressions such as `a == b` or
|
|
304
|
-
`m1 | m2`: they pull OpenCV's expression module, and with it `FileStorage`, PCA and
|
|
305
|
-
`SparseMat` code, into the static library, which grew by about 40 % before commit
|
|
306
|
-
e50c613 replaced them.
|
|
307
|
-
- On Windows the static OpenCV uses the static MSVC runtime (`BUILD_WITH_STATIC_CRT`),
|
|
308
|
-
so the library does too (`MSVC_RUNTIME_LIBRARY` in `CMakeLists.txt`). Its stderr is
|
|
309
|
-
then not captured by pytest's `capfd`, so tests of native output run in a subprocess
|
|
310
|
-
(`test_set_verbose_prints_progress`).
|
|
311
|
-
- With a static OpenCV, CMake installs its licenses, which the build script collects in
|
|
312
|
-
`<prefix>/licenses`, into `.dist-info/licenses/opencv` of the wheel, and the CycloneDX
|
|
313
|
-
SBOM it writes to `<prefix>/sboms/opencv.cdx.json` into `.dist-info/sboms` (PEP 770).
|
|
314
|
-
The static library hides OpenCV from scanners; grype matches the SBOM through its CPE.
|
|
315
|
-
- The SBOM leaves out OpenCV's bundled zlib: only `FileStorage` uses it, and none of it
|
|
316
|
-
is linked into the library. The wheel job checks that every wheel has the SBOM and,
|
|
317
|
-
except on Windows, that `nm` finds no zlib symbol in the library. `write_sbom` fails
|
|
318
|
-
if the build bundles another 3rdparty library; add a new one to the SBOM.
|
|
319
|
-
- To update OpenCV, change the default of `OPENCV_VERSION` and add its hash to
|
|
320
|
-
`OPENCV_SHA256S`. `PATCHMATCH_OPENCV_VERSION` selects another pinned version: the
|
|
321
|
-
CI job `opencv5` builds against OpenCV 5 (Homebrew's `opencv` since 2026), because
|
|
322
|
-
builds from the sdist link whatever OpenCV the system has. The CI cache of the
|
|
323
|
-
OpenCV build is keyed on the hash of the script.
|
|
324
|
-
|
|
325
|
-
### Packaging and versioning
|
|
326
|
-
|
|
327
|
-
- Build backend: scikit-build-core with setuptools-scm. The version comes from the git
|
|
328
|
-
tags; `patchmatch/_version.py` is generated and git-ignored. Without git metadata the
|
|
329
|
-
version falls back to `0.0.0`, so CI checks out with `fetch-depth: 0`.
|
|
330
|
-
- The release wheels are built from the sdist, not from the checkout, and cibuildwheel
|
|
331
|
-
runs `pytest {project}/tests --no-cov` against the sdist contents. Everything the tests
|
|
332
|
-
need has to be in the sdist: `sdist.include` adds `examples/images/forest_pruned.bmp`,
|
|
333
|
-
the rest of `examples/` is excluded. New test data has to be added there as well.
|
|
334
|
-
- Fallback: if a build from the sdist fails (e.g. no compiler, CMake or OpenCV on a
|
|
335
|
-
platform without a wheel), the `tool.scikit-build.overrides` entry builds a pure wheel
|
|
336
|
-
without the library, so the installation succeeds with `patchmatch_available` False and
|
|
337
|
-
the error message of `find_library` explains how to reinstall. Builds from the source
|
|
338
|
-
tree still fail. The CI job `sdist-fallback` and `test_import_without_native_library`
|
|
339
|
-
keep this path working.
|
|
340
|
-
- Wheels: manylinux x86_64 and aarch64, macOS arm64 and x86_64 (deployment target 11.0),
|
|
341
|
-
Windows x64. musllinux is skipped.
|
|
342
|
-
|
|
343
|
-
## CI
|
|
344
|
-
|
|
345
|
-
- `ci.yml`: `lint` runs `pre-commit run --all-files`; `test` runs `uv run pytest` on
|
|
346
|
-
Ubuntu, macOS and Windows with Python 3.11, 3.14 and 3.15, after building (or
|
|
347
|
-
restoring) the OpenCV of the build script. It syncs only the `test` group: the `dev`
|
|
348
|
-
group pulls in pyyaml through pre-commit, which had no wheel for 3.15 yet, and
|
|
349
|
-
`--no-build` refuses to build it.
|
|
350
|
-
- pre-commit runs, besides the file checks, ruff and uv-lock: clang-format
|
|
351
|
-
(`.clang-format`), shellcheck, markdownlint-cli2 (`.markdownlint.yaml`, 88 columns),
|
|
352
|
-
typos (fixes in place), actionlint (checks `run:` scripts with shellcheck only if it
|
|
353
|
-
is on `PATH`) and zizmor (offline, default persona), and mypy as a local hook
|
|
354
|
-
(`uv run --locked --no-build --only-group dev mypy`, like the `lint` job, so the
|
|
355
|
-
library is not built; numpy and pillow are in the `dev` group for their types).
|
|
356
|
-
MegaLinter was considered and rejected: it duplicates pre-commit in a multi-GB Docker
|
|
357
|
-
image and bundles the linter versions instead of pinning each hook for Dependabot.
|
|
358
|
-
- Dependabot waits 7 days after a release (`cooldown`, required by zizmor).
|
|
359
|
-
- `test` sets `SKBUILD_CMAKE_DEFINE=CMAKE_COMPILE_WARNING_AS_ERROR=ON`, so the warning
|
|
360
|
-
flags of `CMakeLists.txt` (`-Wall -Wextra -Wpedantic -Wshadow`, `/W4` on MSVC) fail the
|
|
361
|
-
build. Only there: builds from the sdist and the wheels must not break when a newer
|
|
362
|
-
compiler adds a warning. The OpenCV headers are system includes and do not count.
|
|
363
|
-
- `sanitizers` builds with `PATCHMATCH_SANITIZE=ON` on Ubuntu and runs the tests with
|
|
364
|
-
the preloaded ASan runtime (see Commands). It reuses the checkout and OpenCV steps of
|
|
365
|
-
`test` through YAML anchors, so change them there.
|
|
366
|
-
Scheduled (nightly) and manual runs of `ci.yml` pass `--hypothesis-profile=deep`
|
|
367
|
-
(`tests/conftest.py`: 5000 examples per property test, about 90 s locally without
|
|
368
|
-
sanitizers) and allow the job 90 minutes; a failure prints a `@reproduce_failure`
|
|
369
|
-
blob. The property tests found real bugs, so they get the most search time.
|
|
370
|
-
- `coverage` builds with `PATCHMATCH_COVERAGE=ON` on Ubuntu, runs the tests and fails
|
|
371
|
-
below 95 % of the C++ lines or 90 % of the branches (gcovr, see Commands).
|
|
372
|
-
It reuses the same anchors. It also writes `coverage.xml` (Python) and
|
|
373
|
-
`coverage-cpp.xml` (`gcovr --sonarqube`) and exports `build/compile_commands.json`,
|
|
374
|
-
and then runs the SonarCloud scan (`sonar-project.properties`, `SONAR_TOKEN`, skipped
|
|
375
|
-
without the secret, e.g. for Dependabot and forks). Automatic analysis in SonarCloud
|
|
376
|
-
is disabled; it cannot import coverage and would conflict with the CI scan. `scripts/`
|
|
377
|
-
has no tests and is excluded from the coverage on new code (80 % in "Sonar way").
|
|
378
|
-
`sonar.qualitygate.wait=true` makes the scan fail on a red quality gate, so the gate
|
|
379
|
-
is part of `ci-ok` and auto-merge waits for it; the skipped scan of Dependabot and
|
|
380
|
-
fork PRs blocks nothing, unlike a required "SonarCloud Code Analysis" check, which
|
|
381
|
-
would never appear there.
|
|
382
|
-
- The `uv run` steps pass `--locked` and `--no-build` (SonarCloud S8544, S8541).
|
|
383
|
-
`--no-build` only forbids building dependencies from source; uv still builds the
|
|
384
|
-
project itself. The OpenCV step runs with `--only-group test`: the project cannot be
|
|
385
|
-
built before OpenCV is installed, `--no-project` would lock nothing, and the `dev`
|
|
386
|
-
group may lack wheels for the newest Python of the matrix.
|
|
387
|
-
- Every setup-uv step sets `version: latest-known`: the newest uv whose checksum is
|
|
388
|
-
bundled with the pinned setup-uv. Without it, CI installs each uv release minutes
|
|
389
|
-
after publication; this way uv only moves when Dependabot updates the action, with
|
|
390
|
-
its cooldown. That uv can be a few releases older than the one of the `uv-lock` hook.
|
|
391
|
-
`prune-cache: true` keeps only wheels built from source in the uv cache; downloaded
|
|
392
|
-
wheels restore no faster than they download.
|
|
393
|
-
- `system-opencv` installs `libopencv-dev` on `ubuntu-24.04` (OpenCV 4.6) instead of
|
|
394
|
-
running the build script and runs the tests, because builds from the sdist link
|
|
395
|
-
whatever OpenCV the system has. The runner is pinned so that the OpenCV version only
|
|
396
|
-
changes on purpose. The Ubuntu mirror is sometimes very slow (180 MB at under
|
|
397
|
-
300 kB/s), so the job caches the downloaded `.deb` files, keyed on the hash of
|
|
398
|
-
`apt-get install --print-uris`, with `restore-keys` for the files that did not change.
|
|
399
|
-
apt checks them against the signed index and dpkg installs them as usual. The job
|
|
400
|
-
downloads first (`--download-only`) and caches only the files of the current list.
|
|
401
|
-
- `wheels.yml`: sdist, then cibuildwheel on five runners, then `sdist-fallback`;
|
|
402
|
-
`publish` attests the build provenance of all files (`actions/attest`) and uploads
|
|
403
|
-
them to PyPI with trusted publishing (environment `pypi`, deployable only from tags
|
|
404
|
-
`v*`) only for a published GitHub release of `mauwii/PyPatchMatch`. It downloads the
|
|
405
|
-
artifacts named `dist-*`, so other artifacts of the run never reach PyPI. The wheel
|
|
406
|
-
jobs cache the OpenCV prefix, keyed on `build_opencv.py` and `pyproject.toml` from the
|
|
407
|
-
sdist (there is no checkout); on Linux a volume makes `/tmp/opencv` of the container
|
|
408
|
-
visible to the cache step. Runs on a tag, and therefore releases, skip the cache and
|
|
409
|
-
build OpenCV from scratch.
|
|
410
|
-
The build and test dependencies of the distributions are not locked, so the workflow
|
|
411
|
-
sets `UV_EXCLUDE_NEWER: 7 days` (the cooldown of `dependabot.yml`): `uv build` and
|
|
412
|
-
cibuildwheel's `build-frontend = "build[uv]"` resolve only releases older than a
|
|
413
|
-
week. On Linux `environment-pass` hands the variable into the container; on macOS
|
|
414
|
-
and Windows cibuildwheel uses the uv of a setup-uv step.
|
|
415
|
-
- `fill-quality.yml` runs for pull requests that change `CMakeLists.txt`,
|
|
416
|
-
`patchmatch/csrc` or the evaluation script: it builds the library of the base commit,
|
|
417
|
-
runs `scripts/evaluate_inpainting.py --baseline` against it (patch size 3, 3 seeds),
|
|
418
|
-
writes the table to the job summary (`--markdown`) and uploads `evaluation.html` as the
|
|
419
|
-
artifact `fill-quality`. It is a report, not a gate: the measures vary over the seeds,
|
|
420
|
-
so it is not in the `needs` of `ci-ok`. It repeats the OpenCV steps of `ci.yml` with
|
|
421
|
-
the same cache key, because anchors do not reach across files.
|
|
422
|
-
- `codeql.yml` analyzes actions, C/C++ and Python.
|
|
423
|
-
- `cache-cleanup.yml` deletes the caches of a pull request when it is closed: only that
|
|
424
|
-
pull request can read them, and at the 10 GB limit GitHub evicts the least recently
|
|
425
|
-
used caches, often those of `main`. For pull requests from forks the token is
|
|
426
|
-
read-only; their caches expire after 7 days without access.
|
|
427
|
-
- `ci-ok` and `wheels-ok` (re-actors/alls-green) are the required status checks; add new
|
|
428
|
-
jobs to their `needs`. The ruleset of `main` also has a `code_scanning` rule: CodeQL
|
|
429
|
-
alerts of severity error or security severity high and above block the merge (CodeQL
|
|
430
|
-
uploads its results for Dependabot PRs too, so they are not stuck).
|
|
431
|
-
- Actions are pinned to full commit SHAs with a `# vX.Y.Z` comment, and checkouts use
|
|
432
|
-
`persist-credentials: false`. The pre-commit hooks are pinned the same way
|
|
433
|
-
(`rev: <sha> # frozen: vX.Y.Z`; `pre-commit autoupdate --freeze` by hand), because a
|
|
434
|
-
moved tag would otherwise reach CI and fresh clones without review. Dependabot
|
|
435
|
-
updates actions, uv and pre-commit weekly in groups, SHA and comment together. The
|
|
436
|
-
repository is a GitHub fork, on which Dependabot version updates are off by default;
|
|
437
|
-
they are enabled in Insights → Dependency graph → Dependabot.
|
|
438
|
-
|
|
439
|
-
## Conventions
|
|
440
|
-
|
|
441
|
-
- Python >= 3.11: keep `from __future__ import annotations`. Ruff targets py311 with the
|
|
442
|
-
rules B, C4, E, F, I, RUF, SIM, UP and W, and formats at 88 columns. `patchmatch/csrc`
|
|
443
|
-
is excluded from ruff. C++ is formatted by clang-format (Microsoft style, 120 columns,
|
|
444
|
-
includes grouped as own header, `<...>`, `"..."`); naming is not checked: `m_`
|
|
445
|
-
members, `k` constants. Commit a reformat separately and add its hash to
|
|
446
|
-
`.git-blame-ignore-revs`.
|
|
447
|
-
- The package ships `py.typed`; public functions are fully annotated. mypy checks
|
|
448
|
-
`patchmatch` and `tests` in strict mode (`[tool.mypy]`); the tests have to use the
|
|
449
|
-
API correctly but need no annotations of their own. A deliberately wrong argument
|
|
450
|
-
goes through a test parameter instead of a `# type: ignore`.
|
|
451
|
-
- Backwards compatibility: `from patchmatch import patch_match` and
|
|
452
|
-
`patch_match.CShapeT`/`CMatT` keep working (tested).
|
|
453
|
-
- Comments explain why: which crash a check prevents, why a construct is avoided.
|
|
454
|
-
- No `NOSONAR` markers: change the code so that the SonarCloud rule is satisfied, and ask
|
|
455
|
-
when that seems impossible. The only exception is `nnf.cpp`: cpp:S2245 flags every
|
|
456
|
-
declaration of `std::mt19937`, which only drives the randomized search.
|
|
457
|
-
- Tests check invariants instead of golden images: holes filled, all other pixels
|
|
458
|
-
unchanged, determinism with `set_random_seed`, fills that blend in with their
|
|
459
|
-
surroundings. `test_patch_match.py` and `test_e2e.py` seed through an autouse fixture.
|
|
460
|
-
Unit tests use a small synthetic image; the `e2e` tests use
|
|
461
|
-
`examples/images/forest_pruned.bmp` and mirror the workflows of the README and the
|
|
462
|
-
examples. `test_properties.py` checks the invariants on random inputs with Hypothesis
|
|
463
|
-
(in the `test` group, so the wheel tests run it too); it seeds inside the tests,
|
|
464
|
-
because Hypothesis rejects function-scoped fixtures, and has no deadline, because
|
|
465
|
-
the sanitizer and coverage builds are much slower.
|
|
466
|
-
- The README documents the behavior (mask semantics, `patch_size` as a radius, the
|
|
467
|
-
fallback installation); update it together with API or installation changes.
|
|
468
|
-
- Commit messages: imperative subject in sentence case without a prefix (e.g. "Reject
|
|
469
|
-
invalid patch sizes and empty regularity maps"), and a body that explains the reason,
|
|
470
|
-
with measurements where relevant. Changes go through PRs from branches such as
|
|
471
|
-
`fix/<topic>`.
|
|
472
|
-
|
|
473
|
-
## Git and GitHub
|
|
474
|
-
|
|
475
|
-
- `origin` is `mauwii/PyPatchMatch`, where PRs and releases go; `upstream` is
|
|
476
|
-
`invoke-ai/PyPatchMatch`. `gh` resolves the default repository to upstream, so pass
|
|
477
|
-
`-R mauwii/PyPatchMatch`.
|
|
478
|
-
- Releases: creating a GitHub release with a tag like `v2.0.0` (or `v2.0.0rc2` for
|
|
479
|
-
pre-releases) builds the wheels and publishes them to PyPI.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|