PyPatchMatch 2.0.0b2__tar.gz → 2.0.0rc1__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.0b2 → pypatchmatch-2.0.0rc1}/PKG-INFO +19 -5
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/README.md +18 -4
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/_lib.py +8 -2
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/_version.py +3 -3
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/inpaint.cpp +8 -4
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/nnf.cpp +7 -23
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/nnf.h +5 -31
- pypatchmatch-2.0.0rc1/patchmatch/csrc/pyinterface.cpp +214 -0
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/pyinterface.h +6 -0
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/patch_match.py +51 -13
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/pyproject.toml +9 -0
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/scripts/build_opencv.py +17 -0
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/tests/test_e2e.py +2 -3
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/tests/test_patch_match.py +82 -2
- pypatchmatch-2.0.0b2/patchmatch/csrc/pyinterface.cpp +0 -132
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/CMakeLists.txt +0 -0
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/LICENSE +0 -0
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/examples/images/forest_pruned.bmp +0 -0
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/__init__.py +0 -0
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/inpaint.h +0 -0
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/masked_image.cpp +0 -0
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/masked_image.h +0 -0
- {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPatchMatch
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0rc1
|
|
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>
|
|
@@ -52,6 +52,15 @@ OpenCV installation is needed. On other platforms, e.g. Alpine Linux or Windows
|
|
|
52
52
|
pip builds from the source distribution, which requires a C++17 compiler, CMake and the
|
|
53
53
|
OpenCV development files (e.g. `apt install libopencv-dev` or `brew install opencv`).
|
|
54
54
|
|
|
55
|
+
If that build fails, PyPatchMatch is installed without the native library instead of
|
|
56
|
+
blocking the installation: `patchmatch.patchmatch_available` is `False` and the
|
|
57
|
+
inpainting functions raise `RuntimeError`. After installing the missing tools, reinstall
|
|
58
|
+
it without the cached build:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
pip install --force-reinstall --no-deps --no-cache-dir PyPatchMatch
|
|
62
|
+
```
|
|
63
|
+
|
|
55
64
|
## Usage
|
|
56
65
|
|
|
57
66
|
Python (see
|
|
@@ -66,17 +75,22 @@ result = patchmatch.inpaint(image, mask, patch_size=3)
|
|
|
66
75
|
```
|
|
67
76
|
|
|
68
77
|
The mask must have the same height and width as the image. If `mask` is omitted, all
|
|
69
|
-
pure white pixels are treated as holes.
|
|
70
|
-
|
|
71
|
-
their values (see
|
|
78
|
+
pure white pixels are treated as holes. Only the holes are filled; all other pixels keep
|
|
79
|
+
their values. The optional keyword argument `global_mask`, in the same format, marks
|
|
80
|
+
pixels that are neither filled nor used as a source; they keep their values as well (see
|
|
72
81
|
[examples/py_example_global_mask.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example_global_mask.py)).
|
|
73
82
|
`patchmatch.patchmatch_available` tells whether the native library could be loaded.
|
|
74
83
|
The previous import path `from patchmatch import patch_match` keeps working.
|
|
75
84
|
|
|
85
|
+
`patch_size` is the radius of the compared patches, which span `2 * patch_size + 1`
|
|
86
|
+
pixels in each direction. Larger patches follow larger structures but are much slower;
|
|
87
|
+
the default of 15 compares 31x31 patches, the examples use 3.
|
|
88
|
+
|
|
76
89
|
`patchmatch.set_random_seed(seed)` sets the seed of the randomized search and
|
|
77
90
|
`patchmatch.set_verbose(True)` prints the progress of the native code to stderr.
|
|
78
91
|
`patchmatch.inpaint_regularity(image, mask, ijmap)` additionally guides the search with
|
|
79
|
-
a regularity map, an HxWx3 float32 array with the regularity coordinates of each pixel
|
|
92
|
+
a regularity map, an HxWx3 float32 array with the regularity coordinates of each pixel
|
|
93
|
+
in its first two channels. Its `guide_weight` must be at least 0.
|
|
80
94
|
|
|
81
95
|
C++ (see
|
|
82
96
|
[examples/cpp_example.cpp](https://github.com/mauwii/PyPatchMatch/blob/main/examples/cpp_example.cpp),
|
|
@@ -22,6 +22,15 @@ OpenCV installation is needed. On other platforms, e.g. Alpine Linux or Windows
|
|
|
22
22
|
pip builds from the source distribution, which requires a C++17 compiler, CMake and the
|
|
23
23
|
OpenCV development files (e.g. `apt install libopencv-dev` or `brew install opencv`).
|
|
24
24
|
|
|
25
|
+
If that build fails, PyPatchMatch is installed without the native library instead of
|
|
26
|
+
blocking the installation: `patchmatch.patchmatch_available` is `False` and the
|
|
27
|
+
inpainting functions raise `RuntimeError`. After installing the missing tools, reinstall
|
|
28
|
+
it without the cached build:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pip install --force-reinstall --no-deps --no-cache-dir PyPatchMatch
|
|
32
|
+
```
|
|
33
|
+
|
|
25
34
|
## Usage
|
|
26
35
|
|
|
27
36
|
Python (see
|
|
@@ -36,17 +45,22 @@ result = patchmatch.inpaint(image, mask, patch_size=3)
|
|
|
36
45
|
```
|
|
37
46
|
|
|
38
47
|
The mask must have the same height and width as the image. If `mask` is omitted, all
|
|
39
|
-
pure white pixels are treated as holes.
|
|
40
|
-
|
|
41
|
-
their values (see
|
|
48
|
+
pure white pixels are treated as holes. Only the holes are filled; all other pixels keep
|
|
49
|
+
their values. The optional keyword argument `global_mask`, in the same format, marks
|
|
50
|
+
pixels that are neither filled nor used as a source; they keep their values as well (see
|
|
42
51
|
[examples/py_example_global_mask.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example_global_mask.py)).
|
|
43
52
|
`patchmatch.patchmatch_available` tells whether the native library could be loaded.
|
|
44
53
|
The previous import path `from patchmatch import patch_match` keeps working.
|
|
45
54
|
|
|
55
|
+
`patch_size` is the radius of the compared patches, which span `2 * patch_size + 1`
|
|
56
|
+
pixels in each direction. Larger patches follow larger structures but are much slower;
|
|
57
|
+
the default of 15 compares 31x31 patches, the examples use 3.
|
|
58
|
+
|
|
46
59
|
`patchmatch.set_random_seed(seed)` sets the seed of the randomized search and
|
|
47
60
|
`patchmatch.set_verbose(True)` prints the progress of the native code to stderr.
|
|
48
61
|
`patchmatch.inpaint_regularity(image, mask, ijmap)` additionally guides the search with
|
|
49
|
-
a regularity map, an HxWx3 float32 array with the regularity coordinates of each pixel
|
|
62
|
+
a regularity map, an HxWx3 float32 array with the regularity coordinates of each pixel
|
|
63
|
+
in its first two channels. Its `guide_weight` must be at least 0.
|
|
50
64
|
|
|
51
65
|
C++ (see
|
|
52
66
|
[examples/cpp_example.cpp](https://github.com/mauwii/PyPatchMatch/blob/main/examples/cpp_example.cpp),
|
|
@@ -80,9 +80,13 @@ def find_library() -> Path:
|
|
|
80
80
|
candidate = Path(directory) / LIBRARY_NAME
|
|
81
81
|
if candidate.is_file():
|
|
82
82
|
return candidate
|
|
83
|
+
# Without a wheel for the platform, a failed build from the sdist installs the
|
|
84
|
+
# package without the library, see the overrides in pyproject.toml.
|
|
83
85
|
raise OSError(
|
|
84
|
-
f"{LIBRARY_NAME} not found in {list(package.__path__)}
|
|
85
|
-
"
|
|
86
|
+
f"{LIBRARY_NAME} not found in {list(package.__path__)}. Without a wheel for "
|
|
87
|
+
"this platform, PyPatchMatch is built from source, which needs a C++ "
|
|
88
|
+
"compiler, CMake and OpenCV. Install them and reinstall it without the "
|
|
89
|
+
"cache: pip install --force-reinstall --no-deps --no-cache-dir pypatchmatch"
|
|
86
90
|
)
|
|
87
91
|
|
|
88
92
|
|
|
@@ -95,6 +99,8 @@ def load_library() -> ctypes.CDLL:
|
|
|
95
99
|
lib.PM_set_verbose.restype = None
|
|
96
100
|
lib.PM_free_pymat.argtypes = [CMatT]
|
|
97
101
|
lib.PM_free_pymat.restype = None
|
|
102
|
+
lib.PM_last_error.argtypes = []
|
|
103
|
+
lib.PM_last_error.restype = ctypes.c_char_p
|
|
98
104
|
|
|
99
105
|
lib.PM_inpaint.argtypes = [CMatT, CMatT, ctypes.c_int]
|
|
100
106
|
lib.PM_inpaint2.argtypes = [CMatT, CMatT, CMatT, ctypes.c_int]
|
|
@@ -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.0rc1'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 0, 0, 'rc1')
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g75098309c'
|
|
@@ -44,7 +44,6 @@ namespace
|
|
|
44
44
|
auto source_ptr = source.get_image(ys, xs);
|
|
45
45
|
auto target_ptr = target.ptr<double>(yt, xt);
|
|
46
46
|
|
|
47
|
-
#pragma unroll
|
|
48
47
|
for (int c = 0; c < 3; ++c)
|
|
49
48
|
target_ptr[c] += static_cast<double>(source_ptr[c]) * weight;
|
|
50
49
|
target_ptr[3] += weight;
|
|
@@ -126,11 +125,16 @@ cv::Mat Inpainting::run(bool verbose, bool verbose_visualize, unsigned int rando
|
|
|
126
125
|
target = _expectation_maximization(source, target, level, verbose);
|
|
127
126
|
}
|
|
128
127
|
|
|
129
|
-
//
|
|
130
|
-
//
|
|
128
|
+
// Only the holes are filled. The votes of overlapping patches also change the known
|
|
129
|
+
// pixels near the holes, and the pyramid leaves globally masked pixels black, which
|
|
130
|
+
// are neither filled nor used as a source. Keep the input values of both.
|
|
131
131
|
cv::Mat result = target.image();
|
|
132
|
+
if (result.empty()) // OpenCV rejects empty operands in matrix expressions
|
|
133
|
+
return result;
|
|
134
|
+
cv::Mat keep = m_initial.mask() == 0;
|
|
132
135
|
if (!m_initial.global_mask().empty())
|
|
133
|
-
|
|
136
|
+
keep |= m_initial.global_mask() != 0;
|
|
137
|
+
m_initial.image().copyTo(result, keep);
|
|
134
138
|
return result;
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -287,27 +287,6 @@ int PatchSSDDistanceMetric::operator()(const MaskedImage &source, int source_y,
|
|
|
287
287
|
return distance_masked_images(source, source_y, source_x, target, target_y, target_x, m_patch_size);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
int DebugPatchSSDDistanceMetric::operator()(const MaskedImage &source, int source_y, int source_x, const MaskedImage &target, int target_y, int target_x) const
|
|
291
|
-
{
|
|
292
|
-
fprintf(stderr, "DebugPatchSSDDistanceMetric: %d %d %d %d\n", source.size().width, source.size().height, m_width, m_height);
|
|
293
|
-
return distance_masked_images(source, source_y, source_x, target, target_y, target_x, m_patch_size);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
int RegularityGuidedPatchDistanceMetricV1::operator()(const MaskedImage &source, int source_y, int source_x, const MaskedImage &target, int target_y, int target_x) const
|
|
297
|
-
{
|
|
298
|
-
double dx = remainder(double(source_x - target_x) / source.size().width, m_dx1);
|
|
299
|
-
double dy = remainder(double(source_y - target_y) / source.size().height, m_dy2);
|
|
300
|
-
|
|
301
|
-
double score1 = sqrt(dx * dx + dy * dy) / m_scale;
|
|
302
|
-
if (score1 < 0 || score1 > 1)
|
|
303
|
-
score1 = 1;
|
|
304
|
-
score1 *= PatchDistanceMetric::kDistanceScale;
|
|
305
|
-
|
|
306
|
-
double score2 = distance_masked_images(source, source_y, source_x, target, target_y, target_x, m_patch_size);
|
|
307
|
-
double score = score1 * m_weight + score2 / (1 + m_weight);
|
|
308
|
-
return static_cast<int>(score / (1 + m_weight));
|
|
309
|
-
}
|
|
310
|
-
|
|
311
290
|
int RegularityGuidedPatchDistanceMetricV2::operator()(const MaskedImage &source, int source_y, int source_x, const MaskedImage &target, int target_y, int target_x) const
|
|
312
291
|
{
|
|
313
292
|
if (target_y < 0 || target_y >= target.size().height || target_x < 0 || target_x >= target.size().width)
|
|
@@ -338,6 +317,11 @@ int RegularityGuidedPatchDistanceMetricV2::operator()(const MaskedImage &source,
|
|
|
338
317
|
}
|
|
339
318
|
|
|
340
319
|
double score2 = distance_masked_images(source, source_y, source_x, target, target_y, target_x, m_patch_size);
|
|
341
|
-
double score = score1 * m_weight + score2;
|
|
342
|
-
|
|
320
|
+
double score = (score1 * m_weight + score2) / (1 + m_weight);
|
|
321
|
+
// The distance indexes the similarity table in Inpainting, so keep it in range.
|
|
322
|
+
if (!(score > 0))
|
|
323
|
+
return 0;
|
|
324
|
+
if (score >= PatchDistanceMetric::kDistanceScale)
|
|
325
|
+
return PatchDistanceMetric::kDistanceScale;
|
|
326
|
+
return static_cast<int>(score);
|
|
343
327
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
|
-
#include <
|
|
4
|
-
#include <cmath>
|
|
3
|
+
#include <stdexcept>
|
|
5
4
|
#include <opencv2/core.hpp>
|
|
6
5
|
#include "masked_image.h"
|
|
7
6
|
|
|
@@ -116,44 +115,19 @@ public:
|
|
|
116
115
|
static const int kSSDScale;
|
|
117
116
|
};
|
|
118
117
|
|
|
119
|
-
class DebugPatchSSDDistanceMetric : public PatchDistanceMetric
|
|
120
|
-
{
|
|
121
|
-
public:
|
|
122
|
-
DebugPatchSSDDistanceMetric(int patch_size, int width, int height) : PatchDistanceMetric(patch_size), m_width(width), m_height(height) {}
|
|
123
|
-
virtual int operator()(const MaskedImage &source, int source_y, int source_x, const MaskedImage &target, int target_y, int target_x) const;
|
|
124
|
-
|
|
125
|
-
protected:
|
|
126
|
-
int m_width, m_height;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
class RegularityGuidedPatchDistanceMetricV1 : public PatchDistanceMetric
|
|
130
|
-
{
|
|
131
|
-
public:
|
|
132
|
-
RegularityGuidedPatchDistanceMetricV1(int patch_size, double dx1, double dy1, double dx2, double dy2, double weight)
|
|
133
|
-
: PatchDistanceMetric(patch_size), m_dx1(dx1), m_dy1(dy1), m_dx2(dx2), m_dy2(dy2), m_weight(weight)
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
assert(m_dy1 == 0);
|
|
137
|
-
assert(m_dx2 == 0);
|
|
138
|
-
m_scale = sqrt(m_dx1 * m_dx1 + m_dy2 * m_dy2) / 4;
|
|
139
|
-
}
|
|
140
|
-
virtual int operator()(const MaskedImage &source, int source_y, int source_x, const MaskedImage &target, int target_y, int target_x) const;
|
|
141
|
-
|
|
142
|
-
protected:
|
|
143
|
-
double m_dx1, m_dy1, m_dx2, m_dy2;
|
|
144
|
-
double m_scale, m_weight;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
118
|
class RegularityGuidedPatchDistanceMetricV2 : public PatchDistanceMetric
|
|
148
119
|
{
|
|
149
120
|
public:
|
|
150
121
|
RegularityGuidedPatchDistanceMetricV2(int patch_size, cv::Mat ijmap, double weight)
|
|
151
122
|
: PatchDistanceMetric(patch_size), m_ijmap(ijmap), m_weight(weight)
|
|
152
123
|
{
|
|
124
|
+
// The distance is divided by 1 + weight, so a negative weight flips its sign.
|
|
125
|
+
if (!(weight >= 0))
|
|
126
|
+
throw std::invalid_argument("the guide weight must be >= 0");
|
|
153
127
|
}
|
|
154
128
|
virtual int operator()(const MaskedImage &source, int source_y, int source_x, const MaskedImage &target, int target_y, int target_x) const;
|
|
155
129
|
|
|
156
130
|
protected:
|
|
157
131
|
cv::Mat m_ijmap;
|
|
158
|
-
double
|
|
132
|
+
double m_weight;
|
|
159
133
|
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#include <atomic>
|
|
2
|
+
#include <cstring>
|
|
3
|
+
#include <exception>
|
|
4
|
+
#include <memory>
|
|
5
|
+
#include <string>
|
|
6
|
+
|
|
7
|
+
#include "pyinterface.h"
|
|
8
|
+
#include "inpaint.h"
|
|
9
|
+
|
|
10
|
+
int _dtype_py_to_cv(int dtype_py);
|
|
11
|
+
int _dtype_cv_to_py(int dtype_cv);
|
|
12
|
+
cv::Mat _py_to_cv2(PM_mat_t pymat);
|
|
13
|
+
PM_mat_t _cv2_to_py(cv::Mat cvmat);
|
|
14
|
+
|
|
15
|
+
namespace
|
|
16
|
+
{
|
|
17
|
+
// The settings are set by one thread while others may be inpainting, since ctypes
|
|
18
|
+
// releases the GIL.
|
|
19
|
+
std::atomic<unsigned int> &random_seed()
|
|
20
|
+
{
|
|
21
|
+
static std::atomic value{1212u};
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
std::atomic<bool> &verbose()
|
|
26
|
+
{
|
|
27
|
+
static std::atomic value{false};
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Message of the last failed call on the calling thread, see PM_last_error.
|
|
32
|
+
std::string &last_error()
|
|
33
|
+
{
|
|
34
|
+
thread_local std::string message;
|
|
35
|
+
return message;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
void set_last_error(const char *message) noexcept
|
|
39
|
+
{
|
|
40
|
+
try
|
|
41
|
+
{
|
|
42
|
+
last_error() = message;
|
|
43
|
+
}
|
|
44
|
+
catch (...)
|
|
45
|
+
{
|
|
46
|
+
last_error().clear();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A C++ exception must not propagate through the C interface into ctypes, which
|
|
51
|
+
// would abort the process. Report it with an empty result instead.
|
|
52
|
+
template <typename Func>
|
|
53
|
+
PM_mat_t guarded(Func &&func) noexcept
|
|
54
|
+
{
|
|
55
|
+
try
|
|
56
|
+
{
|
|
57
|
+
return func();
|
|
58
|
+
}
|
|
59
|
+
catch (const std::exception &e)
|
|
60
|
+
{
|
|
61
|
+
set_last_error(e.what());
|
|
62
|
+
}
|
|
63
|
+
catch (...)
|
|
64
|
+
{
|
|
65
|
+
set_last_error("unknown error");
|
|
66
|
+
}
|
|
67
|
+
return PM_mat_t{nullptr, {0, 0, 0}, 0};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const char *PM_last_error()
|
|
72
|
+
{
|
|
73
|
+
return last_error().c_str();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
void PM_set_random_seed(unsigned int seed)
|
|
77
|
+
{
|
|
78
|
+
random_seed().store(seed);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
void PM_set_verbose(int value)
|
|
82
|
+
{
|
|
83
|
+
verbose().store(value != 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
void PM_free_pymat(PM_mat_t pymat)
|
|
87
|
+
{
|
|
88
|
+
// Takes back the ownership of the buffer that _cv2_to_py released. The buffer
|
|
89
|
+
// crosses the C interface as a plain pointer, so it cannot be a std::vector
|
|
90
|
+
// (SonarCloud cpp:S5945).
|
|
91
|
+
std::unique_ptr<unsigned char[]> data(static_cast<unsigned char *>(pymat.data_ptr)); // NOSONAR
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
PM_mat_t PM_inpaint(PM_mat_t source_py, PM_mat_t mask_py, int patch_size)
|
|
95
|
+
{
|
|
96
|
+
return guarded([source_py, mask_py, patch_size] {
|
|
97
|
+
cv::Mat source = _py_to_cv2(source_py);
|
|
98
|
+
cv::Mat mask = _py_to_cv2(mask_py);
|
|
99
|
+
auto metric = PatchSSDDistanceMetric(patch_size);
|
|
100
|
+
cv::Mat result = Inpainting(source, mask, &metric).run(verbose().load(), false, random_seed().load());
|
|
101
|
+
return _cv2_to_py(result);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
PM_mat_t PM_inpaint_regularity(PM_mat_t source_py, PM_mat_t mask_py, PM_mat_t ijmap_py, int patch_size, float guide_weight)
|
|
106
|
+
{
|
|
107
|
+
return guarded([source_py, mask_py, ijmap_py, patch_size, guide_weight] {
|
|
108
|
+
cv::Mat source = _py_to_cv2(source_py);
|
|
109
|
+
cv::Mat mask = _py_to_cv2(mask_py);
|
|
110
|
+
cv::Mat ijmap = _py_to_cv2(ijmap_py);
|
|
111
|
+
|
|
112
|
+
auto metric = RegularityGuidedPatchDistanceMetricV2(patch_size, ijmap, guide_weight);
|
|
113
|
+
cv::Mat result = Inpainting(source, mask, &metric).run(verbose().load(), false, random_seed().load());
|
|
114
|
+
return _cv2_to_py(result);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
PM_mat_t PM_inpaint2(PM_mat_t source_py, PM_mat_t mask_py, PM_mat_t global_mask_py, int patch_size)
|
|
119
|
+
{
|
|
120
|
+
return guarded([source_py, mask_py, global_mask_py, patch_size] {
|
|
121
|
+
cv::Mat source = _py_to_cv2(source_py);
|
|
122
|
+
cv::Mat mask = _py_to_cv2(mask_py);
|
|
123
|
+
cv::Mat global_mask = _py_to_cv2(global_mask_py);
|
|
124
|
+
|
|
125
|
+
auto metric = PatchSSDDistanceMetric(patch_size);
|
|
126
|
+
cv::Mat result = Inpainting(source, mask, global_mask, &metric).run(verbose().load(), false, random_seed().load());
|
|
127
|
+
return _cv2_to_py(result);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
PM_mat_t PM_inpaint2_regularity(PM_mat_t source_py, PM_mat_t mask_py, PM_mat_t global_mask_py, PM_mat_t ijmap_py, int patch_size, float guide_weight)
|
|
132
|
+
{
|
|
133
|
+
return guarded([source_py, mask_py, global_mask_py, ijmap_py, patch_size, guide_weight] {
|
|
134
|
+
cv::Mat source = _py_to_cv2(source_py);
|
|
135
|
+
cv::Mat mask = _py_to_cv2(mask_py);
|
|
136
|
+
cv::Mat global_mask = _py_to_cv2(global_mask_py);
|
|
137
|
+
cv::Mat ijmap = _py_to_cv2(ijmap_py);
|
|
138
|
+
|
|
139
|
+
auto metric = RegularityGuidedPatchDistanceMetricV2(patch_size, ijmap, guide_weight);
|
|
140
|
+
cv::Mat result = Inpainting(source, mask, global_mask, &metric).run(verbose().load(), false, random_seed().load());
|
|
141
|
+
return _cv2_to_py(result);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
int _dtype_py_to_cv(int dtype_py)
|
|
146
|
+
{
|
|
147
|
+
switch (dtype_py)
|
|
148
|
+
{
|
|
149
|
+
case PM_UINT8:
|
|
150
|
+
return CV_8U;
|
|
151
|
+
case PM_INT8:
|
|
152
|
+
return CV_8S;
|
|
153
|
+
case PM_UINT16:
|
|
154
|
+
return CV_16U;
|
|
155
|
+
case PM_INT16:
|
|
156
|
+
return CV_16S;
|
|
157
|
+
case PM_INT32:
|
|
158
|
+
return CV_32S;
|
|
159
|
+
case PM_FLOAT32:
|
|
160
|
+
return CV_32F;
|
|
161
|
+
case PM_FLOAT64:
|
|
162
|
+
return CV_64F;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return CV_8U;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
int _dtype_cv_to_py(int dtype_cv)
|
|
169
|
+
{
|
|
170
|
+
switch (dtype_cv)
|
|
171
|
+
{
|
|
172
|
+
case CV_8U:
|
|
173
|
+
return PM_UINT8;
|
|
174
|
+
case CV_8S:
|
|
175
|
+
return PM_INT8;
|
|
176
|
+
case CV_16U:
|
|
177
|
+
return PM_UINT16;
|
|
178
|
+
case CV_16S:
|
|
179
|
+
return PM_INT16;
|
|
180
|
+
case CV_32S:
|
|
181
|
+
return PM_INT32;
|
|
182
|
+
case CV_32F:
|
|
183
|
+
return PM_FLOAT32;
|
|
184
|
+
case CV_64F:
|
|
185
|
+
return PM_FLOAT64;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return PM_UINT8;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
cv::Mat _py_to_cv2(PM_mat_t pymat)
|
|
192
|
+
{
|
|
193
|
+
int dtype = CV_MAKETYPE(_dtype_py_to_cv(pymat.dtype), pymat.shape.channels);
|
|
194
|
+
return cv::Mat(cv::Size(pymat.shape.width, pymat.shape.height), dtype, pymat.data_ptr).clone();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
PM_mat_t _cv2_to_py(cv::Mat cvmat)
|
|
198
|
+
{
|
|
199
|
+
if (!cvmat.isContinuous())
|
|
200
|
+
cvmat = cvmat.clone();
|
|
201
|
+
|
|
202
|
+
PM_shape_t shape = {cvmat.size().width, cvmat.size().height, cvmat.channels()};
|
|
203
|
+
int dtype = _dtype_cv_to_py(cvmat.depth());
|
|
204
|
+
size_t dsize = cvmat.total() * cvmat.elemSize();
|
|
205
|
+
|
|
206
|
+
// A null pointer reports an error to Python, so allocate at least one byte. A failed
|
|
207
|
+
// allocation throws std::bad_alloc, which guarded() reports.
|
|
208
|
+
auto data = std::make_unique<unsigned char[]>(dsize > 0 ? dsize : 1);
|
|
209
|
+
if (dsize > 0)
|
|
210
|
+
std::memcpy(data.get(), cvmat.data, dsize);
|
|
211
|
+
|
|
212
|
+
// Python passes the buffer back to PM_free_pymat, which frees it.
|
|
213
|
+
return PM_mat_t{data.release(), shape, dtype};
|
|
214
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
1
3
|
#include <opencv2/core.hpp>
|
|
2
4
|
#include <cstdlib>
|
|
3
5
|
#include <cstdio>
|
|
@@ -29,6 +31,10 @@ extern "C"
|
|
|
29
31
|
int dtype;
|
|
30
32
|
};
|
|
31
33
|
|
|
34
|
+
// The PM_inpaint* functions return a null data_ptr on failure; this returns the
|
|
35
|
+
// error message of the last failure on the calling thread.
|
|
36
|
+
const char *PM_last_error(void);
|
|
37
|
+
|
|
32
38
|
void PM_set_random_seed(unsigned int seed);
|
|
33
39
|
void PM_set_verbose(int value);
|
|
34
40
|
|
|
@@ -17,6 +17,8 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
import ctypes
|
|
19
19
|
import logging
|
|
20
|
+
import math
|
|
21
|
+
import operator
|
|
20
22
|
from collections.abc import Callable
|
|
21
23
|
from typing import TypeAlias
|
|
22
24
|
|
|
@@ -47,6 +49,9 @@ except OSError as e:
|
|
|
47
49
|
# True if the native library was loaded successfully.
|
|
48
50
|
patchmatch_available = _lib is not None
|
|
49
51
|
|
|
52
|
+
# The native code computes 2 * patch_size + 1 as a C int.
|
|
53
|
+
_MAX_PATCH_SIZE = (2**31 - 2) // 2
|
|
54
|
+
|
|
50
55
|
|
|
51
56
|
def _get_lib() -> ctypes.CDLL:
|
|
52
57
|
if _lib is None:
|
|
@@ -78,13 +83,17 @@ def inpaint(
|
|
|
78
83
|
white pixels (255, 255, 255) are treated as holes.
|
|
79
84
|
global_mask: mask like ``mask`` of pixels that are neither filled nor used as
|
|
80
85
|
a source; they keep their values from ``image``.
|
|
81
|
-
patch_size:
|
|
86
|
+
patch_size: radius of the compared patches, which span
|
|
87
|
+
``(2 * patch_size + 1) ** 2`` pixels. Larger patches follow larger
|
|
88
|
+
structures but are much slower; the examples use 3.
|
|
82
89
|
|
|
83
90
|
Returns:
|
|
84
|
-
The repaired image, with the same shape as ``image``.
|
|
91
|
+
The repaired image, with the same shape as ``image``. Only the holes are
|
|
92
|
+
filled; all other pixels keep their values from ``image``.
|
|
85
93
|
"""
|
|
86
94
|
lib = _get_lib()
|
|
87
|
-
|
|
95
|
+
patch_size = _check_patch_size(patch_size)
|
|
96
|
+
image, mask, global_mask = _prepare_inputs(image, mask, global_mask)
|
|
88
97
|
|
|
89
98
|
if global_mask is None:
|
|
90
99
|
return _call(lib.PM_inpaint, image, mask, ctypes.c_int(patch_size))
|
|
@@ -103,12 +112,15 @@ def inpaint_regularity(
|
|
|
103
112
|
"""Like :func:`inpaint`, additionally guided by a regularity map.
|
|
104
113
|
|
|
105
114
|
Args:
|
|
106
|
-
ijmap: HxWx3 float32 array with the regularity coordinates
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
ijmap: HxWx3 float32 array of finite values with the regularity coordinates
|
|
116
|
+
of each pixel in the first two channels; the third channel is unused.
|
|
117
|
+
A map of a different size is scaled to the image.
|
|
118
|
+
guide_weight: non-negative weight of the regularity term relative to the
|
|
119
|
+
patch distance.
|
|
109
120
|
"""
|
|
110
121
|
lib = _get_lib()
|
|
111
|
-
|
|
122
|
+
patch_size = _check_patch_size(patch_size)
|
|
123
|
+
image, mask, global_mask = _prepare_inputs(image, mask, global_mask)
|
|
112
124
|
|
|
113
125
|
if not (
|
|
114
126
|
isinstance(ijmap, np.ndarray)
|
|
@@ -118,25 +130,46 @@ def inpaint_regularity(
|
|
|
118
130
|
and ijmap.size > 0
|
|
119
131
|
):
|
|
120
132
|
raise ValueError("ijmap must be a non-empty HxWx3 float32 array")
|
|
133
|
+
# NaN and infinity turn into out-of-range patch distances in the native code
|
|
134
|
+
if not np.isfinite(ijmap).all():
|
|
135
|
+
raise ValueError("ijmap must only contain finite values")
|
|
121
136
|
ijmap = np.ascontiguousarray(ijmap)
|
|
122
137
|
|
|
138
|
+
# the native code divides by 1 + guide_weight and indexes a table with the result
|
|
139
|
+
guide_weight = float(guide_weight)
|
|
140
|
+
if not (math.isfinite(guide_weight) and guide_weight >= 0):
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"guide_weight must be a finite number >= 0, got {guide_weight}"
|
|
143
|
+
)
|
|
144
|
+
|
|
123
145
|
args = (ijmap, ctypes.c_int(patch_size), ctypes.c_float(guide_weight))
|
|
124
146
|
if global_mask is None:
|
|
125
147
|
return _call(lib.PM_inpaint_regularity, image, mask, *args)
|
|
126
148
|
return _call(lib.PM_inpaint2_regularity, image, mask, global_mask, *args)
|
|
127
149
|
|
|
128
150
|
|
|
151
|
+
def _check_patch_size(patch_size: int) -> int:
|
|
152
|
+
try:
|
|
153
|
+
patch_size = operator.index(patch_size)
|
|
154
|
+
except TypeError:
|
|
155
|
+
raise TypeError(
|
|
156
|
+
f"patch_size must be an integer, got {type(patch_size).__name__}"
|
|
157
|
+
) from None
|
|
158
|
+
# The native code crashes for 0 and never terminates for negative sizes. ctypes
|
|
159
|
+
# silently wraps larger values into the C int range, e.g. 2**31 to a negative size.
|
|
160
|
+
if not 1 <= patch_size <= _MAX_PATCH_SIZE:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"patch_size must be between 1 and {_MAX_PATCH_SIZE}, got {patch_size}"
|
|
163
|
+
)
|
|
164
|
+
return patch_size
|
|
165
|
+
|
|
166
|
+
|
|
129
167
|
def _prepare_inputs(
|
|
130
168
|
image: ImageLike,
|
|
131
169
|
mask: ImageLike | None,
|
|
132
170
|
global_mask: ImageLike | None,
|
|
133
|
-
patch_size: int,
|
|
134
171
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray | None]:
|
|
135
172
|
"""Validate the inputs and convert them to contiguous arrays."""
|
|
136
|
-
# the native code crashes for 0 and never terminates for negative sizes
|
|
137
|
-
if patch_size < 1:
|
|
138
|
-
raise ValueError(f"patch_size must be at least 1, got {patch_size}")
|
|
139
|
-
|
|
140
173
|
image = _canonize_image_array(image)
|
|
141
174
|
mask = _default_mask(image) if mask is None else _canonize_mask_array(mask)
|
|
142
175
|
if global_mask is not None:
|
|
@@ -154,12 +187,17 @@ def _prepare_inputs(
|
|
|
154
187
|
|
|
155
188
|
def _call(func: Callable[..., CMatT], *args: object) -> np.ndarray:
|
|
156
189
|
"""Call ``func`` with arrays converted to pymats and copy the result."""
|
|
190
|
+
lib = _get_lib()
|
|
157
191
|
c_args = [np_to_pymat(a) if isinstance(a, np.ndarray) else a for a in args]
|
|
158
192
|
ret = func(*c_args)
|
|
193
|
+
if not ret.data_ptr:
|
|
194
|
+
# the error message is kept per thread, like the call itself
|
|
195
|
+
message = lib.PM_last_error().decode(errors="replace").strip()
|
|
196
|
+
raise RuntimeError(f"patchmatch failed: {message}")
|
|
159
197
|
try:
|
|
160
198
|
return pymat_to_np(ret)
|
|
161
199
|
finally:
|
|
162
|
-
|
|
200
|
+
lib.PM_free_pymat(ret)
|
|
163
201
|
|
|
164
202
|
|
|
165
203
|
def _canonize_image_array(image: ImageLike) -> np.ndarray:
|
|
@@ -59,6 +59,15 @@ metadata.version.provider = "scikit_build_core.metadata.setuptools_scm"
|
|
|
59
59
|
sdist.include = ["examples/images/forest_pruned.bmp", "patchmatch/_version.py"]
|
|
60
60
|
sdist.exclude = [".*", "examples", "uv.lock"]
|
|
61
61
|
|
|
62
|
+
# Without a matching wheel, pip builds the sdist, which needs a C++ compiler, CMake and
|
|
63
|
+
# OpenCV. If that fails, build a wheel without the native library instead of failing
|
|
64
|
+
# the installation; patchmatch_available is then False, as with 1.x. Builds from the
|
|
65
|
+
# source tree still fail, and cibuildwheel rejects the pure wheel of a failed build.
|
|
66
|
+
[[tool.scikit-build.overrides]]
|
|
67
|
+
if.from-sdist = true
|
|
68
|
+
if.failed = true
|
|
69
|
+
wheel.cmake = false
|
|
70
|
+
|
|
62
71
|
[tool.setuptools_scm]
|
|
63
72
|
version_file = "patchmatch/_version.py"
|
|
64
73
|
# used when building without git metadata, e.g. from a GitHub source archive
|
|
@@ -6,6 +6,7 @@ wheels small and free of OpenCV's GUI/codec dependencies.
|
|
|
6
6
|
Usage: python build_opencv.py (installs into $OpenCV_ROOT)
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import hashlib
|
|
9
10
|
import os
|
|
10
11
|
import shutil
|
|
11
12
|
import subprocess
|
|
@@ -19,6 +20,9 @@ OPENCV_VERSION = "4.14.0"
|
|
|
19
20
|
OPENCV_URL = (
|
|
20
21
|
f"https://github.com/opencv/opencv/archive/refs/tags/{OPENCV_VERSION}.tar.gz"
|
|
21
22
|
)
|
|
23
|
+
# The wheels link this code statically, so the archive is pinned by its SHA-256.
|
|
24
|
+
# Update it together with the version, e.g. with `sha256sum` or `shasum -a 256`.
|
|
25
|
+
OPENCV_SHA256 = "ee8fb9b30eb60850431b4656447080e3737b56e45719c92b67f245950609f86e"
|
|
22
26
|
|
|
23
27
|
CMAKE_OPTIONS = {
|
|
24
28
|
"CMAKE_BUILD_TYPE": "Release",
|
|
@@ -87,6 +91,18 @@ def find_cmake() -> str:
|
|
|
87
91
|
return str(scripts / ("cmake.exe" if os.name == "nt" else "cmake"))
|
|
88
92
|
|
|
89
93
|
|
|
94
|
+
def verify_archive(archive: Path) -> None:
|
|
95
|
+
sha256 = hashlib.sha256()
|
|
96
|
+
with archive.open("rb") as f:
|
|
97
|
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
98
|
+
sha256.update(chunk)
|
|
99
|
+
if sha256.hexdigest() != OPENCV_SHA256:
|
|
100
|
+
sys.exit(
|
|
101
|
+
f"SHA-256 mismatch for {OPENCV_URL}: "
|
|
102
|
+
f"expected {OPENCV_SHA256}, got {sha256.hexdigest()}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
90
106
|
def main() -> None:
|
|
91
107
|
# OpenCV_ROOT is the variable CMake's find_package(OpenCV) looks for.
|
|
92
108
|
root = os.environ.get("OpenCV_ROOT") # noqa: SIM112
|
|
@@ -102,6 +118,7 @@ def main() -> None:
|
|
|
102
118
|
archive = Path(tmp) / "opencv.tar.gz"
|
|
103
119
|
print(f"Downloading {OPENCV_URL}", flush=True)
|
|
104
120
|
urllib.request.urlretrieve(OPENCV_URL, archive)
|
|
121
|
+
verify_archive(archive)
|
|
105
122
|
with tarfile.open(archive) as tar:
|
|
106
123
|
# extraction filters are missing on older patch releases, e.g. the
|
|
107
124
|
# last Windows installer of Python 3.10 (3.10.11)
|
|
@@ -41,9 +41,8 @@ def assert_plausible_fill(
|
|
|
41
41
|
|
|
42
42
|
assert not white_pixels(result)[holes].any(), "holes were not filled"
|
|
43
43
|
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
assert outside < 1, outside
|
|
44
|
+
# only the holes are filled
|
|
45
|
+
np.testing.assert_array_equal(result[~holes], source[~holes])
|
|
47
46
|
|
|
48
47
|
# the filling blends in with its surroundings
|
|
49
48
|
band = surrounding(holes) & ~excluded
|
|
@@ -45,9 +45,13 @@ def seed():
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def assert_filled(result: np.ndarray, source: np.ndarray) -> None:
|
|
48
|
+
"""The hole is filled and all other pixels keep their values."""
|
|
48
49
|
assert result.shape == source.shape
|
|
49
50
|
assert result.dtype == np.uint8
|
|
50
51
|
assert not (result[HOLE] == 255).all(axis=-1).any()
|
|
52
|
+
known = np.ones(source.shape[:2], dtype=bool)
|
|
53
|
+
known[HOLE] = False
|
|
54
|
+
np.testing.assert_array_equal(result[known], source[known])
|
|
51
55
|
|
|
52
56
|
|
|
53
57
|
# --- package ----------------------------------------------------------------
|
|
@@ -79,6 +83,8 @@ def test_inpaint_explicit_mask(image, hole_mask):
|
|
|
79
83
|
result = patchmatch.inpaint(image, hole_mask, patch_size=3)
|
|
80
84
|
assert result.shape == image.shape
|
|
81
85
|
assert (result[HOLE] != 0).any()
|
|
86
|
+
known = hole_mask == 0
|
|
87
|
+
np.testing.assert_array_equal(result[known], image[known])
|
|
82
88
|
|
|
83
89
|
|
|
84
90
|
def test_inpaint_global_mask(image, hole_mask):
|
|
@@ -90,6 +96,14 @@ def test_inpaint_global_mask(image, hole_mask):
|
|
|
90
96
|
np.testing.assert_array_equal(result[:8], image[:8])
|
|
91
97
|
|
|
92
98
|
|
|
99
|
+
@pytest.mark.parametrize("shape", [(0, 0, 3), (0, 10, 3), (10, 0, 3)])
|
|
100
|
+
def test_inpaint_empty_image(shape):
|
|
101
|
+
empty = np.zeros(shape, dtype=np.uint8)
|
|
102
|
+
global_mask = np.zeros(shape[:2], dtype=np.uint8)
|
|
103
|
+
assert patchmatch.inpaint(empty, patch_size=3).shape == shape
|
|
104
|
+
assert patchmatch.inpaint(empty, global_mask=global_mask).shape == shape
|
|
105
|
+
|
|
106
|
+
|
|
93
107
|
@pytest.mark.parametrize("patch_size", [1, 3, 7])
|
|
94
108
|
def test_inpaint_patch_sizes(image, patch_size):
|
|
95
109
|
assert_filled(patchmatch.inpaint(image, patch_size=patch_size), image)
|
|
@@ -243,7 +257,8 @@ def test_mask_size_must_match_image(image, ijmap, func, argument):
|
|
|
243
257
|
func(image, **kwargs)
|
|
244
258
|
|
|
245
259
|
|
|
246
|
-
|
|
260
|
+
# 2**31 wrapped to a negative C int, which never terminated
|
|
261
|
+
@pytest.mark.parametrize("patch_size", [0, -1, 2**30, 2**31])
|
|
247
262
|
@pytest.mark.parametrize(
|
|
248
263
|
"func",
|
|
249
264
|
[patchmatch.inpaint, patchmatch.inpaint_regularity],
|
|
@@ -255,6 +270,18 @@ def test_invalid_patch_size(image, ijmap, func, patch_size):
|
|
|
255
270
|
func(image, None, patch_size=patch_size, **kwargs)
|
|
256
271
|
|
|
257
272
|
|
|
273
|
+
def test_patch_size_must_be_an_integer(image):
|
|
274
|
+
with pytest.raises(TypeError, match="patch_size must be an integer"):
|
|
275
|
+
patchmatch.inpaint(image, patch_size=3.0)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_patch_size_accepts_numpy_integers(image):
|
|
279
|
+
expected = patchmatch.inpaint(image, patch_size=3)
|
|
280
|
+
np.testing.assert_array_equal(
|
|
281
|
+
patchmatch.inpaint(image, patch_size=np.int64(3)), expected
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
258
285
|
@pytest.mark.parametrize(
|
|
259
286
|
"bad_ijmap",
|
|
260
287
|
[
|
|
@@ -262,14 +289,46 @@ def test_invalid_patch_size(image, ijmap, func, patch_size):
|
|
|
262
289
|
np.zeros((HEIGHT, WIDTH, 2), dtype=np.float32),
|
|
263
290
|
np.zeros((0, WIDTH, 3), dtype=np.float32),
|
|
264
291
|
[[[0.0, 0.0, 0.0]]],
|
|
292
|
+
np.full((HEIGHT, WIDTH, 3), np.nan, dtype=np.float32),
|
|
293
|
+
np.full((HEIGHT, WIDTH, 3), np.inf, dtype=np.float32),
|
|
265
294
|
],
|
|
266
|
-
ids=["float64", "2-channel", "empty", "list"],
|
|
295
|
+
ids=["float64", "2-channel", "empty", "list", "nan", "inf"],
|
|
267
296
|
)
|
|
268
297
|
def test_inpaint_regularity_invalid_ijmap(image, bad_ijmap):
|
|
269
298
|
with pytest.raises(ValueError, match="ijmap"):
|
|
270
299
|
patchmatch.inpaint_regularity(image, None, bad_ijmap)
|
|
271
300
|
|
|
272
301
|
|
|
302
|
+
@pytest.mark.parametrize("guide_weight", [-1.0, -0.5, float("nan"), float("inf")])
|
|
303
|
+
def test_inpaint_regularity_invalid_guide_weight(image, ijmap, guide_weight):
|
|
304
|
+
# a weight of -1 used to crash the native code, other negative weights made it
|
|
305
|
+
# read outside of the similarity table
|
|
306
|
+
with pytest.raises(ValueError, match="guide_weight"):
|
|
307
|
+
patchmatch.inpaint_regularity(image, None, ijmap, guide_weight=guide_weight)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# --- native errors ----------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_native_exception_raises_runtime_error():
|
|
314
|
+
# A null data pointer with a non-empty shape fails an OpenCV assertion. The
|
|
315
|
+
# exception must not propagate through the C interface and abort the process.
|
|
316
|
+
lib = patch_match._get_lib()
|
|
317
|
+
bad = _lib.CMatT(None, _lib.CShapeT(4, 4, 3), 0)
|
|
318
|
+
args = (bad, bad, ctypes.c_int(3))
|
|
319
|
+
with pytest.raises(RuntimeError, match=r"patchmatch failed: .*Assertion failed"):
|
|
320
|
+
patch_match._call(lib.PM_inpaint, *args)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_native_rejects_negative_guide_weight(image, hole_mask, ijmap):
|
|
324
|
+
"""The C++ metric checks the weight as well, for callers that bypass Python."""
|
|
325
|
+
lib = patch_match._get_lib()
|
|
326
|
+
mask = hole_mask[..., np.newaxis]
|
|
327
|
+
args = (image, mask, ijmap, ctypes.c_int(3), ctypes.c_float(-1))
|
|
328
|
+
with pytest.raises(RuntimeError, match="guide weight must be >= 0"):
|
|
329
|
+
patch_match._call(lib.PM_inpaint_regularity, *args)
|
|
330
|
+
|
|
331
|
+
|
|
273
332
|
# --- missing native library -------------------------------------------------
|
|
274
333
|
|
|
275
334
|
|
|
@@ -297,6 +356,27 @@ def test_find_library_reports_missing_file(monkeypatch):
|
|
|
297
356
|
_lib.find_library()
|
|
298
357
|
|
|
299
358
|
|
|
359
|
+
def test_import_without_native_library():
|
|
360
|
+
"""A failed build from the sdist installs the package without the library.
|
|
361
|
+
|
|
362
|
+
The import has to work and explain how to fix it, see the overrides in
|
|
363
|
+
pyproject.toml. The module is reloaded in a separate process, so the other
|
|
364
|
+
tests keep the loaded library.
|
|
365
|
+
"""
|
|
366
|
+
code = (
|
|
367
|
+
"import importlib\n"
|
|
368
|
+
"from patchmatch import _lib, patch_match\n"
|
|
369
|
+
"_lib.LIBRARY_NAME = 'missing-library'\n"
|
|
370
|
+
"importlib.reload(patch_match)\n"
|
|
371
|
+
"assert not patch_match.patchmatch_available\n"
|
|
372
|
+
)
|
|
373
|
+
process = subprocess.run(
|
|
374
|
+
[sys.executable, "-c", code], capture_output=True, text=True, check=True
|
|
375
|
+
)
|
|
376
|
+
assert "missing-library not found" in process.stderr
|
|
377
|
+
assert "--no-cache-dir" in process.stderr
|
|
378
|
+
|
|
379
|
+
|
|
300
380
|
# --- ctypes conversion ------------------------------------------------------
|
|
301
381
|
|
|
302
382
|
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
#include "pyinterface.h"
|
|
2
|
-
#include "inpaint.h"
|
|
3
|
-
|
|
4
|
-
static unsigned int PM_seed = 1212;
|
|
5
|
-
static bool PM_verbose = false;
|
|
6
|
-
|
|
7
|
-
int _dtype_py_to_cv(int dtype_py);
|
|
8
|
-
int _dtype_cv_to_py(int dtype_cv);
|
|
9
|
-
cv::Mat _py_to_cv2(PM_mat_t pymat);
|
|
10
|
-
PM_mat_t _cv2_to_py(cv::Mat cvmat);
|
|
11
|
-
|
|
12
|
-
void PM_set_random_seed(unsigned int seed)
|
|
13
|
-
{
|
|
14
|
-
PM_seed = seed;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
void PM_set_verbose(int value)
|
|
18
|
-
{
|
|
19
|
-
PM_verbose = static_cast<bool>(value);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
void PM_free_pymat(PM_mat_t pymat)
|
|
23
|
-
{
|
|
24
|
-
free(pymat.data_ptr);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
PM_mat_t PM_inpaint(PM_mat_t source_py, PM_mat_t mask_py, int patch_size)
|
|
28
|
-
{
|
|
29
|
-
cv::Mat source = _py_to_cv2(source_py);
|
|
30
|
-
cv::Mat mask = _py_to_cv2(mask_py);
|
|
31
|
-
auto metric = PatchSSDDistanceMetric(patch_size);
|
|
32
|
-
cv::Mat result = Inpainting(source, mask, &metric).run(PM_verbose, false, PM_seed);
|
|
33
|
-
return _cv2_to_py(result);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
PM_mat_t PM_inpaint_regularity(PM_mat_t source_py, PM_mat_t mask_py, PM_mat_t ijmap_py, int patch_size, float guide_weight)
|
|
37
|
-
{
|
|
38
|
-
cv::Mat source = _py_to_cv2(source_py);
|
|
39
|
-
cv::Mat mask = _py_to_cv2(mask_py);
|
|
40
|
-
cv::Mat ijmap = _py_to_cv2(ijmap_py);
|
|
41
|
-
|
|
42
|
-
auto metric = RegularityGuidedPatchDistanceMetricV2(patch_size, ijmap, guide_weight);
|
|
43
|
-
cv::Mat result = Inpainting(source, mask, &metric).run(PM_verbose, false, PM_seed);
|
|
44
|
-
return _cv2_to_py(result);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
PM_mat_t PM_inpaint2(PM_mat_t source_py, PM_mat_t mask_py, PM_mat_t global_mask_py, int patch_size)
|
|
48
|
-
{
|
|
49
|
-
cv::Mat source = _py_to_cv2(source_py);
|
|
50
|
-
cv::Mat mask = _py_to_cv2(mask_py);
|
|
51
|
-
cv::Mat global_mask = _py_to_cv2(global_mask_py);
|
|
52
|
-
|
|
53
|
-
auto metric = PatchSSDDistanceMetric(patch_size);
|
|
54
|
-
cv::Mat result = Inpainting(source, mask, global_mask, &metric).run(PM_verbose, false, PM_seed);
|
|
55
|
-
return _cv2_to_py(result);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
PM_mat_t PM_inpaint2_regularity(PM_mat_t source_py, PM_mat_t mask_py, PM_mat_t global_mask_py, PM_mat_t ijmap_py, int patch_size, float guide_weight)
|
|
59
|
-
{
|
|
60
|
-
cv::Mat source = _py_to_cv2(source_py);
|
|
61
|
-
cv::Mat mask = _py_to_cv2(mask_py);
|
|
62
|
-
cv::Mat global_mask = _py_to_cv2(global_mask_py);
|
|
63
|
-
cv::Mat ijmap = _py_to_cv2(ijmap_py);
|
|
64
|
-
|
|
65
|
-
auto metric = RegularityGuidedPatchDistanceMetricV2(patch_size, ijmap, guide_weight);
|
|
66
|
-
cv::Mat result = Inpainting(source, mask, global_mask, &metric).run(PM_verbose, false, PM_seed);
|
|
67
|
-
return _cv2_to_py(result);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
int _dtype_py_to_cv(int dtype_py)
|
|
71
|
-
{
|
|
72
|
-
switch (dtype_py)
|
|
73
|
-
{
|
|
74
|
-
case PM_UINT8:
|
|
75
|
-
return CV_8U;
|
|
76
|
-
case PM_INT8:
|
|
77
|
-
return CV_8S;
|
|
78
|
-
case PM_UINT16:
|
|
79
|
-
return CV_16U;
|
|
80
|
-
case PM_INT16:
|
|
81
|
-
return CV_16S;
|
|
82
|
-
case PM_INT32:
|
|
83
|
-
return CV_32S;
|
|
84
|
-
case PM_FLOAT32:
|
|
85
|
-
return CV_32F;
|
|
86
|
-
case PM_FLOAT64:
|
|
87
|
-
return CV_64F;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return CV_8U;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
int _dtype_cv_to_py(int dtype_cv)
|
|
94
|
-
{
|
|
95
|
-
switch (dtype_cv)
|
|
96
|
-
{
|
|
97
|
-
case CV_8U:
|
|
98
|
-
return PM_UINT8;
|
|
99
|
-
case CV_8S:
|
|
100
|
-
return PM_INT8;
|
|
101
|
-
case CV_16U:
|
|
102
|
-
return PM_UINT16;
|
|
103
|
-
case CV_16S:
|
|
104
|
-
return PM_INT16;
|
|
105
|
-
case CV_32S:
|
|
106
|
-
return PM_INT32;
|
|
107
|
-
case CV_32F:
|
|
108
|
-
return PM_FLOAT32;
|
|
109
|
-
case CV_64F:
|
|
110
|
-
return PM_FLOAT64;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return PM_UINT8;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
cv::Mat _py_to_cv2(PM_mat_t pymat)
|
|
117
|
-
{
|
|
118
|
-
int dtype = CV_MAKETYPE(_dtype_py_to_cv(pymat.dtype), pymat.shape.channels);
|
|
119
|
-
return cv::Mat(cv::Size(pymat.shape.width, pymat.shape.height), dtype, pymat.data_ptr).clone();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
PM_mat_t _cv2_to_py(cv::Mat cvmat)
|
|
123
|
-
{
|
|
124
|
-
PM_shape_t shape = {cvmat.size().width, cvmat.size().height, cvmat.channels()};
|
|
125
|
-
int dtype = _dtype_cv_to_py(cvmat.depth());
|
|
126
|
-
size_t dsize = cvmat.total() * cvmat.elemSize();
|
|
127
|
-
|
|
128
|
-
void *data_ptr = reinterpret_cast<void *>(malloc(dsize));
|
|
129
|
-
memcpy(data_ptr, reinterpret_cast<void *>(cvmat.data), dsize);
|
|
130
|
-
|
|
131
|
-
return PM_mat_t{data_ptr, shape, dtype};
|
|
132
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|