PyPatchMatch 2.0.2__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.2 → pypatchmatch-2.1.0}/CLAUDE.md +14 -29
  2. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/PKG-INFO +4 -3
  3. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/README.md +2 -1
  4. pypatchmatch-2.1.0/examples/images/chelsea.png +0 -0
  5. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/_version.py +3 -3
  6. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/csrc/inpaint.cpp +215 -33
  7. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/csrc/inpaint.h +4 -3
  8. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/csrc/masked_image.cpp +31 -27
  9. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/csrc/masked_image.h +28 -24
  10. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/csrc/nnf.cpp +64 -51
  11. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/csrc/nnf.h +41 -23
  12. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/csrc/pyinterface.cpp +18 -27
  13. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/patch_match.py +2 -2
  14. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/pyproject.toml +6 -2
  15. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/scripts/build_opencv.py +3 -6
  16. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/scripts/evaluate_inpainting.py +48 -8
  17. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/tests/test_e2e.py +39 -35
  18. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/tests/test_patch_match.py +24 -0
  19. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/CMakeLists.txt +0 -0
  20. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/LICENSE +0 -0
  21. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/SECURITY.md +0 -0
  22. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/examples/images/forest_pruned.bmp +0 -0
  23. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/__init__.py +0 -0
  24. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/_lib.py +0 -0
  25. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/csrc/pyinterface.h +0 -0
  26. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/patchmatch/py.typed +0 -0
  27. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/scripts/render_readme_example.py +0 -0
  28. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/tests/conftest.py +0 -0
  29. {pypatchmatch-2.0.2 → pypatchmatch-2.1.0}/tests/test_properties.py +0 -0
@@ -77,35 +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), returns a copy of the image without a native call if `_has_holes` finds no
88
- hole outside the global mask, picks the entry point (`PM_inpaint`, `PM_inpaint2`
89
- with a global mask, and their `_regularity` variants) and runs it through `_call`.
90
- It loads the library at import time; on `OSError` (no library) or `AttributeError`
91
- (one without the `PM_*` symbols) it logs a warning and sets
92
- `patchmatch_available = False`, and every function then raises `RuntimeError`
93
- through `_get_lib()`.
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPatchMatch
3
- Version: 2.0.2
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.2/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)
@@ -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
@@ -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.2'
22
- __version_tuple__ = version_tuple = (2, 0, 2)
21
+ __version__ = version = '2.1.0'
22
+ __version_tuple__ = version_tuple = (2, 1, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g38b611eb9'
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
  };
@@ -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
- const auto &kernel_size = MaskedImage::kDownsampleKernelSize;
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 = -kernel_size.height / 2 + 1; dy <= kernel_size.height / 2; ++dy)
27
+ for (int dy = -half + 1; dy <= half; ++dy)
29
28
  {
30
- for (int dx = -kernel_size.width / 2 + 1; dx <= kernel_size.width / 2; ++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 = kernel[kernel_size.height / 2 - 1 + dy] * kernel[kernel_size.width / 2 - 1 + dx];
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 (!m_global_mask.empty())
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 (!m_global_mask.empty())
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 (!m_global_mask.empty())
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 (!m_global_mask.empty())
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
- for (int i = 1; i < size.height - 1; ++i)
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 auto *ptry1 = m_image.ptr<unsigned char>(i + 1, 0);
160
- const auto *ptry2 = m_image.ptr<unsigned char>(i - 1, 0);
161
- const auto *ptrx1 = m_image.ptr<unsigned char>(i, 0) + 3;
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 int x = j / 3;
169
- const bool skip_y = is_globally_masked(i + 1, x) || is_globally_masked(i - 1, x);
170
- const bool skip_x = is_globally_masked(i, x + 1) || is_globally_masked(i, x - 1);
171
- mptry[j] = skip_y ? 128 : (ptry1[j] / 2 - ptry2[j] / 2) + 128;
172
- mptrx[j] = skip_x ? 128 : (ptrx1[j] / 2 - ptrx2[j] / 2) + 128;
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