PyPatchMatch 2.0.1__tar.gz → 2.0.2__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 → pypatchmatch-2.0.2}/CLAUDE.md +7 -4
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/PKG-INFO +8 -7
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/README.md +6 -5
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/_version.py +3 -3
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/csrc/masked_image.cpp +4 -2
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/csrc/nnf.cpp +9 -4
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/patch_match.py +33 -9
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/tests/test_patch_match.py +104 -12
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/CMakeLists.txt +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/LICENSE +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/SECURITY.md +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/examples/images/forest_pruned.bmp +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/__init__.py +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/_lib.py +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/csrc/inpaint.cpp +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/csrc/inpaint.h +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/csrc/masked_image.h +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/csrc/nnf.h +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/csrc/pyinterface.cpp +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/csrc/pyinterface.h +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/patchmatch/py.typed +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/pyproject.toml +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/scripts/build_opencv.py +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/scripts/evaluate_inpainting.py +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/scripts/render_readme_example.py +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/tests/conftest.py +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/tests/test_e2e.py +0 -0
- {pypatchmatch-2.0.1 → pypatchmatch-2.0.2}/tests/test_properties.py +0 -0
|
@@ -84,10 +84,13 @@ Other entry points:
|
|
|
84
84
|
every name in `patch_match.__all__` is re-exported.
|
|
85
85
|
2. `patchmatch/patch_match.py` is the public API. It validates and canonicalizes the
|
|
86
86
|
inputs (`_prepare_inputs`, `_check_patch_size`, the `ijmap` and `guide_weight`
|
|
87
|
-
checks),
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
checks), returns a copy of the image without a native call if `_has_holes` finds no
|
|
88
|
+
hole outside the global mask, picks the entry point (`PM_inpaint`, `PM_inpaint2`
|
|
89
|
+
with a global mask, and their `_regularity` variants) and runs it through `_call`.
|
|
90
|
+
It loads the library at import time; on `OSError` (no library) or `AttributeError`
|
|
91
|
+
(one without the `PM_*` symbols) it logs a warning and sets
|
|
92
|
+
`patchmatch_available = False`, and every function then raises `RuntimeError`
|
|
93
|
+
through `_get_lib()`.
|
|
91
94
|
3. `patchmatch/_lib.py` is the ctypes layer: the `CShapeT`/`CMatT` structures, the dtype
|
|
92
95
|
table, `np_to_pymat`/`pymat_to_np`, `find_library` (looks for `libpatchmatch.so`,
|
|
93
96
|
`libpatchmatch.dylib` or `patchmatch.dll` in every directory of `patchmatch.__path__`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPatchMatch
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
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,7 +42,7 @@ 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
46
|
|
|
47
47
|
From left to right: the photo, the input of
|
|
48
48
|
[examples/py_example.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example.py)
|
|
@@ -99,11 +99,12 @@ The previous import path `from patchmatch import patch_match` keeps working.
|
|
|
99
99
|
pixels in each direction. Larger patches follow larger structures but are much slower;
|
|
100
100
|
the default of 15 compares 31x31 patches, the examples use 3.
|
|
101
101
|
|
|
102
|
-
`patchmatch.set_random_seed(seed)` sets the seed of the randomized search
|
|
103
|
-
`patchmatch.set_verbose(True)` prints the progress of the native code to
|
|
104
|
-
`patchmatch.inpaint_regularity(image, mask, ijmap)` additionally guides the
|
|
105
|
-
a regularity map, an HxWx3 float32 array with the regularity coordinates of
|
|
106
|
-
in its first two channels. Its `guide_weight` must be
|
|
102
|
+
`patchmatch.set_random_seed(seed)` sets the seed of the randomized search, modulo
|
|
103
|
+
`2**32`, and `patchmatch.set_verbose(True)` prints the progress of the native code to
|
|
104
|
+
stderr. `patchmatch.inpaint_regularity(image, mask, ijmap)` additionally guides the
|
|
105
|
+
search with a regularity map, an HxWx3 float32 array with the regularity coordinates of
|
|
106
|
+
each pixel in its first two channels. Its `guide_weight` must be between 0 and the
|
|
107
|
+
largest float32, about 3.4e38.
|
|
107
108
|
|
|
108
109
|
C++ (see
|
|
109
110
|
[examples/cpp_example.cpp](https://github.com/mauwii/PyPatchMatch/blob/main/examples/cpp_example.cpp),
|
|
@@ -69,11 +69,12 @@ The previous import path `from patchmatch import patch_match` keeps working.
|
|
|
69
69
|
pixels in each direction. Larger patches follow larger structures but are much slower;
|
|
70
70
|
the default of 15 compares 31x31 patches, the examples use 3.
|
|
71
71
|
|
|
72
|
-
`patchmatch.set_random_seed(seed)` sets the seed of the randomized search
|
|
73
|
-
`patchmatch.set_verbose(True)` prints the progress of the native code to
|
|
74
|
-
`patchmatch.inpaint_regularity(image, mask, ijmap)` additionally guides the
|
|
75
|
-
a regularity map, an HxWx3 float32 array with the regularity coordinates of
|
|
76
|
-
in its first two channels. Its `guide_weight` must be
|
|
72
|
+
`patchmatch.set_random_seed(seed)` sets the seed of the randomized search, modulo
|
|
73
|
+
`2**32`, and `patchmatch.set_verbose(True)` prints the progress of the native code to
|
|
74
|
+
stderr. `patchmatch.inpaint_regularity(image, mask, ijmap)` additionally guides the
|
|
75
|
+
search with a regularity map, an HxWx3 float32 array with the regularity coordinates of
|
|
76
|
+
each pixel in its first two channels. Its `guide_weight` must be between 0 and the
|
|
77
|
+
largest float32, about 3.4e38.
|
|
77
78
|
|
|
78
79
|
C++ (see
|
|
79
80
|
[examples/cpp_example.cpp](https://github.com/mauwii/PyPatchMatch/blob/main/examples/cpp_example.cpp),
|
|
@@ -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.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 0, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g38b611eb9'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#include "masked_image.h"
|
|
2
2
|
|
|
3
3
|
#include <algorithm>
|
|
4
|
+
#include <cstdint>
|
|
4
5
|
|
|
5
6
|
const cv::Size MaskedImage::kDownsampleKernelSize = cv::Size(6, 6);
|
|
6
7
|
const std::array<int, 6> MaskedImage::kDownsampleKernel = {1, 5, 10, 10, 5, 1};
|
|
@@ -107,8 +108,9 @@ MaskedImage MaskedImage::upsample(int new_w, int new_h) const
|
|
|
107
108
|
{
|
|
108
109
|
for (int x = 0; x < new_w; ++x)
|
|
109
110
|
{
|
|
110
|
-
|
|
111
|
-
const
|
|
111
|
+
// In 64 bits: the products overflow an int from 65,537 pixels on.
|
|
112
|
+
const auto yy = static_cast<int>(std::int64_t{y} * size.height / new_h);
|
|
113
|
+
const auto xx = static_cast<int>(std::int64_t{x} * size.width / new_w);
|
|
112
114
|
|
|
113
115
|
if (is_globally_masked(yy, xx))
|
|
114
116
|
{
|
|
@@ -296,12 +296,17 @@ int RegularityGuidedPatchDistanceMetricV2::operator()(
|
|
|
296
296
|
if (target_y < 0 || target_y >= target.size().height || target_x < 0 || target_x >= target.size().width)
|
|
297
297
|
return PatchDistanceMetric::kDistanceScale;
|
|
298
298
|
|
|
299
|
-
// Map pyramid-level coordinates to the full-resolution ijmap
|
|
300
|
-
//
|
|
299
|
+
// Map pyramid-level coordinates to the full-resolution ijmap, height and width
|
|
300
|
+
// independently; a coordinate inside the image stays inside the map. In 64 bits:
|
|
301
|
+
// the products overflow an int from 46,342 pixels on.
|
|
301
302
|
const int map_h = m_ijmap.size().height;
|
|
302
303
|
const int map_w = m_ijmap.size().width;
|
|
303
|
-
auto map_y = [&](const MaskedImage &img, int y) {
|
|
304
|
-
|
|
304
|
+
auto map_y = [&](const MaskedImage &img, int y) {
|
|
305
|
+
return static_cast<int>(std::int64_t{y} * map_h / img.size().height);
|
|
306
|
+
};
|
|
307
|
+
auto map_x = [&](const MaskedImage &img, int x) {
|
|
308
|
+
return static_cast<int>(std::int64_t{x} * map_w / img.size().width);
|
|
309
|
+
};
|
|
305
310
|
|
|
306
311
|
double score1 = PatchDistanceMetric::kDistanceScale;
|
|
307
312
|
if (!source.is_globally_masked(source_y, source_x) && !target.is_globally_masked(target_y, target_x))
|
|
@@ -17,7 +17,6 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
import ctypes
|
|
19
19
|
import logging
|
|
20
|
-
import math
|
|
21
20
|
import operator
|
|
22
21
|
from collections.abc import Callable
|
|
23
22
|
from typing import SupportsIndex, TypeAlias
|
|
@@ -44,7 +43,9 @@ ImageLike: TypeAlias = np.ndarray | Image.Image
|
|
|
44
43
|
|
|
45
44
|
try:
|
|
46
45
|
_lib: ctypes.CDLL | None = load_library()
|
|
47
|
-
except OSError as e:
|
|
46
|
+
except (OSError, AttributeError) as e:
|
|
47
|
+
# AttributeError: a library without the PM_* functions, e.g. one that 1.x
|
|
48
|
+
# compiled into the package directory and that pip does not remove on upgrade
|
|
48
49
|
_lib = None
|
|
49
50
|
logger.warning("patchmatch failed to load: %s", e)
|
|
50
51
|
|
|
@@ -54,6 +55,9 @@ patchmatch_available = _lib is not None
|
|
|
54
55
|
# The native code computes 2 * patch_size + 1 as a C int.
|
|
55
56
|
_MAX_PATCH_SIZE = (2**31 - 2) // 2
|
|
56
57
|
|
|
58
|
+
# The native code takes a C float, which turns larger weights into infinity.
|
|
59
|
+
_MAX_GUIDE_WEIGHT = float(np.finfo(np.float32).max)
|
|
60
|
+
|
|
57
61
|
|
|
58
62
|
def _get_lib() -> ctypes.CDLL:
|
|
59
63
|
if _lib is None:
|
|
@@ -62,10 +66,17 @@ def _get_lib() -> ctypes.CDLL:
|
|
|
62
66
|
|
|
63
67
|
|
|
64
68
|
def set_random_seed(seed: int) -> None:
|
|
69
|
+
"""Set the seed of the randomized search of all following inpaintings.
|
|
70
|
+
|
|
71
|
+
Every inpainting starts from this seed, so equal inputs give equal results. The
|
|
72
|
+
native code keeps it as a 32-bit unsigned int, so ``seed`` is taken modulo
|
|
73
|
+
``2**32``: ``2**32`` behaves like 0 and -1 like ``2**32 - 1``.
|
|
74
|
+
"""
|
|
65
75
|
_get_lib().PM_set_random_seed(ctypes.c_uint(seed))
|
|
66
76
|
|
|
67
77
|
|
|
68
78
|
def set_verbose(verbose: bool) -> None:
|
|
79
|
+
"""Print the progress of all following inpaintings to stderr if ``verbose``."""
|
|
69
80
|
_get_lib().PM_set_verbose(ctypes.c_int(verbose))
|
|
70
81
|
|
|
71
82
|
|
|
@@ -96,8 +107,7 @@ def inpaint(
|
|
|
96
107
|
lib = _get_lib()
|
|
97
108
|
patch_size = _check_patch_size(patch_size)
|
|
98
109
|
image, mask, global_mask = _prepare_inputs(image, mask, global_mask)
|
|
99
|
-
|
|
100
|
-
if image.size == 0:
|
|
110
|
+
if not _has_holes(mask, global_mask):
|
|
101
111
|
return image.copy()
|
|
102
112
|
|
|
103
113
|
if global_mask is None:
|
|
@@ -140,15 +150,16 @@ def inpaint_regularity(
|
|
|
140
150
|
raise ValueError("ijmap must only contain finite values")
|
|
141
151
|
ijmap = np.ascontiguousarray(ijmap)
|
|
142
152
|
|
|
143
|
-
# the native code divides by 1 + guide_weight and indexes a table with the result
|
|
153
|
+
# the native code divides by 1 + guide_weight and indexes a table with the result;
|
|
154
|
+
# the comparison also rejects NaN
|
|
144
155
|
guide_weight = float(guide_weight)
|
|
145
|
-
if not
|
|
156
|
+
if not 0 <= guide_weight <= _MAX_GUIDE_WEIGHT:
|
|
146
157
|
raise ValueError(
|
|
147
|
-
f"guide_weight must be
|
|
158
|
+
f"guide_weight must be between 0 and {_MAX_GUIDE_WEIGHT:g}, "
|
|
159
|
+
f"got {guide_weight}"
|
|
148
160
|
)
|
|
149
161
|
|
|
150
|
-
|
|
151
|
-
if image.size == 0:
|
|
162
|
+
if not _has_holes(mask, global_mask):
|
|
152
163
|
return image.copy()
|
|
153
164
|
|
|
154
165
|
args = (ijmap, ctypes.c_int(patch_size), ctypes.c_float(guide_weight))
|
|
@@ -194,6 +205,19 @@ def _prepare_inputs(
|
|
|
194
205
|
return image, mask, global_mask
|
|
195
206
|
|
|
196
207
|
|
|
208
|
+
def _has_holes(mask: np.ndarray, global_mask: np.ndarray | None) -> bool:
|
|
209
|
+
"""Whether a hole lies outside the global mask.
|
|
210
|
+
|
|
211
|
+
Otherwise the result equals the input, but the native code still runs every
|
|
212
|
+
level of the pyramid. This also covers empty images, which OpenCV 4.6 would
|
|
213
|
+
return with the shape (0, 0, 1).
|
|
214
|
+
"""
|
|
215
|
+
holes = mask != 0
|
|
216
|
+
if global_mask is not None:
|
|
217
|
+
holes &= global_mask == 0
|
|
218
|
+
return bool(holes.any())
|
|
219
|
+
|
|
220
|
+
|
|
197
221
|
def _call(func: Callable[..., CMatT], *args: object) -> np.ndarray:
|
|
198
222
|
"""Call ``func`` with arrays converted to pymats and copy the result."""
|
|
199
223
|
lib = _get_lib()
|
|
@@ -14,6 +14,7 @@ from patchmatch import _lib, patch_match
|
|
|
14
14
|
|
|
15
15
|
HEIGHT, WIDTH = 48, 64
|
|
16
16
|
HOLE = (slice(20, 28), slice(24, 36))
|
|
17
|
+
AROUND_HOLE = (slice(14, 34), slice(18, 42))
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
@pytest.fixture
|
|
@@ -27,6 +28,21 @@ def image() -> np.ndarray:
|
|
|
27
28
|
return img
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def regular_image() -> np.ndarray:
|
|
33
|
+
"""Noisy pattern that repeats every 8 pixels, as ijmap describes, with the hole.
|
|
34
|
+
|
|
35
|
+
The regularity term copies from whole periods away, which in the gradient of
|
|
36
|
+
``image`` shifted the fill by up to 35 color values.
|
|
37
|
+
"""
|
|
38
|
+
rng = np.random.default_rng(0)
|
|
39
|
+
tile = rng.integers(80, 120, (8, 8, 3))
|
|
40
|
+
noise = rng.integers(0, 8, (HEIGHT, WIDTH, 3))
|
|
41
|
+
img = (np.tile(tile, (HEIGHT // 8, WIDTH // 8, 1)) + noise).astype(np.uint8)
|
|
42
|
+
img[HOLE] = 255
|
|
43
|
+
return img
|
|
44
|
+
|
|
45
|
+
|
|
30
46
|
@pytest.fixture
|
|
31
47
|
def hole_mask() -> np.ndarray:
|
|
32
48
|
mask = np.zeros((HEIGHT, WIDTH), dtype=np.uint8)
|
|
@@ -48,14 +64,21 @@ def seed():
|
|
|
48
64
|
|
|
49
65
|
|
|
50
66
|
def assert_filled(result: np.ndarray, source: np.ndarray) -> None:
|
|
51
|
-
"""The hole is filled and all other pixels keep their values."""
|
|
67
|
+
"""The hole is filled to blend in, and all other pixels keep their values."""
|
|
52
68
|
assert result.shape == source.shape
|
|
53
69
|
assert result.dtype == np.uint8
|
|
54
|
-
assert not (result[HOLE] == 255).all(axis=-1).any()
|
|
55
70
|
known = np.ones(source.shape[:2], dtype=bool)
|
|
56
71
|
known[HOLE] = False
|
|
57
72
|
np.testing.assert_array_equal(result[known], source[known])
|
|
58
73
|
|
|
74
|
+
# a hole that is not filled stays black, see _initialize_pyramid
|
|
75
|
+
ring = np.zeros_like(known)
|
|
76
|
+
ring[AROUND_HOLE] = True
|
|
77
|
+
ring[HOLE] = False
|
|
78
|
+
fill = result[HOLE].reshape(-1, 3).mean(axis=0)
|
|
79
|
+
color_diff = np.abs(fill - source[ring].mean(axis=0))
|
|
80
|
+
assert color_diff.max() < 10, color_diff
|
|
81
|
+
|
|
59
82
|
|
|
60
83
|
# --- package ----------------------------------------------------------------
|
|
61
84
|
|
|
@@ -83,11 +106,7 @@ def test_inpaint_default_mask_fills_white_pixels(image):
|
|
|
83
106
|
|
|
84
107
|
def test_inpaint_explicit_mask(image, hole_mask):
|
|
85
108
|
image[HOLE] = 0 # not white: only the explicit mask marks the hole
|
|
86
|
-
|
|
87
|
-
assert result.shape == image.shape
|
|
88
|
-
assert (result[HOLE] != 0).any()
|
|
89
|
-
known = hole_mask == 0
|
|
90
|
-
np.testing.assert_array_equal(result[known], image[known])
|
|
109
|
+
assert_filled(patchmatch.inpaint(image, hole_mask, patch_size=3), image)
|
|
91
110
|
|
|
92
111
|
|
|
93
112
|
def test_inpaint_global_mask(image, hole_mask):
|
|
@@ -127,6 +146,33 @@ def test_inpaint_empty_image(shape, ijmap):
|
|
|
127
146
|
assert result is not empty
|
|
128
147
|
|
|
129
148
|
|
|
149
|
+
@pytest.mark.parametrize("covered", [False, True], ids=["no-hole", "global-mask"])
|
|
150
|
+
@pytest.mark.parametrize(
|
|
151
|
+
"func",
|
|
152
|
+
[patchmatch.inpaint, patchmatch.inpaint_regularity],
|
|
153
|
+
ids=lambda f: f.__name__,
|
|
154
|
+
)
|
|
155
|
+
def test_input_without_holes_skips_the_native_code(
|
|
156
|
+
monkeypatch, image, hole_mask, ijmap, func, covered
|
|
157
|
+
):
|
|
158
|
+
"""The result equals the input, but the native code ran every level.
|
|
159
|
+
|
|
160
|
+
That took 14 s for a 512 x 512 image with the default patch size.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def native_call(*args):
|
|
164
|
+
pytest.fail("the native code was called")
|
|
165
|
+
|
|
166
|
+
monkeypatch.setattr(patch_match, "_call", native_call)
|
|
167
|
+
mask = hole_mask if covered else np.zeros_like(hole_mask)
|
|
168
|
+
kwargs = {"ijmap": ijmap} if func is patchmatch.inpaint_regularity else {}
|
|
169
|
+
|
|
170
|
+
result = func(image, mask, global_mask=hole_mask if covered else None, **kwargs)
|
|
171
|
+
|
|
172
|
+
np.testing.assert_array_equal(result, image)
|
|
173
|
+
assert not np.shares_memory(result, image)
|
|
174
|
+
|
|
175
|
+
|
|
130
176
|
@pytest.mark.parametrize("patch_size", [1, 3, 7])
|
|
131
177
|
def test_inpaint_patch_sizes(image, patch_size):
|
|
132
178
|
assert_filled(patchmatch.inpaint(image, patch_size=patch_size), image)
|
|
@@ -154,6 +200,30 @@ def test_inpaint_thin_hole_takes_the_colors_beside_it():
|
|
|
154
200
|
assert np.abs(fill - plain).max() < 10, (seed, fill)
|
|
155
201
|
|
|
156
202
|
|
|
203
|
+
@pytest.mark.parametrize("regularity", [False, True], ids=["inpaint", "regularity"])
|
|
204
|
+
@pytest.mark.parametrize("tall", [True, False], ids=["tall", "wide"])
|
|
205
|
+
def test_inpaint_very_large_image(tall, regularity):
|
|
206
|
+
"""Coordinate products overflowed a C int near the end of very large images.
|
|
207
|
+
|
|
208
|
+
The regularity metric crashed from 46,342 pixels on, and the upsampling filled
|
|
209
|
+
a hole near the bottom black from 65,537 pixels on.
|
|
210
|
+
"""
|
|
211
|
+
image = np.random.default_rng(0).integers(90, 110, (70_000, 4, 3), dtype=np.uint8)
|
|
212
|
+
image[-30:-20, 1:3] = 255
|
|
213
|
+
if not tall:
|
|
214
|
+
image = image.transpose(1, 0, 2)
|
|
215
|
+
holes = (image == 255).all(axis=2)
|
|
216
|
+
|
|
217
|
+
if regularity:
|
|
218
|
+
ijmap = np.zeros(image.shape, dtype=np.float32)
|
|
219
|
+
result = patchmatch.inpaint_regularity(image, None, ijmap, patch_size=1)
|
|
220
|
+
else:
|
|
221
|
+
result = patchmatch.inpaint(image, patch_size=1)
|
|
222
|
+
|
|
223
|
+
np.testing.assert_array_equal(result[~holes], image[~holes])
|
|
224
|
+
assert np.abs(result[holes].mean(axis=0) - 100).max() < 10
|
|
225
|
+
|
|
226
|
+
|
|
157
227
|
def test_inpaint_does_not_modify_inputs(image, hole_mask):
|
|
158
228
|
image_before, mask_before = image.copy(), hole_mask.copy()
|
|
159
229
|
patchmatch.inpaint(image, hole_mask, global_mask=hole_mask, patch_size=3)
|
|
@@ -205,19 +275,19 @@ def test_set_verbose_prints_progress(verbose):
|
|
|
205
275
|
@pytest.mark.parametrize("use_global_mask", [False, True], ids=["", "global"])
|
|
206
276
|
@pytest.mark.parametrize("guide_weight", [0.0, 0.25, 1.0])
|
|
207
277
|
def test_inpaint_regularity(
|
|
208
|
-
|
|
278
|
+
regular_image, hole_mask, ijmap, use_mask, use_global_mask, guide_weight
|
|
209
279
|
):
|
|
210
280
|
global_mask = np.zeros_like(hole_mask)
|
|
211
281
|
global_mask[:8] = 1
|
|
212
282
|
result = patchmatch.inpaint_regularity(
|
|
213
|
-
|
|
283
|
+
regular_image,
|
|
214
284
|
hole_mask if use_mask else None,
|
|
215
285
|
ijmap,
|
|
216
286
|
global_mask=global_mask if use_global_mask else None,
|
|
217
287
|
patch_size=3,
|
|
218
288
|
guide_weight=guide_weight,
|
|
219
289
|
)
|
|
220
|
-
assert_filled(result,
|
|
290
|
+
assert_filled(result, regular_image)
|
|
221
291
|
|
|
222
292
|
|
|
223
293
|
def test_inpaint_regularity_scales_smaller_ijmap(image):
|
|
@@ -347,10 +417,10 @@ def test_inpaint_regularity_invalid_ijmap(image, bad_ijmap):
|
|
|
347
417
|
patchmatch.inpaint_regularity(image, None, bad_ijmap)
|
|
348
418
|
|
|
349
419
|
|
|
350
|
-
@pytest.mark.parametrize("guide_weight", [-1.0, -0.5, float("nan"), float("inf")])
|
|
420
|
+
@pytest.mark.parametrize("guide_weight", [-1.0, -0.5, float("nan"), float("inf"), 1e39])
|
|
351
421
|
def test_inpaint_regularity_invalid_guide_weight(image, ijmap, guide_weight):
|
|
352
422
|
# a weight of -1 used to crash the native code, other negative weights made it
|
|
353
|
-
# read outside of the similarity table
|
|
423
|
+
# read outside of the similarity table, and 1e39 became infinity as a C float
|
|
354
424
|
with pytest.raises(ValueError, match="guide_weight"):
|
|
355
425
|
patchmatch.inpaint_regularity(image, None, ijmap, guide_weight=guide_weight)
|
|
356
426
|
|
|
@@ -447,6 +517,28 @@ def test_import_without_native_library():
|
|
|
447
517
|
assert "--no-cache-dir" in process.stderr
|
|
448
518
|
|
|
449
519
|
|
|
520
|
+
def test_import_with_incompatible_library():
|
|
521
|
+
"""A library without the PM_* functions must not break the import either.
|
|
522
|
+
|
|
523
|
+
1.x compiled the library into the package directory, and pip keeps it there.
|
|
524
|
+
Pillow's extension module stands in for it.
|
|
525
|
+
"""
|
|
526
|
+
code = (
|
|
527
|
+
"import importlib\n"
|
|
528
|
+
"from pathlib import Path\n"
|
|
529
|
+
"import PIL._imaging\n"
|
|
530
|
+
"from patchmatch import _lib, patch_match\n"
|
|
531
|
+
"_lib.find_library = lambda: Path(PIL._imaging.__file__)\n"
|
|
532
|
+
"importlib.reload(patch_match)\n"
|
|
533
|
+
"assert not patch_match.patchmatch_available\n"
|
|
534
|
+
)
|
|
535
|
+
process = subprocess.run(
|
|
536
|
+
[sys.executable, "-c", code], capture_output=True, text=True, check=True
|
|
537
|
+
)
|
|
538
|
+
assert "patchmatch failed to load" in process.stderr
|
|
539
|
+
assert "PM_" in process.stderr
|
|
540
|
+
|
|
541
|
+
|
|
450
542
|
# --- ctypes conversion ------------------------------------------------------
|
|
451
543
|
|
|
452
544
|
|
|
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
|