PyPatchMatch 2.0.2__tar.gz → 2.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/CLAUDE.md +14 -29
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/CMakeLists.txt +2 -1
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/PKG-INFO +4 -3
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/README.md +2 -1
- pypatchmatch-2.1.1/examples/images/chelsea.png +0 -0
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/patchmatch/_version.py +3 -3
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/patchmatch/csrc/inpaint.cpp +215 -33
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/patchmatch/csrc/inpaint.h +4 -3
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/patchmatch/csrc/masked_image.cpp +31 -27
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/patchmatch/csrc/masked_image.h +28 -24
- pypatchmatch-2.1.1/patchmatch/csrc/nnf.cpp +455 -0
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/patchmatch/csrc/nnf.h +74 -30
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/patchmatch/csrc/pyinterface.cpp +18 -27
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/patchmatch/patch_match.py +2 -2
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/pyproject.toml +6 -2
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/scripts/build_opencv.py +3 -6
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/scripts/evaluate_inpainting.py +48 -8
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/tests/test_e2e.py +39 -35
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/tests/test_patch_match.py +24 -0
- pypatchmatch-2.0.2/patchmatch/csrc/nnf.cpp +0 -337
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/LICENSE +0 -0
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/SECURITY.md +0 -0
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/examples/images/forest_pruned.bmp +0 -0
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/patchmatch/__init__.py +0 -0
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/patchmatch/_lib.py +0 -0
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/patchmatch/csrc/pyinterface.h +0 -0
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/patchmatch/py.typed +0 -0
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/scripts/render_readme_example.py +0 -0
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/tests/conftest.py +0 -0
- {pypatchmatch-2.0.2 → pypatchmatch-2.1.1}/tests/test_properties.py +0 -0
|
@@ -77,35 +77,20 @@ Other entry points:
|
|
|
77
77
|
|
|
78
78
|
## Architecture
|
|
79
79
|
|
|
80
|
-
### Layers
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
3. `patchmatch/_lib.py` is the ctypes layer: the `CShapeT`/`CMatT` structures, the dtype
|
|
95
|
-
table, `np_to_pymat`/`pymat_to_np`, `find_library` (looks for `libpatchmatch.so`,
|
|
96
|
-
`libpatchmatch.dylib` or `patchmatch.dll` in every directory of `patchmatch.__path__`)
|
|
97
|
-
and `load_library` (argtypes and restype of every `PM_*` function).
|
|
98
|
-
4. `patchmatch/csrc/pyinterface.{h,cpp}` is the `extern "C"` API: the `PM_*` functions,
|
|
99
|
-
the conversion between `PM_mat_t` and `cv::Mat`, the global settings and the error
|
|
100
|
-
reporting.
|
|
101
|
-
5. `csrc/inpaint.{h,cpp}`: `Inpainting`, the image pyramid and the EM loop.
|
|
102
|
-
6. `csrc/nnf.{h,cpp}`: `NearestNeighborField` (propagation and random search) and the
|
|
103
|
-
distance metrics `PatchSSDDistanceMetric` and `RegularityGuidedPatchDistanceMetricV2`.
|
|
104
|
-
7. `csrc/masked_image.{h,cpp}`: `MaskedImage`, an image with its hole mask and optional
|
|
105
|
-
global mask, down- and upsampling, gradients.
|
|
106
|
-
|
|
107
|
-
The C++ headers double as the C++ API: `examples/cpp_example.cpp` uses `Inpainting` and
|
|
108
|
-
`PatchSSDDistanceMetric` directly.
|
|
80
|
+
### Layers
|
|
81
|
+
|
|
82
|
+
`patchmatch.inpaint` calls through `patch_match.py` (public API, input validation) →
|
|
83
|
+
`_lib.py` (ctypes) → `csrc/pyinterface.*` (`extern "C"`) → `inpaint.*` (`Inpainting`,
|
|
84
|
+
pyramid, EM loop) → `nnf.*` (`NearestNeighborField`, distance metrics) →
|
|
85
|
+
`masked_image.*` (`MaskedImage`).
|
|
86
|
+
|
|
87
|
+
- `patch_match.py` loads the library at import time; on `OSError` (no library) or
|
|
88
|
+
`AttributeError` (one without the `PM_*` symbols) it sets `patchmatch_available =
|
|
89
|
+
False`, and every function then raises `RuntimeError` through `_get_lib()`. Without a
|
|
90
|
+
hole outside the global mask (`_has_holes`) it returns a copy without a native call.
|
|
91
|
+
- `__init__.py` re-exports `patch_match.__all__` (`test_top_level_exports`).
|
|
92
|
+
- The C++ headers double as the C++ API: `examples/cpp_example.cpp` uses `Inpainting`
|
|
93
|
+
and `PatchSSDDistanceMetric` directly.
|
|
109
94
|
|
|
110
95
|
### ctypes instead of a CPython extension
|
|
111
96
|
|
|
@@ -57,8 +57,9 @@ if(PATCHMATCH_SANITIZE)
|
|
|
57
57
|
endif()
|
|
58
58
|
# GCC's -fsanitize=undefined leaves out float-cast-overflow
|
|
59
59
|
set(PATCHMATCH_SANITIZERS -fsanitize=address,undefined,float-cast-overflow)
|
|
60
|
+
# -UNDEBUG keeps the asserts, which check the incremental distances of the propagation
|
|
60
61
|
target_compile_options(patchmatch PRIVATE ${PATCHMATCH_SANITIZERS}
|
|
61
|
-
-fno-sanitize-recover=all -fno-omit-frame-pointer -g)
|
|
62
|
+
-fno-sanitize-recover=all -fno-omit-frame-pointer -g -UNDEBUG)
|
|
62
63
|
target_link_options(patchmatch PRIVATE ${PATCHMATCH_SANITIZERS})
|
|
63
64
|
endif()
|
|
64
65
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPatchMatch
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: This library implements the PatchMatch based inpainting algorithm.
|
|
5
5
|
Keywords: inpainting,patchmatch,image-processing,opencv
|
|
6
6
|
Author-Email: The InvokeAI Project <lincoln.stein@gmail.com>, Jiayuan Mao <maojiayuan@gmail.com>, Kyle Schouviller <kyle0654@hotmail.com>, Matthias Wild <mauwii@outlook.de>, Younesse ANDAM <younesse.andam@gmail.com>
|
|
@@ -30,19 +30,20 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
|
|
31
31
|
# PatchMatch based Inpainting
|
|
32
32
|
|
|
33
|
-
[](https://github.com/mauwii/PyPatchMatch/actions/workflows/ci.yml)
|
|
34
33
|
[](https://sonarcloud.io/summary/new_code?id=mauwii_PyPatchMatch)
|
|
35
34
|
[](https://github.com/mauwii/PyPatchMatch/blob/main/LICENSE)
|
|
36
35
|
[](https://pypi.org/project/PyPatchMatch/)
|
|
37
36
|
[](https://pepy.tech/projects/pypatchmatch)
|
|
38
37
|
[](https://github.com/astral-sh/ruff)
|
|
38
|
+
[](https://github.com/DietrichGebert/ponytail)
|
|
39
|
+
[](https://github.com/JuliusBrussee/caveman)
|
|
39
40
|
|
|
40
41
|
This library implements the PatchMatch based inpainting algorithm. It provides both C++
|
|
41
42
|
and Python interfaces. This implementation is heavily based on the implementation by
|
|
42
43
|
Younesse ANDAM: [younesse-cv/PatchMatch](https://github.com/younesse-cv/PatchMatch),
|
|
43
44
|
with some bug fixes, and updates.
|
|
44
45
|
|
|
45
|
-

|
|
46
47
|
|
|
47
48
|
From left to right: the photo, the input of
|
|
48
49
|
[examples/py_example.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example.py)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# PatchMatch based Inpainting
|
|
2
2
|
|
|
3
|
-
[](https://github.com/mauwii/PyPatchMatch/actions/workflows/ci.yml)
|
|
4
3
|
[](https://sonarcloud.io/summary/new_code?id=mauwii_PyPatchMatch)
|
|
5
4
|
[](https://github.com/mauwii/PyPatchMatch/blob/main/LICENSE)
|
|
6
5
|
[](https://pypi.org/project/PyPatchMatch/)
|
|
7
6
|
[](https://pepy.tech/projects/pypatchmatch)
|
|
8
7
|
[](https://github.com/astral-sh/ruff)
|
|
8
|
+
[](https://github.com/DietrichGebert/ponytail)
|
|
9
|
+
[](https://github.com/JuliusBrussee/caveman)
|
|
9
10
|
|
|
10
11
|
This library implements the PatchMatch based inpainting algorithm. It provides both C++
|
|
11
12
|
and Python interfaces. This implementation is heavily based on the implementation by
|
|
Binary file
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.1.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 1, 1)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gef1a63c74'
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
#include <algorithm>
|
|
4
4
|
#include <array>
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
#include <cstdlib>
|
|
5
7
|
#include <iostream>
|
|
8
|
+
#include <optional>
|
|
6
9
|
|
|
7
10
|
namespace
|
|
8
11
|
{
|
|
@@ -30,6 +33,108 @@ namespace
|
|
|
30
33
|
return table;
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
// Calls visit(y, x) for the pixels of the image within radius of (cy, cx).
|
|
37
|
+
template <typename Visit>
|
|
38
|
+
void for_window(cv::Size size, int cy, int cx, int radius, Visit visit)
|
|
39
|
+
{
|
|
40
|
+
for (int y = std::max(0, cy - radius); y <= std::min(size.height - 1, cy + radius); ++y)
|
|
41
|
+
for (int x = std::max(0, cx - radius); x <= std::min(size.width - 1, cx + radius); ++x)
|
|
42
|
+
visit(y, x);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Mean of value(y, x) over the pixels within radius of (cy, cx) for which select(y, x) holds.
|
|
46
|
+
template <typename Select, typename Value>
|
|
47
|
+
cv::Vec3d window_mean(cv::Size size, int cy, int cx, int radius, Select select, Value value)
|
|
48
|
+
{
|
|
49
|
+
cv::Vec3d sum;
|
|
50
|
+
int count = 0;
|
|
51
|
+
for_window(size, cy, cx, radius, [&sum, &count, &select, &value](int y, int x) {
|
|
52
|
+
if (select(y, x))
|
|
53
|
+
{
|
|
54
|
+
sum += value(y, x);
|
|
55
|
+
++count;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return count > 0 ? sum / count : sum;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
bool is_known(const MaskedImage &image, int y, int x)
|
|
62
|
+
{
|
|
63
|
+
return !image.is_masked(y, x) && !image.is_globally_masked(y, x);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
bool is_hole(const MaskedImage &image, int y, int x)
|
|
67
|
+
{
|
|
68
|
+
return image.is_masked(y, x) && !image.is_globally_masked(y, x);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Chessboard distance of the hole pixels to the nearest known pixel, 0 beyond radius.
|
|
72
|
+
cv::Mat hole_depths(const MaskedImage &image, int radius)
|
|
73
|
+
{
|
|
74
|
+
const auto size = image.size();
|
|
75
|
+
cv::Mat depth(size, CV_32S, cv::Scalar(0));
|
|
76
|
+
for (int cy = 0; cy < size.height; ++cy)
|
|
77
|
+
{
|
|
78
|
+
for (int cx = 0; cx < size.width; ++cx)
|
|
79
|
+
{
|
|
80
|
+
if (!is_hole(image, cy, cx))
|
|
81
|
+
continue;
|
|
82
|
+
int &d = depth.at<int>(cy, cx);
|
|
83
|
+
for_window(size, cy, cx, radius, [&d, &image, cy, cx](int y, int x) {
|
|
84
|
+
const int distance = std::max(std::abs(y - cy), std::abs(x - cx));
|
|
85
|
+
if (is_known(image, y, x) && (d == 0 || distance < d))
|
|
86
|
+
d = distance;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return depth;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Shifts the hole pixels near the border by the color step across it, which showed as an outline.
|
|
94
|
+
void blend_hole_borders(cv::Mat &result, const MaskedImage &initial)
|
|
95
|
+
{
|
|
96
|
+
constexpr int kWidth = 4;
|
|
97
|
+
constexpr int kStepRadius = 1;
|
|
98
|
+
constexpr int kSpreadRadius = 4;
|
|
99
|
+
const auto size = initial.size();
|
|
100
|
+
const auto known = [&initial](int y, int x) { return is_known(initial, y, x); };
|
|
101
|
+
const auto hole = [&initial](int y, int x) { return is_hole(initial, y, x); };
|
|
102
|
+
const auto color = [&result](int y, int x) {
|
|
103
|
+
const auto *pixel = result.ptr<unsigned char>(y, x);
|
|
104
|
+
return cv::Vec3d(pixel[0], pixel[1], pixel[2]);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const cv::Mat depth = hole_depths(initial, kWidth);
|
|
108
|
+
const auto border = [&depth](int y, int x) { return depth.at<int>(y, x) == 1; };
|
|
109
|
+
|
|
110
|
+
cv::Mat steps(size, CV_64FC3, cv::Scalar::all(0));
|
|
111
|
+
for (int y = 0; y < size.height; ++y)
|
|
112
|
+
{
|
|
113
|
+
for (int x = 0; x < size.width; ++x)
|
|
114
|
+
{
|
|
115
|
+
if (border(y, x))
|
|
116
|
+
steps.at<cv::Vec3d>(y, x) = window_mean(size, y, x, kStepRadius, known, color) -
|
|
117
|
+
window_mean(size, y, x, kStepRadius, hole, color);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const auto step = [&steps](int y, int x) { return steps.at<cv::Vec3d>(y, x); };
|
|
122
|
+
for (int y = 0; y < size.height; ++y)
|
|
123
|
+
{
|
|
124
|
+
for (int x = 0; x < size.width; ++x)
|
|
125
|
+
{
|
|
126
|
+
const int d = depth.at<int>(y, x);
|
|
127
|
+
if (d == 0)
|
|
128
|
+
continue;
|
|
129
|
+
const cv::Vec3d shift =
|
|
130
|
+
window_mean(size, y, x, kSpreadRadius, border, step) * (kWidth + 1 - d) / kWidth;
|
|
131
|
+
auto *pixel = result.ptr<unsigned char>(y, x);
|
|
132
|
+
for (int c = 0; c < 3; ++c)
|
|
133
|
+
pixel[c] = cv::saturate_cast<unsigned char>(pixel[c] + shift[c]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
33
138
|
template <typename... Args>
|
|
34
139
|
void trace(bool verbose, const Args &...args)
|
|
35
140
|
{
|
|
@@ -70,14 +175,59 @@ namespace
|
|
|
70
175
|
target_ptr[3] = weight;
|
|
71
176
|
}
|
|
72
177
|
|
|
178
|
+
// Counts the holes of an image in rectangles, from the number of holes above and left of each pixel.
|
|
179
|
+
class HoleCounter
|
|
180
|
+
{
|
|
181
|
+
public:
|
|
182
|
+
explicit HoleCounter(const MaskedImage &image)
|
|
183
|
+
: m_size(image.size()), m_sums(static_cast<std::size_t>(m_size.height + 1) * (m_size.width + 1), 0)
|
|
184
|
+
{
|
|
185
|
+
for (int y = 0; y < m_size.height; ++y)
|
|
186
|
+
{
|
|
187
|
+
for (int x = 0; x < m_size.width; ++x)
|
|
188
|
+
{
|
|
189
|
+
const std::int64_t hole = image.is_masked(y, x) ? 1 : 0;
|
|
190
|
+
sum(y + 1, x + 1) = hole + sum(y, x + 1) + sum(y + 1, x) - sum(y, x);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Whether the rectangle from (y0, x0) to (y1, x1), clipped to the image, contains a hole.
|
|
196
|
+
bool any(std::int64_t y0, std::int64_t x0, std::int64_t y1, std::int64_t x1) const
|
|
197
|
+
{
|
|
198
|
+
const auto clip = [](std::int64_t value, int size) {
|
|
199
|
+
return static_cast<int>(std::clamp<std::int64_t>(value, 0, size));
|
|
200
|
+
};
|
|
201
|
+
const int top = clip(y0, m_size.height);
|
|
202
|
+
const int bottom = clip(y1 + 1, m_size.height);
|
|
203
|
+
const int left = clip(x0, m_size.width);
|
|
204
|
+
const int right = clip(x1 + 1, m_size.width);
|
|
205
|
+
return sum(bottom, right) - sum(top, right) - sum(bottom, left) + sum(top, left) > 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private:
|
|
209
|
+
std::int64_t &sum(int y, int x)
|
|
210
|
+
{
|
|
211
|
+
return m_sums[static_cast<std::size_t>(y) * (m_size.width + 1) + x];
|
|
212
|
+
}
|
|
213
|
+
std::int64_t sum(int y, int x) const
|
|
214
|
+
{
|
|
215
|
+
return m_sums[static_cast<std::size_t>(y) * (m_size.width + 1) + x];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
cv::Size m_size;
|
|
219
|
+
std::vector<std::int64_t> m_sums;
|
|
220
|
+
};
|
|
221
|
+
|
|
73
222
|
// Casts the votes of one nearest-neighbor field into the vote image.
|
|
74
223
|
class VoteCaster
|
|
75
224
|
{
|
|
76
225
|
public:
|
|
226
|
+
// With holes, only the patches whose votes reach one of them are cast.
|
|
77
227
|
VoteCaster(
|
|
78
228
|
const NearestNeighborField &nnf, bool source2target, cv::Mat &vote, const MaskedImage &source,
|
|
79
|
-
bool upscaled, bool best_only)
|
|
80
|
-
: m_nnf(&nnf), m_vote(&vote), m_source(&source), m_source_size(nnf.source_size()),
|
|
229
|
+
bool upscaled, bool best_only, const HoleCounter *holes)
|
|
230
|
+
: m_nnf(&nnf), m_vote(&vote), m_source(&source), m_holes(holes), m_source_size(nnf.source_size()),
|
|
81
231
|
m_target_size(nnf.target_size()), m_source2target(source2target), m_upscaled(upscaled),
|
|
82
232
|
m_best_only(best_only)
|
|
83
233
|
{
|
|
@@ -89,6 +239,8 @@ namespace
|
|
|
89
239
|
{
|
|
90
240
|
const int yp = m_nnf->at(i, j, 0);
|
|
91
241
|
const int xp = m_nnf->at(i, j, 1);
|
|
242
|
+
if (m_holes && !votes_for_hole(m_source2target ? yp : i, m_source2target ? xp : j, patch_size))
|
|
243
|
+
return;
|
|
92
244
|
for (int di = -patch_size; di <= patch_size; ++di)
|
|
93
245
|
{
|
|
94
246
|
for (int dj = -patch_size; dj <= patch_size; ++dj)
|
|
@@ -103,12 +255,23 @@ namespace
|
|
|
103
255
|
if (yt < 0 || yt >= m_target_size.height || xt < 0 || xt >= m_target_size.width ||
|
|
104
256
|
m_nnf->target().is_globally_masked(yt, xt))
|
|
105
257
|
continue;
|
|
106
|
-
|
|
258
|
+
// Near ties go to the patch centered closest, not the first cast, which made blocks.
|
|
259
|
+
const double offset2 = static_cast<double>(di) * di + static_cast<double>(dj) * dj;
|
|
260
|
+
cast_pair(ys, xs, yt, xt, m_best_only ? weight * (1 - 1e-5 * offset2) : weight);
|
|
107
261
|
}
|
|
108
262
|
}
|
|
109
263
|
}
|
|
110
264
|
|
|
111
265
|
private:
|
|
266
|
+
// Whether the patch around (y, x) of the voted image, scaled like the votes, has a hole.
|
|
267
|
+
bool votes_for_hole(int y, int x, int patch_size) const
|
|
268
|
+
{
|
|
269
|
+
const std::int64_t scale = m_upscaled ? 2 : 1;
|
|
270
|
+
return m_holes->any(
|
|
271
|
+
scale * (std::int64_t{y} - patch_size), scale * (std::int64_t{x} - patch_size),
|
|
272
|
+
scale * (std::int64_t{y} + patch_size + 1) - 1, scale * (std::int64_t{x} + patch_size + 1) - 1);
|
|
273
|
+
}
|
|
274
|
+
|
|
112
275
|
void cast_pair(int ys, int xs, int yt, int xt, double weight) const
|
|
113
276
|
{
|
|
114
277
|
if (!m_source2target)
|
|
@@ -137,6 +300,7 @@ namespace
|
|
|
137
300
|
const NearestNeighborField *m_nnf;
|
|
138
301
|
cv::Mat *m_vote;
|
|
139
302
|
const MaskedImage *m_source;
|
|
303
|
+
const HoleCounter *m_holes;
|
|
140
304
|
cv::Size m_source_size;
|
|
141
305
|
cv::Size m_target_size;
|
|
142
306
|
bool m_source2target;
|
|
@@ -198,18 +362,21 @@ cv::Mat Inpainting::run(bool verbose, unsigned int random_seed)
|
|
|
198
362
|
|
|
199
363
|
source = m_pyramid[level];
|
|
200
364
|
|
|
201
|
-
|
|
365
|
+
const bool coarsest = level == nr_levels - 1;
|
|
366
|
+
if (coarsest)
|
|
202
367
|
{
|
|
203
368
|
target = source.clone();
|
|
204
369
|
target.clear_mask();
|
|
205
|
-
m_source2target = NearestNeighborField(source, target, m_distance_metric);
|
|
206
|
-
m_target2source = NearestNeighborField(target, source, m_distance_metric);
|
|
207
370
|
}
|
|
371
|
+
// The votes of the completeness field keep the known pixels near their input on the
|
|
372
|
+
// coarse levels; on levels 0 and 1, which keep the known pixels, they change nothing.
|
|
373
|
+
if (level < 2)
|
|
374
|
+
m_source2target.reset();
|
|
208
375
|
else
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
376
|
+
m_source2target = coarsest ? NearestNeighborField(source, target, m_distance_metric)
|
|
377
|
+
: NearestNeighborField(source, target, m_distance_metric, *m_source2target);
|
|
378
|
+
m_target2source = coarsest ? NearestNeighborField(target, source, m_distance_metric)
|
|
379
|
+
: NearestNeighborField(target, source, m_distance_metric, m_target2source);
|
|
213
380
|
|
|
214
381
|
trace(verbose, "Initialization done.");
|
|
215
382
|
|
|
@@ -232,6 +399,7 @@ cv::Mat Inpainting::run(bool verbose, unsigned int random_seed)
|
|
|
232
399
|
std::copy(input, input + 3, result.ptr<unsigned char>(y, x));
|
|
233
400
|
}
|
|
234
401
|
}
|
|
402
|
+
blend_hole_borders(result, m_initial);
|
|
235
403
|
return result;
|
|
236
404
|
}
|
|
237
405
|
|
|
@@ -250,16 +418,18 @@ MaskedImage Inpainting::_expectation_maximization(
|
|
|
250
418
|
{
|
|
251
419
|
if (iter_em != 0)
|
|
252
420
|
{
|
|
253
|
-
m_source2target
|
|
421
|
+
if (m_source2target)
|
|
422
|
+
m_source2target->set_target(new_target);
|
|
254
423
|
m_target2source.set_source(new_target);
|
|
255
424
|
target = new_target;
|
|
256
425
|
}
|
|
257
426
|
|
|
258
427
|
trace(verbose, "EM Iteration: ", iter_em);
|
|
259
428
|
|
|
260
|
-
|
|
429
|
+
_update_links(source, iter_em != 0);
|
|
261
430
|
trace(verbose, " NNF minimization started.");
|
|
262
|
-
m_source2target
|
|
431
|
+
if (m_source2target)
|
|
432
|
+
m_source2target->minimize(nr_iters_nnf);
|
|
263
433
|
m_target2source.minimize(nr_iters_nnf);
|
|
264
434
|
trace(verbose, " NNF minimization finished.");
|
|
265
435
|
|
|
@@ -288,38 +458,49 @@ MaskedImage Inpainting::_expectation_maximization(
|
|
|
288
458
|
// structure that the finer levels refine, the best vote there makes seams.
|
|
289
459
|
const bool best_only = level == 0 && iter_em == nr_iters_em - 1;
|
|
290
460
|
|
|
291
|
-
// Votes for best patch from NNF Source->Target (completeness) and Target->Source (coherence).
|
|
292
|
-
_expectation_step(m_source2target, true, vote, new_source, upscaled, best_only);
|
|
293
|
-
trace(verbose, " Expectation source to target finished.");
|
|
294
|
-
_expectation_step(m_target2source, false, vote, new_source, upscaled, best_only);
|
|
295
|
-
trace(verbose, " Expectation target to source finished.");
|
|
296
|
-
|
|
297
|
-
// Compile votes and update pixel values.
|
|
298
461
|
// Known pixels are kept on the two finest levels only. On coarser levels those
|
|
299
462
|
// next to the holes average only the known part of their downsampling kernel,
|
|
300
463
|
// estimates that reach into the holes; keeping them there made the fills copy
|
|
301
464
|
// smooth regions, e.g. a concrete wall into the foliage above it.
|
|
302
465
|
const int new_level = upscaled ? level - 1 : level;
|
|
303
|
-
|
|
466
|
+
const bool keep_known = new_level <= 1;
|
|
467
|
+
|
|
468
|
+
// Votes for best patch from NNF Source->Target (completeness) and Target->Source (coherence).
|
|
469
|
+
if (m_source2target)
|
|
470
|
+
{
|
|
471
|
+
_expectation_step(*m_source2target, true, vote, new_source, upscaled, best_only, keep_known);
|
|
472
|
+
trace(verbose, " Expectation source to target finished.");
|
|
473
|
+
}
|
|
474
|
+
_expectation_step(m_target2source, false, vote, new_source, upscaled, best_only, keep_known);
|
|
475
|
+
trace(verbose, " Expectation target to source finished.");
|
|
476
|
+
|
|
477
|
+
// Compile votes and update pixel values.
|
|
478
|
+
_maximization_step(new_target, vote, new_source, keep_known);
|
|
304
479
|
trace(verbose, " Minimization step finished.");
|
|
305
480
|
}
|
|
306
481
|
|
|
307
482
|
return new_target;
|
|
308
483
|
}
|
|
309
484
|
|
|
310
|
-
// Patches without holes are their own nearest neighbors
|
|
311
|
-
|
|
485
|
+
// Patches without holes are their own nearest neighbors; the others keep their links,
|
|
486
|
+
// measured again on a changed image. Globally masked pixels have no link.
|
|
487
|
+
void Inpainting::_update_links(const MaskedImage &source, bool image_changed)
|
|
312
488
|
{
|
|
489
|
+
const auto for_each_field = [this](const auto &update) {
|
|
490
|
+
if (m_source2target)
|
|
491
|
+
update(*m_source2target);
|
|
492
|
+
update(m_target2source);
|
|
493
|
+
};
|
|
313
494
|
const int patch_size = m_distance_metric->patch_size();
|
|
314
495
|
const auto size = source.size();
|
|
315
496
|
for (int i = 0; i < size.height; ++i)
|
|
316
497
|
{
|
|
317
498
|
for (int j = 0; j < size.width; ++j)
|
|
318
499
|
{
|
|
319
|
-
if (source.contains_mask(i, j, patch_size))
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
500
|
+
if (!source.contains_mask(i, j, patch_size))
|
|
501
|
+
for_each_field([i, j](NearestNeighborField &field) { field.set_identity(i, j); });
|
|
502
|
+
else if (image_changed && !source.is_globally_masked(i, j))
|
|
503
|
+
for_each_field([i, j](NearestNeighborField &field) { field.update_distance(i, j); });
|
|
323
504
|
}
|
|
324
505
|
}
|
|
325
506
|
}
|
|
@@ -327,9 +508,14 @@ void Inpainting::_link_patches_without_holes(const MaskedImage &source)
|
|
|
327
508
|
// Expectation step: vote for best estimations of each pixel.
|
|
328
509
|
void Inpainting::_expectation_step(
|
|
329
510
|
const NearestNeighborField &nnf, bool source2target, cv::Mat &vote, const MaskedImage &source, bool upscaled,
|
|
330
|
-
bool best_only) const
|
|
511
|
+
bool best_only, bool keep_known) const
|
|
331
512
|
{
|
|
332
|
-
|
|
513
|
+
// Votes for known pixels do not count where the maximization step keeps them.
|
|
514
|
+
std::optional<HoleCounter> holes;
|
|
515
|
+
if (keep_known)
|
|
516
|
+
holes.emplace(source);
|
|
517
|
+
const VoteCaster caster(
|
|
518
|
+
nnf, source2target, vote, source, upscaled, best_only, holes.has_value() ? &holes.value() : nullptr);
|
|
333
519
|
const int patch_size = m_distance_metric->patch_size();
|
|
334
520
|
const auto &kDistance2Similarity = distance2similarity();
|
|
335
521
|
const auto source_size = nnf.source_size();
|
|
@@ -380,10 +566,6 @@ void Inpainting::_maximization_step(
|
|
|
380
566
|
target_ptr[1] = g;
|
|
381
567
|
target_ptr[2] = b;
|
|
382
568
|
}
|
|
383
|
-
else
|
|
384
|
-
{
|
|
385
|
-
target.set_mask(i, j, false);
|
|
386
|
-
}
|
|
387
569
|
}
|
|
388
570
|
}
|
|
389
571
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
|
+
#include <optional>
|
|
3
4
|
#include <vector>
|
|
4
5
|
|
|
5
6
|
#include "masked_image.h"
|
|
@@ -16,16 +17,16 @@ public:
|
|
|
16
17
|
private:
|
|
17
18
|
void _initialize_pyramid(void);
|
|
18
19
|
MaskedImage _expectation_maximization(const MaskedImage &source, MaskedImage target, int level, bool verbose);
|
|
19
|
-
void
|
|
20
|
+
void _update_links(const MaskedImage &source, bool image_changed);
|
|
20
21
|
void _expectation_step(
|
|
21
22
|
const NearestNeighborField &nnf, bool source2target, cv::Mat &vote, const MaskedImage &source, bool upscaled,
|
|
22
|
-
bool best_only) const;
|
|
23
|
+
bool best_only, bool keep_known) const;
|
|
23
24
|
void _maximization_step(MaskedImage &target, const cv::Mat &vote, const MaskedImage &source, bool keep_known) const;
|
|
24
25
|
|
|
25
26
|
MaskedImage m_initial;
|
|
26
27
|
std::vector<MaskedImage> m_pyramid;
|
|
27
28
|
|
|
28
|
-
NearestNeighborField m_source2target;
|
|
29
|
+
std::optional<NearestNeighborField> m_source2target;
|
|
29
30
|
NearestNeighborField m_target2source;
|
|
30
31
|
const PatchDistanceMetric *m_distance_metric;
|
|
31
32
|
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#include "masked_image.h"
|
|
2
2
|
|
|
3
3
|
#include <algorithm>
|
|
4
|
+
#include <array>
|
|
4
5
|
#include <cstdint>
|
|
5
6
|
|
|
6
|
-
const cv::Size MaskedImage::kDownsampleKernelSize = cv::Size(6, 6);
|
|
7
|
-
const std::array<int, 6> MaskedImage::kDownsampleKernel = {1, 5, 10, 10, 5, 1};
|
|
8
|
-
|
|
9
7
|
namespace
|
|
10
8
|
{
|
|
9
|
+
constexpr std::array<int, 6> kDownsampleKernel = {1, 5, 10, 10, 5, 1};
|
|
10
|
+
|
|
11
11
|
struct KernelSum
|
|
12
12
|
{
|
|
13
13
|
int r = 0;
|
|
@@ -20,14 +20,13 @@ namespace
|
|
|
20
20
|
// The known pixels under the downsampling kernel at (y, x), weighted by the kernel.
|
|
21
21
|
KernelSum sum_kernel(const MaskedImage &image, int y, int x)
|
|
22
22
|
{
|
|
23
|
-
|
|
24
|
-
const auto &kernel = MaskedImage::kDownsampleKernel;
|
|
23
|
+
constexpr int half = static_cast<int>(kDownsampleKernel.size()) / 2;
|
|
25
24
|
const auto size = image.size();
|
|
26
25
|
|
|
27
26
|
KernelSum sum;
|
|
28
|
-
for (int dy = -
|
|
27
|
+
for (int dy = -half + 1; dy <= half; ++dy)
|
|
29
28
|
{
|
|
30
|
-
for (int dx = -
|
|
29
|
+
for (int dx = -half + 1; dx <= half; ++dx)
|
|
31
30
|
{
|
|
32
31
|
const int yy = y + dy;
|
|
33
32
|
const int xx = x + dx;
|
|
@@ -40,7 +39,7 @@ namespace
|
|
|
40
39
|
continue;
|
|
41
40
|
|
|
42
41
|
const auto *source_ptr = image.get_image(yy, xx);
|
|
43
|
-
const int k =
|
|
42
|
+
const int k = kDownsampleKernel[half - 1 + dy] * kDownsampleKernel[half - 1 + dx];
|
|
44
43
|
sum.r += source_ptr[0] * k;
|
|
45
44
|
sum.g += source_ptr[1] * k;
|
|
46
45
|
sum.b += source_ptr[2] * k;
|
|
@@ -72,14 +71,14 @@ MaskedImage MaskedImage::downsample() const
|
|
|
72
71
|
{
|
|
73
72
|
const auto size = this->size();
|
|
74
73
|
auto ret = MaskedImage(size.width / 2, size.height / 2);
|
|
75
|
-
if (
|
|
74
|
+
if (m_has_global_mask)
|
|
76
75
|
ret.init_global_mask_mat();
|
|
77
76
|
for (int y = 0; y < size.height - 1; y += 2)
|
|
78
77
|
{
|
|
79
78
|
for (int x = 0; x < size.width - 1; x += 2)
|
|
80
79
|
{
|
|
81
80
|
const auto sum = sum_kernel(*this, y, x);
|
|
82
|
-
if (
|
|
81
|
+
if (m_has_global_mask)
|
|
83
82
|
ret.set_global_mask(y / 2, x / 2, sum.globally_masked);
|
|
84
83
|
if (sum.weight == 0)
|
|
85
84
|
{
|
|
@@ -102,7 +101,7 @@ MaskedImage MaskedImage::upsample(int new_w, int new_h) const
|
|
|
102
101
|
{
|
|
103
102
|
const auto size = this->size();
|
|
104
103
|
auto ret = MaskedImage(new_w, new_h);
|
|
105
|
-
if (
|
|
104
|
+
if (m_has_global_mask)
|
|
106
105
|
ret.init_global_mask_mat();
|
|
107
106
|
for (int y = 0; y < new_h; ++y)
|
|
108
107
|
{
|
|
@@ -118,7 +117,7 @@ MaskedImage MaskedImage::upsample(int new_w, int new_h) const
|
|
|
118
117
|
ret.set_mask(y, x, true);
|
|
119
118
|
continue;
|
|
120
119
|
}
|
|
121
|
-
if (
|
|
120
|
+
if (m_has_global_mask)
|
|
122
121
|
ret.set_global_mask(y, x, false);
|
|
123
122
|
if (is_masked(yy, xx))
|
|
124
123
|
{
|
|
@@ -151,25 +150,30 @@ void MaskedImage::compute_image_gradients() const
|
|
|
151
150
|
const auto size = m_image.size();
|
|
152
151
|
m_image_grady = cv::Mat(size, CV_8UC3);
|
|
153
152
|
m_image_gradx = cv::Mat(size, CV_8UC3);
|
|
154
|
-
m_image_grady = cv::Scalar::all(0);
|
|
155
|
-
m_image_gradx = cv::Scalar::all(0);
|
|
156
153
|
|
|
157
|
-
|
|
154
|
+
// The border replicates its pixels, so that patches over the outermost line compare it as well.
|
|
155
|
+
for (int y = 0; y < size.height; ++y)
|
|
158
156
|
{
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
const auto *ptrx2 = m_image.ptr<unsigned char>(i, 0) - 3;
|
|
163
|
-
auto *mptry = m_image_grady.ptr<unsigned char>(i, 0);
|
|
164
|
-
auto *mptrx = m_image_gradx.ptr<unsigned char>(i, 0);
|
|
165
|
-
for (int j = 3; j < size.width * 3 - 3; ++j)
|
|
157
|
+
const int up = std::max(y - 1, 0);
|
|
158
|
+
const int down = std::min(y + 1, size.height - 1);
|
|
159
|
+
for (int x = 0; x < size.width; ++x)
|
|
166
160
|
{
|
|
161
|
+
const int left = std::max(x - 1, 0);
|
|
162
|
+
const int right = std::min(x + 1, size.width - 1);
|
|
167
163
|
// A neutral gradient next to a globally masked pixel keeps its color out of the distances.
|
|
168
|
-
const
|
|
169
|
-
const bool
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
164
|
+
const bool skip_y = is_globally_masked(down, x) || is_globally_masked(up, x);
|
|
165
|
+
const bool skip_x = is_globally_masked(y, right) || is_globally_masked(y, left);
|
|
166
|
+
const unsigned char *below = get_image(down, x);
|
|
167
|
+
const unsigned char *above = get_image(up, x);
|
|
168
|
+
const unsigned char *next = get_image(y, right);
|
|
169
|
+
const unsigned char *previous = get_image(y, left);
|
|
170
|
+
auto *grady = m_image_grady.ptr<unsigned char>(y, x);
|
|
171
|
+
auto *gradx = m_image_gradx.ptr<unsigned char>(y, x);
|
|
172
|
+
for (int c = 0; c < 3; ++c)
|
|
173
|
+
{
|
|
174
|
+
grady[c] = static_cast<unsigned char>(skip_y ? 128 : below[c] / 2 - above[c] / 2 + 128);
|
|
175
|
+
gradx[c] = static_cast<unsigned char>(skip_x ? 128 : next[c] / 2 - previous[c] / 2 + 128);
|
|
176
|
+
}
|
|
173
177
|
}
|
|
174
178
|
}
|
|
175
179
|
|