PyPatchMatch 2.0.1__tar.gz → 2.1.0__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.
Files changed (29) hide show
  1. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/CLAUDE.md +14 -26
  2. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/PKG-INFO +10 -8
  3. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/README.md +8 -6
  4. pypatchmatch-2.1.0/examples/images/chelsea.png +0 -0
  5. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/_version.py +3 -3
  6. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/csrc/inpaint.cpp +215 -33
  7. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/csrc/inpaint.h +4 -3
  8. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/csrc/masked_image.cpp +35 -29
  9. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/csrc/masked_image.h +28 -24
  10. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/csrc/nnf.cpp +73 -55
  11. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/csrc/nnf.h +41 -23
  12. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/csrc/pyinterface.cpp +18 -27
  13. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/patch_match.py +35 -11
  14. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/pyproject.toml +6 -2
  15. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/scripts/build_opencv.py +3 -6
  16. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/scripts/evaluate_inpainting.py +48 -8
  17. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/tests/test_e2e.py +39 -35
  18. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/tests/test_patch_match.py +128 -12
  19. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/CMakeLists.txt +0 -0
  20. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/LICENSE +0 -0
  21. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/SECURITY.md +0 -0
  22. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/examples/images/forest_pruned.bmp +0 -0
  23. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/__init__.py +0 -0
  24. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/_lib.py +0 -0
  25. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/csrc/pyinterface.h +0 -0
  26. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/patchmatch/py.typed +0 -0
  27. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/scripts/render_readme_example.py +0 -0
  28. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/tests/conftest.py +0 -0
  29. {pypatchmatch-2.0.1 → pypatchmatch-2.1.0}/tests/test_properties.py +0 -0
@@ -77,32 +77,20 @@ Other entry points:
77
77
 
78
78
  ## Architecture
79
79
 
80
- ### Layers, in the call order of `patchmatch.inpaint`
81
-
82
- 1. `patchmatch/__init__.py` re-exports the public API of `patch_match.py` and
83
- `__version__` from the generated `_version.py`. `test_top_level_exports` checks that
84
- every name in `patch_match.__all__` is re-exported.
85
- 2. `patchmatch/patch_match.py` is the public API. It validates and canonicalizes the
86
- inputs (`_prepare_inputs`, `_check_patch_size`, the `ijmap` and `guide_weight`
87
- checks), picks the entry point (`PM_inpaint`, `PM_inpaint2` with a global mask, and
88
- their `_regularity` variants) and runs it through `_call`. It loads the library at
89
- import time; on `OSError` it logs a warning and sets `patchmatch_available = False`,
90
- and every function then raises `RuntimeError` through `_get_lib()`.
91
- 3. `patchmatch/_lib.py` is the ctypes layer: the `CShapeT`/`CMatT` structures, the dtype
92
- table, `np_to_pymat`/`pymat_to_np`, `find_library` (looks for `libpatchmatch.so`,
93
- `libpatchmatch.dylib` or `patchmatch.dll` in every directory of `patchmatch.__path__`)
94
- and `load_library` (argtypes and restype of every `PM_*` function).
95
- 4. `patchmatch/csrc/pyinterface.{h,cpp}` is the `extern "C"` API: the `PM_*` functions,
96
- the conversion between `PM_mat_t` and `cv::Mat`, the global settings and the error
97
- reporting.
98
- 5. `csrc/inpaint.{h,cpp}`: `Inpainting`, the image pyramid and the EM loop.
99
- 6. `csrc/nnf.{h,cpp}`: `NearestNeighborField` (propagation and random search) and the
100
- distance metrics `PatchSSDDistanceMetric` and `RegularityGuidedPatchDistanceMetricV2`.
101
- 7. `csrc/masked_image.{h,cpp}`: `MaskedImage`, an image with its hole mask and optional
102
- global mask, down- and upsampling, gradients.
103
-
104
- The C++ headers double as the C++ API: `examples/cpp_example.cpp` uses `Inpainting` and
105
- `PatchSSDDistanceMetric` directly.
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.
106
94
 
107
95
  ### ctypes instead of a CPython extension
