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.
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/CLAUDE.md +12 -21
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/CMakeLists.txt +2 -1
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/PKG-INFO +5 -3
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/README.md +3 -1
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/_lib.py +3 -2
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/_version.py +3 -3
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/inpaint.cpp +138 -26
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/inpaint.h +9 -0
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/masked_image.cpp +21 -9
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/masked_image.h +12 -10
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/nnf.cpp +224 -126
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/nnf.h +59 -28
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/pyinterface.cpp +35 -42
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/pyproject.toml +1 -1
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/scripts/evaluate_inpainting.py +12 -11
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/tests/test_e2e.py +27 -26
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/tests/test_patch_match.py +67 -25
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/LICENSE +0 -0
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/SECURITY.md +0 -0
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/examples/images/chelsea.png +0 -0
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/examples/images/forest_pruned.bmp +0 -0
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/__init__.py +0 -0
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/csrc/pyinterface.h +0 -0
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/patch_match.py +0 -0
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/patchmatch/py.typed +0 -0
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/scripts/build_opencv.py +0 -0
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/scripts/render_readme_example.py +0 -0
- {pypatchmatch-2.1.0 → pypatchmatch-2.2.0b1}/tests/conftest.py +0 -0
- {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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
-
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
|
|
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: `
|
|
73
|
-
|
|
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
|
|
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.
|
|
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
|
-

|
|
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
|
-
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.2.0b1'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 2, 0, 'b1')
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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
|
-
|
|
399
|
+
int level = static_cast<int>(m_pyramid.size()) - 1;
|
|
358
400
|
MaskedImage target;
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
377
|
-
: NearestNeighborField(source, target,
|
|
378
|
-
m_target2source = coarsest ? NearestNeighborField(target, source,
|
|
379
|
-
: NearestNeighborField(target, source,
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
505
|
+
for (int j = 0; j < size.width; ++j)
|
|
395
506
|
{
|
|
396
|
-
if (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
56
|
-
|
|
55
|
+
const auto size = this->size();
|
|
56
|
+
if (!m_hole_sums_computed)
|
|
57
57
|
{
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
};
|