PyPatchMatch 2.1.0__tar.gz → 2.2.0b1__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.1.0 → pypatchmatch-2.2.0b1}/CLAUDE.md +12 -21
  2. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/CMakeLists.txt +2 -1
  3. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/PKG-INFO +5 -3
  4. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/README.md +3 -1
  5. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/_lib.py +3 -2
  6. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/_version.py +3 -3
  7. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/inpaint.cpp +138 -26
  8. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/inpaint.h +9 -0
  9. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/masked_image.cpp +21 -9
  10. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/masked_image.h +12 -10
  11. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/nnf.cpp +224 -126
  12. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/nnf.h +59 -28
  13. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/pyinterface.cpp +35 -42
  14. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/pyproject.toml +1 -1
  15. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/scripts/evaluate_inpainting.py +12 -11
  16. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/tests/test_e2e.py +27 -26
  17. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/tests/test_patch_match.py +67 -25
  18. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/LICENSE +0 -0
  19. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/SECURITY.md +0 -0
  20. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/examples/images/chelsea.png +0 -0
  21. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/examples/images/forest_pruned.bmp +0 -0
  22. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/__init__.py +0 -0
  23. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/pyinterface.h +0 -0
  24. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/patch_match.py +0 -0
  25. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/py.typed +0 -0
  26. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/scripts/build_opencv.py +0 -0
  27. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/scripts/render_readme_example.py +0 -0
  28. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/tests/conftest.py +0 -0
  29. {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/tests/test_properties.py +0 -0
@@ -1,10 +1,9 @@
1
1
  # CLAUDE.md
2
2
 
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in
4
- this repository. The details of the native code, the C interface, packaging and CI are
5
- path-scoped rules in `.claude/rules/`, which load when a matching file is read or
6
- edited; read the rule before changing such a file through Bash. The sanitizer, C++
7
- coverage and fill-quality runs are skills in `.claude/skills/`.
3
+ The details of the native code, the C interface, packaging and CI are path-scoped rules
4
+ in `.claude/rules/`, which load when a matching file is read or edited; read the rule
5
+ before changing such a file through Bash. The sanitizer, C++ coverage and fill-quality
6
+ runs are skills in `.claude/skills/`.
8
7
 
9
8
  ## Project
10
9
 
@@ -27,24 +26,18 @@ uv run pre-commit install
27
26
  uv run pytest # all tests, with the coverage gate
28
27
  uv run pytest -m "not e2e" # fast unit tests only
29
28
  uv run pytest -m e2e --no-cov # end-to-end tests only
30
- uv run pytest tests/test_patch_match.py::test_inpaint_global_mask --no-cov
31
- uv run pytest -k regularity --no-cov
32
29
  uv run mypy # strict, also a pre-commit hook
33
30
  uv run pre-commit run --all-files # what the CI lint job runs
34
- uv run ruff check --fix . && uv run ruff format .
35
31
  uv build # sdist and wheel into dist/
36
32
  ```
37
33
 
38
34
  - Pass `--no-cov` for any partial run: `addopts` always enables coverage, and
39
35
  `fail_under = 95` (branch coverage, subprocesses included through
40
36
  `patch = ["subprocess"]`) fails the run otherwise.
41
- - Use `uv run pytest`, not `python -m pytest`. The latter puts the repository root on
42
- `sys.path`, which imports the source tree `patchmatch/` without the compiled library;
43
- the symptoms are the warning `patchmatch failed to load: libpatchmatch.* not found`
44
- and `RuntimeError: the patchmatch native library is not available` in every test.
45
- - `--cov=patchmatch` measures the source tree, so coverage only works with the editable
46
- install that `uv sync` creates. With a non-editable install (`uv sync --no-editable`,
47
- `uv pip install .`) it reports 0 % and the gate fails.
37
+ - Keep the editable install that `uv sync` creates. With a non-editable one
38
+ (`uv sync --no-editable`, `uv pip install .`), `--cov=patchmatch` measures the unused
39
+ source tree and reports 0 %, and `python -m pytest` imports the source tree from the
40
+ repository root, without the compiled library.
48
41
  - `uv sync` rebuilds the library only when a `tool.uv.cache-keys` entry changes
49
42
  (`CMakeLists.txt`, `patchmatch/csrc/*`, `pyproject.toml`, git commit or tags). Force it
50
43
  with `uv sync --reinstall-package pypatchmatch`.
@@ -69,11 +62,8 @@ Other entry points:
69
62
  - `uv run python scripts/render_readme_example.py` runs `py_example.py` and renders the
70
63
  picture at the top of the README, `examples/images/readme_example.jpg`; rerun it and
71
64
  commit the picture after changes that affect the fills.
72
- - CMake options: `PATCHMATCH_BUILD_EXAMPLES` (OFF), `PATCHMATCH_FAST_MATH` (ON,
73
- `-ffast-math` or `/fp:fast`), `PATCHMATCH_SANITIZE` (OFF, ASan and UBSan with
74
- `float-cast-overflow`, every finding aborts; turns off fast math, which lets the
75
- compiler drop checks; GCC and Clang only), `PATCHMATCH_COVERAGE` (OFF, gcov counters
76
- at `-Og`; GCC and Clang only).
65
+ - CMake options: the `option()` lines of `CMakeLists.txt`. `PATCHMATCH_SANITIZE` turns
66
+ off fast math, which lets the compiler drop checks.
77
67
 
78
68
  ## Architecture
79
69
 
@@ -114,7 +104,8 @@ CMake installs the shared library into the package directory.
114
104
  goes through a test parameter instead of a `# type: ignore`.
115
105
  - Backwards compatibility: `from patchmatch import patch_match` and
116
106
  `patch_match.CShapeT`/`CMatT` keep working (tested).
117
- - Comments explain why: which crash a check prevents, why a construct is avoided.
107
+ - Comments explain why, in one short line where the code would otherwise be misread or
108
+ wrongly simplified; details and measurements go into the commit message.
118
109
  - No `NOSONAR` markers: change the code so that the SonarCloud rule is satisfied, and ask
119
110
  when that seems impossible. The only exception is `nnf.cpp`: cpp:S2245 flags every
120
111
  declaration of `std::mt19937`, which only drives the randomized search.
@@ -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.1.0
3
+ Version: 2.2.0b1
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>
@@ -43,7 +43,7 @@ and Python interfaces. This implementation is heavily based on the implementatio
43
43
  Younesse ANDAM: [younesse-cv/PatchMatch](https://github.com/younesse-cv/PatchMatch),
44
44
  with some bug fixes, and updates.
45
45
 
46
- ![Birdhouses removed from a photo by patchmatch.inpaint](https://raw.githubusercontent.com/mauwii/PyPatchMatch/v2.1.0/examples/images/readme_example.jpg)
46
+ ![Birdhouses removed from a photo by patchmatch.inpaint](https://raw.githubusercontent.com/mauwii/PyPatchMatch/v2.2.0b1/examples/images/readme_example.jpg)
47
47
 
48
48
  From left to right: the photo, the input of
49
49
  [examples/py_example.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example.py)
@@ -98,7 +98,9 @@ The previous import path `from patchmatch import patch_match` keeps working.
98
98
 
99
99
  `patch_size` is the radius of the compared patches, which span `2 * patch_size + 1`
100
100
  pixels in each direction. Larger patches follow larger structures but are much slower;
101
- the default of 15 compares 31x31 patches, the examples use 3.
101
+ the default of 15 compares 31x31 patches, the examples use 3. Below 5, the coarse levels
102
+ that lay out the fill are also tried with a radius of 5, which is kept where it fits
103
+ the surroundings better, so that repeating patterns continue across larger holes.
102
104
 
103
105
  `patchmatch.set_random_seed(seed)` sets the seed of the randomized search, modulo
104
106
  `2**32`, and `patchmatch.set_verbose(True)` prints the progress of the native code to
@@ -68,7 +68,9 @@ The previous import path `from patchmatch import patch_match` keeps working.
68
68
 
69
69
  `patch_size` is the radius of the compared patches, which span `2 * patch_size + 1`
70
70
  pixels in each direction. Larger patches follow larger structures but are much slower;
71
- the default of 15 compares 31x31 patches, the examples use 3.
71
+ the default of 15 compares 31x31 patches, the examples use 3. Below 5, the coarse levels
72
+ that lay out the fill are also tried with a radius of 5, which is kept where it fits
73
+ the surroundings better, so that repeating patterns continue across larger holes.
72
74
 
73
75
  `patchmatch.set_random_seed(seed)` sets the seed of the randomized search, modulo
74
76
  `2**32`, and `patchmatch.set_verbose(True)` prints the progress of the native code to
@@ -82,8 +82,9 @@ def find_library() -> Path:
82
82
  )
83
83
 
84
84
 
85
- def load_library() -> ctypes.CDLL:
86
- lib = ctypes.CDLL(str(find_library()))
85
+ def load_library(path: Path | None = None) -> ctypes.CDLL:
86
+ """Load the installed library, or the build at ``path``."""
87
+ lib = ctypes.CDLL(str(path or find_library()))
87
88
 
88
89
  lib.PM_set_random_seed.argtypes = [ctypes.c_uint]
89
90
  lib.PM_set_random_seed.restype = None
@@ -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.1.0'
22
- __version_tuple__ = version_tuple = (2, 1, 0)
21
+ __version__ = version = '2.2.0b1'
22
+ __version_tuple__ = version_tuple = (2, 2, 0, 'b1')
23
23
 
24
- __commit_id__ = commit_id = 'g2f558d40a'
24
+ __commit_id__ = commit_id = 'gb66a4a1d8'
@@ -58,6 +58,17 @@ namespace
58
58
  return count > 0 ? sum / count : sum;
59
59
  }
60
60
 
61
+ // The pixel loops read three channels and index the masks with the image coordinates.
62
+ void check_inputs(const MaskedImage &image)
63
+ {
64
+ if (image.image().empty())
65
+ return;
66
+ CV_Assert(image.image().type() == CV_8UC3);
67
+ CV_Assert(image.mask().type() == CV_8UC1 && image.mask().size() == image.size());
68
+ if (image.has_global_mask())
69
+ CV_Assert(image.global_mask().type() == CV_8UC1 && image.global_mask().size() == image.size());
70
+ }
71
+
61
72
  bool is_known(const MaskedImage &image, int y, int x)
62
73
  {
63
74
  return !image.is_masked(y, x) && !image.is_globally_masked(y, x);
@@ -68,6 +79,28 @@ namespace
68
79
  return image.is_masked(y, x) && !image.is_globally_masked(y, x);
69
80
  }
70
81
 
82
+ bool has_hole(const MaskedImage &image)
83
+ {
84
+ for (int y = 0; y < image.size().height; ++y)
85
+ {
86
+ for (int x = 0; x < image.size().width; ++x)
87
+ {
88
+ if (is_hole(image, y, x))
89
+ return true;
90
+ }
91
+ }
92
+ return false;
93
+ }
94
+
95
+ // A patch smaller than a structure cannot tell where it continues: on the coarsest level
96
+ // with a hole, holes wider than the patches were filled from several shifted copies.
97
+ constexpr int kCoarsePatchSize = 5;
98
+
99
+ int nnf_iterations(int level)
100
+ {
101
+ return std::min(7, 1 + level);
102
+ }
103
+
71
104
  // Chessboard distance of the hole pixels to the nearest known pixel, 0 beyond radius.
72
105
  cv::Mat hole_depths(const MaskedImage &image, int radius)
73
106
  {
@@ -328,6 +361,8 @@ Inpainting::Inpainting(
328
361
 
329
362
  void Inpainting::_initialize_pyramid()
330
363
  {
364
+ check_inputs(m_initial);
365
+
331
366
  // The colors under the holes would reach the fill through the gradients of their
332
367
  // neighbors and the target of a single-level pyramid. Black, like on coarser levels.
333
368
  auto source = m_initial.clone();
@@ -347,22 +382,62 @@ void Inpainting::_initialize_pyramid()
347
382
  source = source.downsample();
348
383
  m_pyramid.push_back(source);
349
384
  }
385
+
386
+ for (int level = 0; level < static_cast<int>(m_pyramid.size()); ++level)
387
+ {
388
+ if (has_hole(m_pyramid[level]))
389
+ m_coarsest_hole_level = level;
390
+ }
391
+ if (m_distance_metric->patch_size() < kCoarsePatchSize)
392
+ m_coarse_metric = m_distance_metric->with_patch_size(kCoarsePatchSize);
350
393
  }
351
394
 
352
395
  cv::Mat Inpainting::run(bool verbose, unsigned int random_seed)
353
396
  {
354
397
  NearestNeighborField::seed_random(random_seed);
355
- const auto nr_levels = static_cast<int>(m_pyramid.size());
356
398
 
357
- MaskedImage source;
399
+ int level = static_cast<int>(m_pyramid.size()) - 1;
358
400
  MaskedImage target;
359
- for (int level = nr_levels - 1; level >= 0; --level)
401
+ // Below level 2 no level is left to compare the fills on.
402
+ if (m_coarse_metric && m_coarsest_hole_level >= 2)
360
403
  {
361
- trace(verbose, "Inpainting level: ", level);
404
+ target = _run_levels(level, m_coarsest_hole_level + 1, target, m_distance_metric, verbose);
405
+ target = _fill_coarse_levels(target, verbose);
406
+ level = m_coarsest_hole_level - 2;
407
+ }
408
+ target = _run_levels(level, 0, target, m_distance_metric, verbose);
409
+
410
+ // Only the holes are filled. The maximization step of level 0 keeps the known pixels,
411
+ // but the pyramid leaves globally masked pixels black, which are neither filled nor
412
+ // used as a source. Copy every pixel outside the holes from the input.
413
+ // A plain loop instead of OpenCV matrix expressions: those pull OpenCV's whole
414
+ // expression module into the statically linked library, 40 % more on disk.
415
+ cv::Mat result = target.image();
416
+ for (int y = 0; y < result.rows; ++y)
417
+ {
418
+ for (int x = 0; x < result.cols; ++x)
419
+ {
420
+ if (m_initial.is_masked(y, x) && !m_initial.is_globally_masked(y, x))
421
+ continue;
422
+ const unsigned char *input = m_initial.get_image(y, x);
423
+ std::copy(input, input + 3, result.ptr<unsigned char>(y, x));
424
+ }
425
+ }
426
+ blend_hole_borders(result, m_initial);
427
+ return result;
428
+ }
362
429
 
363
- source = m_pyramid[level];
430
+ // Runs the levels from `from` down to `to`; target is empty on the coarsest level.
431
+ MaskedImage Inpainting::_run_levels(
432
+ int from, int to, MaskedImage target, const PatchDistanceMetric *metric, bool verbose)
433
+ {
434
+ m_level_metric = metric;
435
+ for (int level = from; level >= to; --level)
436
+ {
437
+ trace(verbose, "Inpainting level: ", level);
364
438
 
365
- const bool coarsest = level == nr_levels - 1;
439
+ const MaskedImage &source = m_pyramid[level];
440
+ const bool coarsest = level == static_cast<int>(m_pyramid.size()) - 1;
366
441
  if (coarsest)
367
442
  {
368
443
  target = source.clone();
@@ -373,34 +448,70 @@ cv::Mat Inpainting::run(bool verbose, unsigned int random_seed)
373
448
  if (level < 2)
374
449
  m_source2target.reset();
375
450
  else
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);
451
+ m_source2target = coarsest ? NearestNeighborField(source, target, metric)
452
+ : NearestNeighborField(source, target, metric, *m_source2target);
453
+ m_target2source = coarsest ? NearestNeighborField(target, source, metric)
454
+ : NearestNeighborField(target, source, metric, m_target2source);
380
455
 
381
456
  trace(verbose, "Initialization done.");
382
457
 
383
458
  target = _expectation_maximization(source, target, level, verbose);
384
459
  }
460
+ return target;
461
+ }
385
462
 
386
- // Only the holes are filled. The maximization step of level 0 keeps the known pixels,
387
- // but the pyramid leaves globally masked pixels black, which are neither filled nor
388
- // used as a source. Copy every pixel outside the holes from the input.
389
- // A plain loop instead of OpenCV matrix expressions: those pull OpenCV's whole
390
- // expression module into the statically linked library, 40 % more on disk.
391
- cv::Mat result = target.image();
392
- for (int y = 0; y < result.rows; ++y)
463
+ // Fills the coarsest level with a hole and the next one with patch_size and with the
464
+ // larger patches, and keeps the fill with the lower energy on the following level. The
465
+ // larger patches lock onto repeating structures but pick flat sources elsewhere, which
466
+ // made fills of foliage gray.
467
+ MaskedImage Inpainting::_fill_coarse_levels(const MaskedImage &target, bool verbose)
468
+ {
469
+ const int level = m_coarsest_hole_level;
470
+ const auto source2target = m_source2target;
471
+ const auto target2source = m_target2source;
472
+
473
+ const MaskedImage plain = _run_levels(level, level - 1, target, m_distance_metric, verbose);
474
+ const double plain_energy = _hole_energy(level - 2, plain);
475
+ const auto plain_source2target = m_source2target;
476
+ const auto plain_target2source = m_target2source;
477
+
478
+ m_source2target = source2target;
479
+ m_target2source = target2source;
480
+ MaskedImage coarse = _run_levels(level, level - 1, target, m_coarse_metric.get(), verbose);
481
+ const double coarse_energy = _hole_energy(level - 2, coarse);
482
+ trace(verbose, "Energy with patch_size: ", plain_energy, ", with larger patches: ", coarse_energy);
483
+ m_level_metric = m_distance_metric;
484
+ if (coarse_energy < plain_energy)
485
+ return coarse;
486
+ m_source2target = plain_source2target;
487
+ m_target2source = plain_target2source;
488
+ return plain;
489
+ }
490
+
491
+ // The mean distance of the patches with holes after a search with patch_size on the level,
492
+ // started from the field of the level above, like the first iteration of that level.
493
+ double Inpainting::_hole_energy(int level, const MaskedImage &target) const
494
+ {
495
+ const MaskedImage &source = m_pyramid[level];
496
+ NearestNeighborField field(target, source, m_distance_metric, m_target2source);
497
+ field.minimize(nnf_iterations(level));
498
+
499
+ const int patch_size = m_distance_metric->patch_size();
500
+ const auto size = source.size();
501
+ double sum = 0;
502
+ int count = 0;
503
+ for (int i = 0; i < size.height; ++i)
393
504
  {
394
- for (int x = 0; x < result.cols; ++x)
505
+ for (int j = 0; j < size.width; ++j)
395
506
  {
396
- if (m_initial.is_masked(y, x) && !m_initial.is_globally_masked(y, x))
397
- continue;
398
- const unsigned char *input = m_initial.get_image(y, x);
399
- std::copy(input, input + 3, result.ptr<unsigned char>(y, x));
507
+ if (source.contains_mask(i, j, patch_size) && !source.is_globally_masked(i, j))
508
+ {
509
+ sum += field.at(i, j, 2);
510
+ ++count;
511
+ }
400
512
  }
401
513
  }
402
- blend_hole_borders(result, m_initial);
403
- return result;
514
+ return count > 0 ? sum / count : 0;
404
515
  }
405
516
 
406
517
  // EM-Like algorithm (see "PatchMatch" - page 6).
@@ -409,7 +520,7 @@ MaskedImage Inpainting::_expectation_maximization(
409
520
  const MaskedImage &source, MaskedImage target, int level, bool verbose)
410
521
  {
411
522
  const int nr_iters_em = 1 + 2 * level;
412
- const int nr_iters_nnf = std::min(7, 1 + level);
523
+ const int nr_iters_nnf = nnf_iterations(level);
413
524
 
414
525
  MaskedImage new_source;
415
526
  MaskedImage new_target;
@@ -491,7 +602,7 @@ void Inpainting::_update_links(const MaskedImage &source, bool image_changed)
491
602
  update(*m_source2target);
492
603
  update(m_target2source);
493
604
  };
494
- const int patch_size = m_distance_metric->patch_size();
605
+ const int patch_size = m_level_metric->patch_size();
495
606
  const auto size = source.size();
496
607
  for (int i = 0; i < size.height; ++i)
497
608
  {
@@ -516,6 +627,7 @@ void Inpainting::_expectation_step(
516
627
  holes.emplace(source);
517
628
  const VoteCaster caster(
518
629
  nnf, source2target, vote, source, upscaled, best_only, holes.has_value() ? &holes.value() : nullptr);
630
+ // The larger patches of m_coarse_metric would average more votes, which blurred the fills.
519
631
  const int patch_size = m_distance_metric->patch_size();
520
632
  const auto &kDistance2Similarity = distance2similarity();
521
633
  const auto source_size = nnf.source_size();
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
 
3
+ #include <memory>
3
4
  #include <optional>
4
5
  #include <vector>
5
6
 
@@ -16,6 +17,9 @@ public:
16
17
 
17
18
  private:
18
19
  void _initialize_pyramid(void);
20
+ MaskedImage _run_levels(int from, int to, MaskedImage target, const PatchDistanceMetric *metric, bool verbose);
21
+ MaskedImage _fill_coarse_levels(const MaskedImage &target, bool verbose);
22
+ double _hole_energy(int level, const MaskedImage &target) const;
19
23
  MaskedImage _expectation_maximization(const MaskedImage &source, MaskedImage target, int level, bool verbose);
20
24
  void _update_links(const MaskedImage &source, bool image_changed);
21
25
  void _expectation_step(
@@ -29,4 +33,9 @@ private:
29
33
  std::optional<NearestNeighborField> m_source2target;
30
34
  NearestNeighborField m_target2source;
31
35
  const PatchDistanceMetric *m_distance_metric;
36
+ // Searches on the levels that lay out the fill, if patch_size is smaller than kCoarsePatchSize.
37
+ std::unique_ptr<PatchDistanceMetric> m_coarse_metric;
38
+ int m_coarsest_hole_level = -1;
39
+ // Searches on the current level; the votes use patch_size.
40
+ const PatchDistanceMetric *m_level_metric = nullptr;
32
41
  };
@@ -52,19 +52,31 @@ namespace
52
52
 
53
53
  bool MaskedImage::contains_mask(int y, int x, int patch_size) const
54
54
  {
55
- auto mask_size = size();
56
- for (int dy = -patch_size; dy <= patch_size; ++dy)
55
+ const auto size = this->size();
56
+ if (!m_hole_sums_computed)
57
57
  {
58
- for (int dx = -patch_size; dx <= patch_size; ++dx)
58
+ m_hole_sums = cv::Mat(size.height + 1, size.width + 1, CV_32S, cv::Scalar(0));
59
+ for (int yy = 0; yy < size.height; ++yy)
59
60
  {
60
- const int yy = y + dy;
61
- const int xx = x + dx;
62
- if (yy >= 0 && yy < mask_size.height && xx >= 0 && xx < mask_size.width && is_masked(yy, xx) &&
63
- !is_globally_masked(yy, xx))
64
- return true;
61
+ for (int xx = 0; xx < size.width; ++xx)
62
+ {
63
+ const int hole = is_masked(yy, xx) && !is_globally_masked(yy, xx) ? 1 : 0;
64
+ m_hole_sums.at<int>(yy + 1, xx + 1) = hole + m_hole_sums.at<int>(yy, xx + 1) +
65
+ m_hole_sums.at<int>(yy + 1, xx) - m_hole_sums.at<int>(yy, xx);
66
+ }
65
67
  }
68
+ m_hole_sums_computed = true;
66
69
  }
67
- return false;
70
+ // In 64 bits: patch_size may be close to the largest int.
71
+ const auto clip = [](std::int64_t value, int limit) {
72
+ return static_cast<int>(std::clamp<std::int64_t>(value, 0, limit));
73
+ };
74
+ const int top = clip(std::int64_t{y} - patch_size, size.height);
75
+ const int bottom = clip(std::int64_t{y} + patch_size + 1, size.height);
76
+ const int left = clip(std::int64_t{x} - patch_size, size.width);
77
+ const int right = clip(std::int64_t{x} + patch_size + 1, size.width);
78
+ const auto sum = [this](int yy, int xx) { return m_hole_sums.at<int>(yy, xx); };
79
+ return sum(bottom, right) - sum(top, right) - sum(bottom, left) + sum(top, left) > 0;
68
80
  }
69
81
 
70
82
  MaskedImage MaskedImage::downsample() const
@@ -12,13 +12,6 @@ public:
12
12
  : m_image(image), m_mask(mask), m_global_mask(global_mask), m_has_global_mask(!global_mask.empty())
13
13
  {
14
14
  }
15
- MaskedImage(
16
- const cv::Mat &image, const cv::Mat &mask, const cv::Mat &global_mask, const cv::Mat &grady,
17
- const cv::Mat &gradx, bool grad_computed)
18
- : m_image(image), m_mask(mask), m_global_mask(global_mask), m_has_global_mask(!global_mask.empty()),
19
- m_image_grady(grady), m_image_gradx(gradx), m_image_grad_computed(grad_computed)
20
- {
21
- }
22
15
  MaskedImage(int width, int height)
23
16
  : m_image(cv::Size(width, height), CV_8UC3, cv::Scalar::all(0)),
24
17
  m_mask(cv::Size(width, height), CV_8U, cv::Scalar::all(0))
@@ -31,11 +24,10 @@ public:
31
24
  MaskedImage &operator=(MaskedImage &&) noexcept = default;
32
25
  ~MaskedImage() = default;
33
26
 
27
+ // Without the gradients, which would be outdated once the clone is changed.
34
28
  MaskedImage clone() const
35
29
  {
36
- return MaskedImage(
37
- m_image.clone(), m_mask.clone(), m_global_mask.clone(), m_image_grady.clone(), m_image_gradx.clone(),
38
- m_image_grad_computed);
30
+ return MaskedImage(m_image.clone(), m_mask.clone(), m_global_mask.clone());
39
31
  }
40
32
 
41
33
  cv::Size size() const
@@ -74,11 +66,13 @@ public:
74
66
  m_global_mask = cv::Mat(m_mask.size(), CV_8U);
75
67
  m_global_mask.setTo(cv::Scalar(0));
76
68
  m_has_global_mask = !m_global_mask.empty();
69
+ m_hole_sums_computed = false;
77
70
  }
78
71
  void set_global_mask_mat(const cv::Mat &other)
79
72
  {
80
73
  m_global_mask = other;
81
74
  m_has_global_mask = !other.empty();
75
+ m_hole_sums_computed = false;
82
76
  }
83
77
 
84
78
  bool is_masked(int y, int x) const
@@ -92,14 +86,17 @@ public:
92
86
  void set_mask(int y, int x, bool value)
93
87
  {
94
88
  m_mask.at<unsigned char>(y, x) = static_cast<unsigned char>(value);
89
+ m_hole_sums_computed = false;
95
90
  }
96
91
  void set_global_mask(int y, int x, bool value)
97
92
  {
98
93
  m_global_mask.at<unsigned char>(y, x) = static_cast<unsigned char>(value);
94
+ m_hole_sums_computed = false;
99
95
  }
100
96
  void clear_mask()
101
97
  {
102
98
  m_mask.setTo(cv::Scalar(0));
99
+ m_hole_sums_computed = false;
103
100
  }
104
101
 
105
102
  const unsigned char *get_image(int y, int x) const
@@ -127,4 +124,9 @@ private:
127
124
  mutable cv::Mat m_image_grady;
128
125
  mutable cv::Mat m_image_gradx;
129
126
  mutable bool m_image_grad_computed = false;
127
+ // computed on first use by contains_mask: the masked pixels outside the global mask
128
+ // above and left of each pixel. Copies share them with the masks, so change a mask only
129
+ // through an image that nothing has copied yet, as downsample and upsample do.
130
+ mutable cv::Mat m_hole_sums;
131
+ mutable bool m_hole_sums_computed = false;
130
132
  };