108
96
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPatchMatch
3
- Version: 2.0.1
3
+ Version: 2.1.0
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
- [![CI](https://github.com/mauwii/PyPatchMatch/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mauwii/PyPatchMatch/actions/workflows/ci.yml)
34
33
  [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=mauwii_PyPatchMatch&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=mauwii_PyPatchMatch)
35
34
  [![License: MIT](https://img.shields.io/badge/License-MIT-blueviolet.svg)](https://github.com/mauwii/PyPatchMatch/blob/main/LICENSE)
36
35
  [![PyPI](https://img.shields.io/pypi/v/PyPatchMatch)](https://pypi.org/project/PyPatchMatch/)
37
36
  [![Downloads](https://static.pepy.tech/badge/pypatchmatch)](https://pepy.tech/projects/pypatchmatch)
38
37
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
38
+ [![ponytail](https://img.shields.io/badge/over--engineering-ponytail_audited-111111)](https://github.com/DietrichGebert/ponytail)
39
+ [![caveman](https://img.shields.io/badge/agent-caveman-F0A63C)](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
- ![Birdhouses removed from a photo by patchmatch.inpaint](https://raw.githubusercontent.com/mauwii/PyPatchMatch/v2.0.1/examples/images/readme_example.jpg)
46
+ ![Birdhouses removed from a photo by patchmatch.inpaint](https://raw.githubusercontent.com/mauwii/PyPatchMatch/v2.1.0/examples/images/readme_example.jpg)
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)
@@ -99,11 +100,12 @@ The previous import path `from patchmatch import patch_match` keeps working.
99
100
  pixels in each direction. Larger patches follow larger structures but are much slower;
100
101
  the default of 15 compares 31x31 patches, the examples use 3.
101
102
 
102
- `patchmatch.set_random_seed(seed)` sets the seed of the randomized search and
103
- `patchmatch.set_verbose(True)` prints the progress of the native code to stderr.
104
- `patchmatch.inpaint_regularity(image, mask, ijmap)` additionally guides the search with
105
- a regularity map, an HxWx3 float32 array with the regularity coordinates of each pixel
106
- in its first two channels. Its `guide_weight` must be at least 0.
103
+ `patchmatch.set_random_seed(seed)` sets the seed of the randomized search, modulo
104
+ `2**32`, and `patchmatch.set_verbose(True)` prints the progress of the native code to
105
+ stderr. `patchmatch.inpaint_regularity(image, mask, ijmap)` additionally guides the
106
+ search with a regularity map, an HxWx3 float32 array with the regularity coordinates of
107
+ each pixel in its first two channels. Its `guide_weight` must be between 0 and the
108
+ largest float32, about 3.4e38.
107
109
 
108
110
  C++ (see
109
111
  [examples/cpp_example.cpp](https://github.com/mauwii/PyPatchMatch/blob/main/examples/cpp_example.cpp),
@@ -1,11 +1,12 @@
1
1
  # PatchMatch based Inpainting
2
2
 
3
- [![CI](https://github.com/mauwii/PyPatchMatch/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mauwii/PyPatchMatch/actions/workflows/ci.yml)
4
3
  [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=mauwii_PyPatchMatch&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=mauwii_PyPatchMatch)
5
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blueviolet.svg)](https://github.com/mauwii/PyPatchMatch/blob/main/LICENSE)
6
5
  [![PyPI](https://img.shields.io/pypi/v/PyPatchMatch)](https://pypi.org/project/PyPatchMatch/)
7
6
  [![Downloads](https://static.pepy.tech/badge/pypatchmatch)](https://pepy.tech/projects/pypatchmatch)
8
7
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
8
+ [![ponytail](https://img.shields.io/badge/over--engineering-ponytail_audited-111111)](https://github.com/DietrichGebert/ponytail)
9
+ [![caveman](https://img.shields.io/badge/agent-caveman-F0A63C)](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
@@ -69,11 +70,12 @@ The previous import path `from patchmatch import patch_match` keeps working.
69
70
  pixels in each direction. Larger patches follow larger structures but are much slower;
70
71
  the default of 15 compares 31x31 patches, the examples use 3.
71
72
 
72
- `patchmatch.set_random_seed(seed)` sets the seed of the randomized search and
73
- `patchmatch.set_verbose(True)` prints the progress of the native code to stderr.
74
- `patchmatch.inpaint_regularity(image, mask, ijmap)` additionally guides the search with
75
- a regularity map, an HxWx3 float32 array with the regularity coordinates of each pixel
76
- in its first two channels. Its `guide_weight` must be at least 0.
73
+ `patchmatch.set_random_seed(seed)` sets the seed of the randomized search, modulo
74
+ `2**32`, and `patchmatch.set_verbose(True)` prints the progress of the native code to
75
+ stderr. `patchmatch.inpaint_regularity(image, mask, ijmap)` additionally guides the
76
+ search with a regularity map, an HxWx3 float32 array with the regularity coordinates of
77
+ each pixel in its first two channels. Its `guide_weight` must be between 0 and the
78
+ largest float32, about 3.4e38.
77
79
 
78
80
  C++ (see
79
81
  [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.1'
22
- __version_tuple__ = version_tuple = (2, 0, 1)
21
+ __version__ = version = '2.1.0'
22
+ __version_tuple__ = version_tuple = (2, 1, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g995b76749'
24
+ __commit_id__ = commit_id = 'g2f558d40a'
@@ -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
- cast_pair(ys, xs, yt, xt, weight);
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
- if (level == nr_levels - 1)
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
- m_source2target = NearestNeighborField(source, target, m_distance_metric, m_source2target);
211
- m_target2source = NearestNeighborField(target, source, m_distance_metric, m_target2source);
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.set_target(new_target);
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
- _link_patches_without_holes(source);
429
+ _update_links(source, iter_em != 0);
261
430
  trace(verbose, " NNF minimization started.");
262
- m_source2target.minimize(nr_iters_nnf);
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
- _maximization_step(new_target, vote, new_source, new_level <= 1);
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
- void Inpainting::_link_patches_without_holes(const MaskedImage &source)
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
- continue;
321
- m_source2target.set_identity(i, j);
322
- m_target2source.set_identity(i, j);
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
- const VoteCaster caster(nnf, source2target, vote, source, upscaled, best_only);
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 _link_patches_without_holes(const MaskedImage &source);
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
  };