PyPatchMatch 2.0.0rc5__tar.gz → 2.0.0rc6__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.0rc5 → pypatchmatch-2.0.0rc6}/CLAUDE.md +63 -20
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/PKG-INFO +12 -11
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/README.md +7 -6
- pypatchmatch-2.0.0rc6/SECURITY.md +20 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/_version.py +3 -3
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/csrc/inpaint.cpp +12 -1
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/pyproject.toml +13 -6
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/scripts/build_opencv.py +15 -6
- pypatchmatch-2.0.0rc6/tests/conftest.py +6 -0
- pypatchmatch-2.0.0rc6/tests/test_properties.py +159 -0
- pypatchmatch-2.0.0rc5/tests/test_properties.py +0 -95
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/CMakeLists.txt +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/LICENSE +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/examples/images/forest_pruned.bmp +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/__init__.py +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/_lib.py +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/csrc/inpaint.h +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/csrc/masked_image.cpp +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/csrc/masked_image.h +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/csrc/nnf.cpp +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/csrc/nnf.h +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/csrc/pyinterface.cpp +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/csrc/pyinterface.h +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/patch_match.py +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/patchmatch/py.typed +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/scripts/evaluate_inpainting.py +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/tests/test_e2e.py +0 -0
- {pypatchmatch-2.0.0rc5 → pypatchmatch-2.0.0rc6}/tests/test_patch_match.py +0 -0
|
@@ -190,7 +190,7 @@ The C++ headers double as the C++ API: `examples/cpp_example.cpp` uses `Inpainti
|
|
|
190
190
|
|
|
191
191
|
The library does not use the CPython ABI, so the wheels are tagged
|
|
192
192
|
`py3-none-<platform>` (`wheel.py-api = "py3"`): one wheel per platform serves all Python
|
|
193
|
-
versions, and cibuildwheel only builds `
|
|
193
|
+
versions, and cibuildwheel only builds `cp311-*`. Keep `csrc` free of `Python.h`,
|
|
194
194
|
pybind11 or nanobind. The C++ sources are excluded from the wheel (`wheel.exclude`);
|
|
195
195
|
CMake installs the shared library into the package directory.
|
|
196
196
|
|
|
@@ -284,9 +284,11 @@ ctypes releases the GIL during native calls, so inpaintings run truly concurrent
|
|
|
284
284
|
as maximal. The distance indexes `distance2similarity`, so every metric has to clamp
|
|
285
285
|
its result to that range.
|
|
286
286
|
- Mask semantics: `mask` non-zero marks the holes (by default all pure white pixels,
|
|
287
|
-
computed in Python by `_default_mask`); `
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
290
292
|
(`test_colors_under_global_mask_do_not_matter`). At the end of `run()` every pixel
|
|
291
293
|
that is not a hole, including the globally masked ones, is copied back from the input,
|
|
292
294
|
so only the holes change. The tests assert this invariant (`assert_filled`,
|
|
@@ -314,8 +316,11 @@ The release wheels link a static, core-only OpenCV (`scripts/build_opencv.py`:
|
|
|
314
316
|
is linked into the library. The wheel job checks that every wheel has the SBOM and,
|
|
315
317
|
except on Windows, that `nm` finds no zlib symbol in the library. `write_sbom` fails
|
|
316
318
|
if the build bundles another 3rdparty library; add a new one to the SBOM.
|
|
317
|
-
- To update OpenCV, change `OPENCV_VERSION` and
|
|
318
|
-
|
|
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.
|
|
319
324
|
|
|
320
325
|
### Packaging and versioning
|
|
321
326
|
|
|
@@ -338,8 +343,10 @@ The release wheels link a static, core-only OpenCV (`scripts/build_opencv.py`:
|
|
|
338
343
|
## CI
|
|
339
344
|
|
|
340
345
|
- `ci.yml`: `lint` runs `pre-commit run --all-files`; `test` runs `uv run pytest` on
|
|
341
|
-
Ubuntu, macOS and Windows with Python 3.
|
|
342
|
-
OpenCV of the build script.
|
|
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.
|
|
343
350
|
- pre-commit runs, besides the file checks, ruff and uv-lock: clang-format
|
|
344
351
|
(`.clang-format`), shellcheck, markdownlint-cli2 (`.markdownlint.yaml`, 88 columns),
|
|
345
352
|
typos (fixes in place), actionlint (checks `run:` scripts with shellcheck only if it
|
|
@@ -356,6 +363,10 @@ The release wheels link a static, core-only OpenCV (`scripts/build_opencv.py`:
|
|
|
356
363
|
- `sanitizers` builds with `PATCHMATCH_SANITIZE=ON` on Ubuntu and runs the tests with
|
|
357
364
|
the preloaded ASan runtime (see Commands). It reuses the checkout and OpenCV steps of
|
|
358
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.
|
|
359
370
|
- `coverage` builds with `PATCHMATCH_COVERAGE=ON` on Ubuntu, runs the tests and fails
|
|
360
371
|
below 95 % of the C++ lines or 90 % of the branches (gcovr, see Commands).
|
|
361
372
|
It reuses the same anchors. It also writes `coverage.xml` (Python) and
|
|
@@ -364,21 +375,43 @@ The release wheels link a static, core-only OpenCV (`scripts/build_opencv.py`:
|
|
|
364
375
|
without the secret, e.g. for Dependabot and forks). Automatic analysis in SonarCloud
|
|
365
376
|
is disabled; it cannot import coverage and would conflict with the CI scan. `scripts/`
|
|
366
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.
|
|
367
382
|
- The `uv run` steps pass `--locked` and `--no-build` (SonarCloud S8544, S8541).
|
|
368
383
|
`--no-build` only forbids building dependencies from source; uv still builds the
|
|
369
|
-
project itself. The OpenCV step runs with `--only-group
|
|
370
|
-
built before OpenCV is installed,
|
|
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.
|
|
371
393
|
- `system-opencv` installs `libopencv-dev` on `ubuntu-24.04` (OpenCV 4.6) instead of
|
|
372
394
|
running the build script and runs the tests, because builds from the sdist link
|
|
373
395
|
whatever OpenCV the system has. The runner is pinned so that the OpenCV version only
|
|
374
|
-
changes on purpose.
|
|
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.
|
|
375
401
|
- `wheels.yml`: sdist, then cibuildwheel on five runners, then `sdist-fallback`;
|
|
376
402
|
`publish` attests the build provenance of all files (`actions/attest`) and uploads
|
|
377
|
-
them to PyPI with trusted publishing (environment `pypi
|
|
378
|
-
release of `mauwii/PyPatchMatch`.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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.
|
|
382
415
|
- `fill-quality.yml` runs for pull requests that change `CMakeLists.txt`,
|
|
383
416
|
`patchmatch/csrc` or the evaluation script: it builds the library of the base commit,
|
|
384
417
|
runs `scripts/evaluate_inpainting.py --baseline` against it (patch size 3, 3 seeds),
|
|
@@ -387,15 +420,25 @@ The release wheels link a static, core-only OpenCV (`scripts/build_opencv.py`:
|
|
|
387
420
|
so it is not in the `needs` of `ci-ok`. It repeats the OpenCV steps of `ci.yml` with
|
|
388
421
|
the same cache key, because anchors do not reach across files.
|
|
389
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.
|
|
390
427
|
- `ci-ok` and `wheels-ok` (re-actors/alls-green) are the required status checks; add new
|
|
391
|
-
jobs to their `needs`.
|
|
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).
|
|
392
431
|
- Actions are pinned to full commit SHAs with a `# vX.Y.Z` comment, and checkouts use
|
|
393
|
-
`persist-credentials: false`.
|
|
394
|
-
|
|
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.
|
|
395
438
|
|
|
396
439
|
## Conventions
|
|
397
440
|
|
|
398
|
-
- Python >= 3.
|
|
441
|
+
- Python >= 3.11: keep `from __future__ import annotations`. Ruff targets py311 with the
|
|
399
442
|
rules B, C4, E, F, I, RUF, SIM, UP and W, and formats at 88 columns. `patchmatch/csrc`
|
|
400
443
|
is excluded from ruff. C++ is formatted by clang-format (Microsoft style, 120 columns,
|
|
401
444
|
includes grouped as own header, `<...>`, `"..."`); naming is not checked: `m_`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPatchMatch
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0rc6
|
|
4
4
|
Summary: This library implements the PatchMatch based inpainting algorithm.
|
|
5
5
|
Keywords: inpainting,patchmatch,image-processing,opencv
|
|
6
6
|
Author-Email: The InvokeAI Project <lincoln.stein@gmail.com>, Jiayuan Mao <maojiayuan@gmail.com>, Kyle Schouviller <kyle0654@hotmail.com>, Matthias Wild <mauwii@outlook.de>, Younesse ANDAM <younesse.andam@gmail.com>
|
|
@@ -12,20 +12,20 @@ Classifier: Operating System :: Microsoft :: Windows
|
|
|
12
12
|
Classifier: Operating System :: POSIX :: Linux
|
|
13
13
|
Classifier: Programming Language :: C++
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
20
|
Classifier: Topic :: Multimedia :: Graphics
|
|
21
21
|
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Project-URL: Source Code, https://github.com/mauwii/PyPatchMatch
|
|
24
24
|
Project-URL: Issues, https://github.com/mauwii/PyPatchMatch/issues
|
|
25
25
|
Project-URL: Release Notes, https://github.com/mauwii/PyPatchMatch/releases
|
|
26
|
-
Requires-Python: >=3.
|
|
27
|
-
Requires-Dist: numpy>=1.
|
|
28
|
-
Requires-Dist: pillow>=
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: numpy>=1.23.2
|
|
28
|
+
Requires-Dist: pillow>=9.2.0
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
|
|
31
31
|
# PatchMatch based Inpainting
|
|
@@ -78,12 +78,13 @@ result = patchmatch.inpaint(image, mask, patch_size=3)
|
|
|
78
78
|
|
|
79
79
|
The mask must have the same height and width as the image. If `mask` is omitted, all
|
|
80
80
|
pure white pixels are treated as holes. Only the holes are filled; all other pixels keep
|
|
81
|
-
their values
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
their colors have no influence on
|
|
81
|
+
their values, and the colors under the holes have no influence on the fill. Holes need
|
|
82
|
+
hard edges: an anti-aliased brush leaves light pixels along the edge of its strokes that
|
|
83
|
+
are not pure white, so they count as known and the fill continues them. Paint without
|
|
84
|
+
anti-aliasing or pass a mask that covers those pixels as well. The optional keyword
|
|
85
|
+
argument `global_mask`, in the same format, marks pixels that are neither filled nor
|
|
86
|
+
used as a source; they keep their values as well, and their colors have no influence on
|
|
87
|
+
the fill (see
|
|
87
88
|
[examples/py_example_global_mask.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example_global_mask.py)).
|
|
88
89
|
`patchmatch.patchmatch_available` tells whether the native library could be loaded.
|
|
89
90
|
The previous import path `from patchmatch import patch_match` keeps working.
|
|
@@ -48,12 +48,13 @@ result = patchmatch.inpaint(image, mask, patch_size=3)
|
|
|
48
48
|
|
|
49
49
|
The mask must have the same height and width as the image. If `mask` is omitted, all
|
|
50
50
|
pure white pixels are treated as holes. Only the holes are filled; all other pixels keep
|
|
51
|
-
their values
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
their colors have no influence on
|
|
51
|
+
their values, and the colors under the holes have no influence on the fill. Holes need
|
|
52
|
+
hard edges: an anti-aliased brush leaves light pixels along the edge of its strokes that
|
|
53
|
+
are not pure white, so they count as known and the fill continues them. Paint without
|
|
54
|
+
anti-aliasing or pass a mask that covers those pixels as well. The optional keyword
|
|
55
|
+
argument `global_mask`, in the same format, marks pixels that are neither filled nor
|
|
56
|
+
used as a source; they keep their values as well, and their colors have no influence on
|
|
57
|
+
the fill (see
|
|
57
58
|
[examples/py_example_global_mask.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example_global_mask.py)).
|
|
58
59
|
`patchmatch.patchmatch_available` tells whether the native library could be loaded.
|
|
59
60
|
The previous import path `from patchmatch import patch_match` keeps working.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Security fixes are released as a new version of PyPatchMatch on PyPI. Only the latest
|
|
6
|
+
release is supported; older versions are not updated.
|
|
7
|
+
|
|
8
|
+
## Reporting a Vulnerability
|
|
9
|
+
|
|
10
|
+
Please report vulnerabilities privately through GitHub's
|
|
11
|
+
[vulnerability reporting](https://github.com/mauwii/PyPatchMatch/security/advisories/new),
|
|
12
|
+
not in a public issue.
|
|
13
|
+
|
|
14
|
+
## OpenCV
|
|
15
|
+
|
|
16
|
+
The wheels on PyPI contain a statically linked, core-only build of OpenCV; its version
|
|
17
|
+
is listed in the CycloneDX SBOM in `sboms/opencv.cdx.json` of each wheel's
|
|
18
|
+
`.dist-info`. A vulnerability in OpenCV core is fixed by a new PyPatchMatch release
|
|
19
|
+
with an updated OpenCV. Installations built from the source distribution link the
|
|
20
|
+
OpenCV of the system and get its fixes from there.
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 0, 0, '
|
|
21
|
+
__version__ = version = '2.0.0rc6'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 0, 0, 'rc6')
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g7cfa5cb5b'
|
|
@@ -164,7 +164,18 @@ Inpainting::Inpainting(
|
|
|
164
164
|
|
|
165
165
|
void Inpainting::_initialize_pyramid()
|
|
166
166
|
{
|
|
167
|
-
|
|
167
|
+
// The colors under the holes would reach the fill through the gradients of their
|
|
168
|
+
// neighbors and the target of a single-level pyramid. Black, like on coarser levels.
|
|
169
|
+
auto source = m_initial.clone();
|
|
170
|
+
const auto size = source.size();
|
|
171
|
+
for (int y = 0; y < size.height; ++y)
|
|
172
|
+
{
|
|
173
|
+
for (int x = 0; x < size.width; ++x)
|
|
174
|
+
{
|
|
175
|
+
if (source.is_masked(y, x))
|
|
176
|
+
std::fill_n(source.get_mutable_image(y, x), 3, 0);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
168
179
|
m_pyramid.push_back(source);
|
|
169
180
|
while (source.size().height > m_distance_metric->patch_size() &&
|
|
170
181
|
source.size().width > m_distance_metric->patch_size())
|
|
@@ -15,7 +15,7 @@ authors = [
|
|
|
15
15
|
{ name = "Matthias Wild", email = "mauwii@outlook.de" },
|
|
16
16
|
{ name = "Younesse ANDAM", email = "younesse.andam@gmail.com" },
|
|
17
17
|
]
|
|
18
|
-
requires-python = ">=3.
|
|
18
|
+
requires-python = ">=3.11"
|
|
19
19
|
keywords = ["inpainting", "patchmatch", "image-processing", "opencv"]
|
|
20
20
|
classifiers = [
|
|
21
21
|
"Development Status :: 5 - Production/Stable",
|
|
@@ -24,16 +24,16 @@ classifiers = [
|
|
|
24
24
|
"Operating System :: POSIX :: Linux",
|
|
25
25
|
"Programming Language :: C++",
|
|
26
26
|
"Programming Language :: Python :: 3",
|
|
27
|
-
"Programming Language :: Python :: 3.10",
|
|
28
27
|
"Programming Language :: Python :: 3.11",
|
|
29
28
|
"Programming Language :: Python :: 3.12",
|
|
30
29
|
"Programming Language :: Python :: 3.13",
|
|
31
30
|
"Programming Language :: Python :: 3.14",
|
|
31
|
+
"Programming Language :: Python :: 3.15",
|
|
32
32
|
"Topic :: Multimedia :: Graphics",
|
|
33
33
|
"Topic :: Scientific/Engineering :: Image Processing",
|
|
34
34
|
"Typing :: Typed",
|
|
35
35
|
]
|
|
36
|
-
dependencies = ["numpy>=1.
|
|
36
|
+
dependencies = ["numpy>=1.23.2", "pillow>=9.2.0"]
|
|
37
37
|
dynamic = ["version"]
|
|
38
38
|
|
|
39
39
|
[project.urls]
|
|
@@ -78,10 +78,13 @@ fallback_version = "0.0.0"
|
|
|
78
78
|
[tool.cibuildwheel]
|
|
79
79
|
# The wheels are Python-version independent (py3-none-<platform>), so each platform
|
|
80
80
|
# is built and tested once; the CI workflow covers the other Python versions.
|
|
81
|
-
build = "
|
|
81
|
+
build = "cp311-*"
|
|
82
82
|
skip = "*-musllinux_*"
|
|
83
83
|
# OpenCV is built once per job in before-all, only for the runner's native arch
|
|
84
84
|
archs = ["native"]
|
|
85
|
+
# uv installs the build and test environments, so that UV_EXCLUDE_NEWER of wheels.yml
|
|
86
|
+
# applies to them
|
|
87
|
+
build-frontend = "build[uv]"
|
|
85
88
|
test-groups = ["test"]
|
|
86
89
|
test-command = "pytest {project}/tests --no-cov"
|
|
87
90
|
before-all = "python {project}/scripts/build_opencv.py"
|
|
@@ -89,6 +92,7 @@ environment = { OpenCV_ROOT = "/tmp/opencv" }
|
|
|
89
92
|
|
|
90
93
|
[tool.cibuildwheel.linux]
|
|
91
94
|
before-all = "/opt/python/cp312-cp312/bin/python {project}/scripts/build_opencv.py"
|
|
95
|
+
environment-pass = ["UV_EXCLUDE_NEWER"]
|
|
92
96
|
|
|
93
97
|
[tool.cibuildwheel.macos]
|
|
94
98
|
environment = { OpenCV_ROOT = "/tmp/opencv", MACOSX_DEPLOYMENT_TARGET = "11.0" }
|
|
@@ -108,7 +112,7 @@ cache-keys = [
|
|
|
108
112
|
[tool.ruff]
|
|
109
113
|
extend-exclude = ["patchmatch/csrc"]
|
|
110
114
|
line-length = 88
|
|
111
|
-
target-version = "
|
|
115
|
+
target-version = "py311"
|
|
112
116
|
|
|
113
117
|
[tool.ruff.lint]
|
|
114
118
|
select = ["B", "C4", "E", "F", "I", "RUF", "SIM", "UP", "W"]
|
|
@@ -138,7 +142,10 @@ warn_return_any = false
|
|
|
138
142
|
|
|
139
143
|
[tool.pytest.ini_options]
|
|
140
144
|
addopts = ["--cov-branch", "--cov-report=term:skip-covered", "--cov=patchmatch"]
|
|
145
|
+
# shows which test hangs before the job timeout kills the run
|
|
146
|
+
faulthandler_timeout = 300
|
|
141
147
|
markers = ["e2e: end-to-end tests of the user workflows (deselect with -m 'not e2e')"]
|
|
142
|
-
minversion = "
|
|
148
|
+
minversion = "9.0"
|
|
143
149
|
python_files = ["test_*.py"]
|
|
150
|
+
strict = true
|
|
144
151
|
testpaths = ["tests"]
|
|
@@ -4,6 +4,9 @@ The patchmatch library only needs opencv_core. Linking it statically keeps the
|
|
|
4
4
|
wheels small and free of OpenCV's GUI/codec dependencies.
|
|
5
5
|
|
|
6
6
|
Usage: python build_opencv.py (installs into $OpenCV_ROOT)
|
|
7
|
+
|
|
8
|
+
PATCHMATCH_OPENCV_VERSION selects another pinned version, which CI tests because
|
|
9
|
+
builds from the sdist link whatever OpenCV the system has.
|
|
7
10
|
"""
|
|
8
11
|
|
|
9
12
|
import hashlib
|
|
@@ -17,13 +20,20 @@ import tempfile
|
|
|
17
20
|
import urllib.request
|
|
18
21
|
from pathlib import Path
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
# The wheels link this code statically, so every archive is pinned by its SHA-256,
|
|
24
|
+
# e.g. from `sha256sum` or `shasum -a 256`.
|
|
25
|
+
OPENCV_SHA256S = {
|
|
26
|
+
"4.14.0": "ee8fb9b30eb60850431b4656447080e3737b56e45719c92b67f245950609f86e",
|
|
27
|
+
"5.0.0": "b0528f5a1d379d59d4701cb28c36e22214cc51cf64594e5b56f2d3e6c0233095",
|
|
28
|
+
}
|
|
29
|
+
# the version of the release wheels
|
|
30
|
+
OPENCV_VERSION = os.environ.get("PATCHMATCH_OPENCV_VERSION") or "4.14.0"
|
|
31
|
+
if OPENCV_VERSION not in OPENCV_SHA256S:
|
|
32
|
+
sys.exit(f"OpenCV {OPENCV_VERSION} is not pinned, known: {sorted(OPENCV_SHA256S)}")
|
|
33
|
+
OPENCV_SHA256 = OPENCV_SHA256S[OPENCV_VERSION]
|
|
21
34
|
OPENCV_URL = (
|
|
22
35
|
f"https://github.com/opencv/opencv/archive/refs/tags/{OPENCV_VERSION}.tar.gz"
|
|
23
36
|
)
|
|
24
|
-
# The wheels link this code statically, so the archive is pinned by its SHA-256.
|
|
25
|
-
# Update it together with the version, e.g. with `sha256sum` or `shasum -a 256`.
|
|
26
|
-
OPENCV_SHA256 = "ee8fb9b30eb60850431b4656447080e3737b56e45719c92b67f245950609f86e"
|
|
27
37
|
|
|
28
38
|
CMAKE_OPTIONS = {
|
|
29
39
|
"CMAKE_BUILD_TYPE": "Release",
|
|
@@ -155,8 +165,7 @@ def main() -> None:
|
|
|
155
165
|
urllib.request.urlretrieve(OPENCV_URL, archive)
|
|
156
166
|
verify_archive(archive)
|
|
157
167
|
with tarfile.open(archive) as tar:
|
|
158
|
-
# extraction filters are missing on older patch releases
|
|
159
|
-
# last Windows installer of Python 3.10 (3.10.11)
|
|
168
|
+
# extraction filters are missing on older patch releases (3.11 < 3.11.4)
|
|
160
169
|
if hasattr(tarfile, "data_filter"):
|
|
161
170
|
tar.extractall(tmp, filter="data")
|
|
162
171
|
else:
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Property-based tests: the invariants of the inpainting on random inputs."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from hypothesis import given, settings
|
|
5
|
+
from hypothesis import strategies as st
|
|
6
|
+
from hypothesis.extra import numpy as hnp
|
|
7
|
+
|
|
8
|
+
import patchmatch
|
|
9
|
+
|
|
10
|
+
sizes = st.integers(1, 24)
|
|
11
|
+
patch_sizes = st.integers(1, 4)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@st.composite
|
|
15
|
+
def images_and_masks(draw):
|
|
16
|
+
"""An image, a hole mask (None: the white pixels) and an optional global mask."""
|
|
17
|
+
shape = (draw(sizes), draw(sizes))
|
|
18
|
+
image = draw(hnp.arrays(np.uint8, (*shape, 3)))
|
|
19
|
+
mask = draw(st.none() | hnp.arrays(np.bool_, shape))
|
|
20
|
+
global_mask = draw(st.none() | hnp.arrays(np.bool_, shape))
|
|
21
|
+
return image, mask, global_mask
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def white_pixels(image):
|
|
25
|
+
return (image == 255).all(axis=-1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def assert_only_holes_changed(result, image, mask, global_mask):
|
|
29
|
+
assert result.shape == image.shape
|
|
30
|
+
assert result.dtype == np.uint8
|
|
31
|
+
holes = white_pixels(image) if mask is None else mask
|
|
32
|
+
if global_mask is not None:
|
|
33
|
+
holes = holes & ~global_mask
|
|
34
|
+
np.testing.assert_array_equal(result[~holes], image[~holes])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def fills(image, mask, ijmap, **kwargs):
|
|
38
|
+
"""The results of inpaint and inpaint_regularity with the same seed."""
|
|
39
|
+
patchmatch.set_random_seed(0)
|
|
40
|
+
return (
|
|
41
|
+
patchmatch.inpaint(image, mask, **kwargs),
|
|
42
|
+
patchmatch.inpaint_regularity(image, mask, ijmap, **kwargs),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def assert_same_fills(actual, expected):
|
|
47
|
+
for result, reference in zip(actual, expected, strict=True):
|
|
48
|
+
np.testing.assert_array_equal(result, reference)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@settings(deadline=None)
|
|
52
|
+
@given(images_and_masks(), patch_sizes)
|
|
53
|
+
def test_inpaint(inputs, patch_size):
|
|
54
|
+
image, mask, global_mask = inputs
|
|
55
|
+
|
|
56
|
+
def inpaint():
|
|
57
|
+
patchmatch.set_random_seed(0)
|
|
58
|
+
return patchmatch.inpaint(
|
|
59
|
+
image, mask, global_mask=global_mask, patch_size=patch_size
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
result = inpaint()
|
|
63
|
+
assert_only_holes_changed(result, image, mask, global_mask)
|
|
64
|
+
np.testing.assert_array_equal(inpaint(), result)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@settings(deadline=None)
|
|
68
|
+
@given(
|
|
69
|
+
images_and_masks(),
|
|
70
|
+
patch_sizes,
|
|
71
|
+
hnp.arrays(
|
|
72
|
+
np.float32,
|
|
73
|
+
st.tuples(sizes, sizes, st.just(3)),
|
|
74
|
+
elements=st.floats(-2, 2, width=32),
|
|
75
|
+
),
|
|
76
|
+
st.floats(0, 10),
|
|
77
|
+
)
|
|
78
|
+
def test_inpaint_regularity(inputs, patch_size, ijmap, guide_weight):
|
|
79
|
+
image, mask, global_mask = inputs
|
|
80
|
+
result = patchmatch.inpaint_regularity(
|
|
81
|
+
image,
|
|
82
|
+
mask,
|
|
83
|
+
ijmap,
|
|
84
|
+
global_mask=global_mask,
|
|
85
|
+
patch_size=patch_size,
|
|
86
|
+
guide_weight=guide_weight,
|
|
87
|
+
)
|
|
88
|
+
assert_only_holes_changed(result, image, mask, global_mask)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@settings(deadline=None)
|
|
92
|
+
@given(images_and_masks(), hnp.arrays(np.uint8, (24, 24, 3)), patch_sizes)
|
|
93
|
+
def test_colors_under_global_mask_do_not_matter(inputs, colors, patch_size):
|
|
94
|
+
image, mask, global_mask = inputs
|
|
95
|
+
if global_mask is None:
|
|
96
|
+
global_mask = np.zeros(image.shape[:2], dtype=bool)
|
|
97
|
+
if mask is None:
|
|
98
|
+
mask = white_pixels(image)
|
|
99
|
+
colors = colors[: image.shape[0], : image.shape[1]]
|
|
100
|
+
recolored = np.where(global_mask[..., None], colors, image)
|
|
101
|
+
ijmap = image.astype(np.float32) / 255
|
|
102
|
+
kwargs = {"global_mask": global_mask, "patch_size": patch_size}
|
|
103
|
+
|
|
104
|
+
holes = mask & ~global_mask
|
|
105
|
+
actual = fills(recolored, mask, ijmap, **kwargs)
|
|
106
|
+
expected = fills(image, mask, ijmap, **kwargs)
|
|
107
|
+
for result, reference in zip(actual, expected, strict=True):
|
|
108
|
+
np.testing.assert_array_equal(result[holes], reference[holes])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@settings(deadline=None)
|
|
112
|
+
@given(images_and_masks(), hnp.arrays(np.uint8, (24, 24, 3)), patch_sizes)
|
|
113
|
+
def test_colors_under_the_holes_do_not_matter(inputs, colors, patch_size):
|
|
114
|
+
image, mask, global_mask = inputs
|
|
115
|
+
if mask is None:
|
|
116
|
+
mask = white_pixels(image)
|
|
117
|
+
holes = mask if global_mask is None else mask & ~global_mask
|
|
118
|
+
colors = colors[: image.shape[0], : image.shape[1]]
|
|
119
|
+
recolored = np.where(holes[..., None], colors, image)
|
|
120
|
+
ijmap = image.astype(np.float32) / 255
|
|
121
|
+
kwargs = {"global_mask": global_mask, "patch_size": patch_size}
|
|
122
|
+
|
|
123
|
+
assert_same_fills(
|
|
124
|
+
fills(recolored, mask, ijmap, **kwargs), fills(image, mask, ijmap, **kwargs)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@settings(deadline=None)
|
|
129
|
+
@given(images_and_masks(), patch_sizes)
|
|
130
|
+
def test_empty_global_mask_changes_nothing(inputs, patch_size):
|
|
131
|
+
image, mask, _ = inputs
|
|
132
|
+
ijmap = image.astype(np.float32) / 255
|
|
133
|
+
empty = np.zeros(image.shape[:2], dtype=bool)
|
|
134
|
+
|
|
135
|
+
assert_same_fills(
|
|
136
|
+
fills(image, mask, ijmap, global_mask=empty, patch_size=patch_size),
|
|
137
|
+
fills(image, mask, ijmap, patch_size=patch_size),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@settings(deadline=None)
|
|
142
|
+
@given(images_and_masks(), hnp.arrays(np.uint8, (24, 24)), patch_sizes)
|
|
143
|
+
def test_only_the_holes_of_the_mask_matter(inputs, values, patch_size):
|
|
144
|
+
"""Any non-zero value marks a hole, and globally masked pixels are none."""
|
|
145
|
+
image, mask, global_mask = inputs
|
|
146
|
+
if global_mask is None:
|
|
147
|
+
global_mask = np.zeros(image.shape[:2], dtype=bool)
|
|
148
|
+
holes = white_pixels(image) if mask is None else mask
|
|
149
|
+
holes = holes & ~global_mask
|
|
150
|
+
values = values[: image.shape[0], : image.shape[1]]
|
|
151
|
+
# any value under the global mask, a non-zero one in the holes, else zero
|
|
152
|
+
other_mask = np.where(global_mask, values, np.where(holes, values | 1, 0))
|
|
153
|
+
ijmap = image.astype(np.float32) / 255
|
|
154
|
+
kwargs = {"global_mask": global_mask, "patch_size": patch_size}
|
|
155
|
+
|
|
156
|
+
assert_same_fills(
|
|
157
|
+
fills(image, other_mask, ijmap, **kwargs),
|
|
158
|
+
fills(image, mask, ijmap, **kwargs),
|
|
159
|
+
)
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
"""Property-based tests: the invariants of the inpainting on random inputs."""
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
from hypothesis import given, settings
|
|
5
|
-
from hypothesis import strategies as st
|
|
6
|
-
from hypothesis.extra import numpy as hnp
|
|
7
|
-
|
|
8
|
-
import patchmatch
|
|
9
|
-
|
|
10
|
-
sizes = st.integers(1, 24)
|
|
11
|
-
patch_sizes = st.integers(1, 4)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@st.composite
|
|
15
|
-
def images_and_masks(draw):
|
|
16
|
-
"""An image, a hole mask (None: the white pixels) and an optional global mask."""
|
|
17
|
-
shape = (draw(sizes), draw(sizes))
|
|
18
|
-
image = draw(hnp.arrays(np.uint8, (*shape, 3)))
|
|
19
|
-
mask = draw(st.none() | hnp.arrays(np.bool_, shape))
|
|
20
|
-
global_mask = draw(st.none() | hnp.arrays(np.bool_, shape))
|
|
21
|
-
return image, mask, global_mask
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def assert_only_holes_changed(result, image, mask, global_mask):
|
|
25
|
-
assert result.shape == image.shape
|
|
26
|
-
assert result.dtype == np.uint8
|
|
27
|
-
holes = (image == 255).all(axis=-1) if mask is None else mask
|
|
28
|
-
if global_mask is not None:
|
|
29
|
-
holes = holes & ~global_mask
|
|
30
|
-
np.testing.assert_array_equal(result[~holes], image[~holes])
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@settings(deadline=None)
|
|
34
|
-
@given(images_and_masks(), patch_sizes)
|
|
35
|
-
def test_inpaint(inputs, patch_size):
|
|
36
|
-
image, mask, global_mask = inputs
|
|
37
|
-
|
|
38
|
-
def inpaint():
|
|
39
|
-
patchmatch.set_random_seed(0)
|
|
40
|
-
return patchmatch.inpaint(
|
|
41
|
-
image, mask, global_mask=global_mask, patch_size=patch_size
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
result = inpaint()
|
|
45
|
-
assert_only_holes_changed(result, image, mask, global_mask)
|
|
46
|
-
np.testing.assert_array_equal(inpaint(), result)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@settings(deadline=None)
|
|
50
|
-
@given(
|
|
51
|
-
images_and_masks(),
|
|
52
|
-
patch_sizes,
|
|
53
|
-
hnp.arrays(
|
|
54
|
-
np.float32,
|
|
55
|
-
st.tuples(sizes, sizes, st.just(3)),
|
|
56
|
-
elements=st.floats(-2, 2, width=32),
|
|
57
|
-
),
|
|
58
|
-
st.floats(0, 10),
|
|
59
|
-
)
|
|
60
|
-
def test_inpaint_regularity(inputs, patch_size, ijmap, guide_weight):
|
|
61
|
-
image, mask, global_mask = inputs
|
|
62
|
-
result = patchmatch.inpaint_regularity(
|
|
63
|
-
image,
|
|
64
|
-
mask,
|
|
65
|
-
ijmap,
|
|
66
|
-
global_mask=global_mask,
|
|
67
|
-
patch_size=patch_size,
|
|
68
|
-
guide_weight=guide_weight,
|
|
69
|
-
)
|
|
70
|
-
assert_only_holes_changed(result, image, mask, global_mask)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
@settings(deadline=None)
|
|
74
|
-
@given(images_and_masks(), hnp.arrays(np.uint8, (24, 24, 3)), patch_sizes)
|
|
75
|
-
def test_colors_under_global_mask_do_not_matter(inputs, colors, patch_size):
|
|
76
|
-
image, mask, global_mask = inputs
|
|
77
|
-
if global_mask is None:
|
|
78
|
-
global_mask = np.zeros(image.shape[:2], dtype=bool)
|
|
79
|
-
if mask is None:
|
|
80
|
-
mask = (image == 255).all(axis=-1)
|
|
81
|
-
colors = colors[: image.shape[0], : image.shape[1]]
|
|
82
|
-
recolored = np.where(global_mask[..., None], colors, image)
|
|
83
|
-
ijmap = image.astype(np.float32) / 255
|
|
84
|
-
|
|
85
|
-
def fills(source):
|
|
86
|
-
patchmatch.set_random_seed(0)
|
|
87
|
-
kwargs = {"global_mask": global_mask, "patch_size": patch_size}
|
|
88
|
-
return (
|
|
89
|
-
patchmatch.inpaint(source, mask, **kwargs),
|
|
90
|
-
patchmatch.inpaint_regularity(source, mask, ijmap, **kwargs),
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
holes = mask & ~global_mask
|
|
94
|
-
for result, expected in zip(fills(recolored), fills(image), strict=True):
|
|
95
|
-
np.testing.assert_array_equal(result[holes], expected[holes])
|
|
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
|