PyPatchMatch 2.0.0b1__tar.gz → 2.0.0b2__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 (23) hide show
  1. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/CMakeLists.txt +13 -0
  2. pypatchmatch-2.0.0b2/PKG-INFO +140 -0
  3. pypatchmatch-2.0.0b1/PKG-INFO → pypatchmatch-2.0.0b2/README.md +31 -39
  4. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/_version.py +3 -3
  5. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/csrc/inpaint.cpp +20 -12
  6. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/csrc/masked_image.h +1 -0
  7. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/csrc/nnf.cpp +33 -7
  8. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/csrc/nnf.h +5 -0
  9. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/csrc/pyinterface.cpp +1 -2
  10. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/patch_match.py +14 -5
  11. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/pyproject.toml +9 -0
  12. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/scripts/build_opencv.py +15 -3
  13. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/tests/test_e2e.py +37 -8
  14. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/tests/test_patch_match.py +30 -4
  15. pypatchmatch-2.0.0b1/README.md +0 -96
  16. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/LICENSE +0 -0
  17. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/examples/images/forest_pruned.bmp +0 -0
  18. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/__init__.py +0 -0
  19. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/_lib.py +0 -0
  20. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/csrc/inpaint.h +0 -0
  21. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/csrc/masked_image.cpp +0 -0
  22. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/csrc/pyinterface.h +0 -0
  23. {pypatchmatch-2.0.0b1 → pypatchmatch-2.0.0b2}/patchmatch/py.typed +0 -0
@@ -61,3 +61,16 @@ install(TARGETS patchmatch
61
61
  LIBRARY DESTINATION patchmatch
62
62
  RUNTIME DESTINATION patchmatch
63
63
  )
64
+
65
+ # A static OpenCV becomes part of the library, so a wheel has to ship its licenses,
66
+ # which scripts/build_opencv.py collects in <prefix>/licenses.
67
+ if(DEFINED SKBUILD_METADATA_DIR AND NOT OpenCV_SHARED)
68
+ set(PATCHMATCH_OPENCV_LICENSES "${OpenCV_INSTALL_PATH}/licenses")
69
+ if(EXISTS "${PATCHMATCH_OPENCV_LICENSES}/opencv-LICENSE")
70
+ install(DIRECTORY "${PATCHMATCH_OPENCV_LICENSES}/"
71
+ DESTINATION "${SKBUILD_METADATA_DIR}/licenses/opencv")
72
+ else()
73
+ message(WARNING "No OpenCV licenses in ${PATCHMATCH_OPENCV_LICENSES}, "
74
+ "the wheel will not include them")
75
+ endif()
76
+ endif()
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyPatchMatch
3
+ Version: 2.0.0b2
4
+ Summary: This library implements the PatchMatch based inpainting algorithm.
5
+ Keywords: inpainting,patchmatch,image-processing,opencv
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>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Operating System :: MacOS
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: C++
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Multimedia :: Graphics
21
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
22
+ Classifier: Typing :: Typed
23
+ Project-URL: Source Code, https://github.com/mauwii/PyPatchMatch
24
+ Project-URL: Issues, https://github.com/mauwii/PyPatchMatch/issues
25
+ Project-URL: Release Notes, https://github.com/mauwii/PyPatchMatch/releases
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: numpy
28
+ Requires-Dist: pillow
29
+ Description-Content-Type: text/markdown
30
+
31
+ # PatchMatch based Inpainting
32
+
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blueviolet.svg)](https://github.com/mauwii/PyPatchMatch/blob/main/LICENSE)
34
+ [![PyPI](https://img.shields.io/pypi/v/PyPatchMatch)](https://pypi.org/project/PyPatchMatch/)
35
+ [![Downloads](https://static.pepy.tech/badge/pypatchmatch)](https://pepy.tech/projects/pypatchmatch)
36
+ [![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)
37
+
38
+ This library implements the PatchMatch based inpainting algorithm. It provides both C++
39
+ and Python interfaces. This implementation is heavily based on the implementation by
40
+ Younesse ANDAM: [younesse-cv/PatchMatch](https://github.com/younesse-cv/PatchMatch),
41
+ with some bug fixes, and updates.
42
+
43
+ ## Installation
44
+
45
+ ```sh
46
+ pip install PyPatchMatch
47
+ ```
48
+
49
+ Wheels for Linux (glibc, x86_64 and aarch64), macOS 11+ (arm64 and x86_64) and Windows
50
+ (x64) ship the compiled library with a statically linked OpenCV, so no compiler or
51
+ OpenCV installation is needed. On other platforms, e.g. Alpine Linux or Windows on ARM,
52
+ pip builds from the source distribution, which requires a C++17 compiler, CMake and the
53
+ OpenCV development files (e.g. `apt install libopencv-dev` or `brew install opencv`).
54
+
55
+ ## Usage
56
+
57
+ Python (see
58
+ [examples/py_example.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example.py)):
59
+
60
+ ```python
61
+ import patchmatch
62
+
63
+ image = ... # HxWx3 uint8 numpy array or PIL image
64
+ mask = ... # HxW uint8 or bool numpy array or PIL image, non-zero marks the holes
65
+ result = patchmatch.inpaint(image, mask, patch_size=3)
66
+ ```
67
+
68
+ The mask must have the same height and width as the image. If `mask` is omitted, all
69
+ pure white pixels are treated as holes. The optional keyword argument `global_mask`, in
70
+ the same format, marks pixels that are neither filled nor used as a source; they keep
71
+ their values (see
72
+ [examples/py_example_global_mask.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example_global_mask.py)).
73
+ `patchmatch.patchmatch_available` tells whether the native library could be loaded.
74
+ The previous import path `from patchmatch import patch_match` keeps working.
75
+
76
+ `patchmatch.set_random_seed(seed)` sets the seed of the randomized search and
77
+ `patchmatch.set_verbose(True)` prints the progress of the native code to stderr.
78
+ `patchmatch.inpaint_regularity(image, mask, ijmap)` additionally guides the search with
79
+ a regularity map, an HxWx3 float32 array with the regularity coordinates of each pixel.
80
+
81
+ C++ (see
82
+ [examples/cpp_example.cpp](https://github.com/mauwii/PyPatchMatch/blob/main/examples/cpp_example.cpp),
83
+ build and run it with
84
+ [examples/cpp_example_run.sh](https://github.com/mauwii/PyPatchMatch/blob/main/examples/cpp_example_run.sh)):
85
+
86
+ ```cpp
87
+ #include "inpaint.h"
88
+
89
+ int main() {
90
+ cv::Mat image = ...;
91
+ cv::Mat mask = ...;
92
+
93
+ auto metric = PatchSSDDistanceMetric(5);
94
+ cv::Mat result = Inpainting(image, mask, &metric).run();
95
+ }
96
+ ```
97
+
98
+ The library is built with CMake; `PATCHMATCH_BUILD_EXAMPLES` builds the example and
99
+ `PATCHMATCH_WITH_HIGHGUI` enables the debug visualization of `Inpainting::run`.
100
+
101
+ ## Development
102
+
103
+ The project is managed with [uv](https://docs.astral.sh/uv/):
104
+
105
+ ```sh
106
+ uv sync # create .venv, build the library, install dev deps
107
+ uv run pre-commit install # enable ruff and the other hooks on commit
108
+ uv run pytest # run the test suite
109
+ uv run pytest -m "not e2e" # only the fast unit tests
110
+ ```
111
+
112
+ Releases are published to PyPI by creating a GitHub release; the version is taken from
113
+ its tag (e.g. `v2.0.0`).
114
+
115
+ ## License
116
+
117
+ PyPatchMatch is released under the
118
+ [MIT License](https://github.com/mauwii/PyPatchMatch/blob/main/LICENSE). The wheels
119
+ contain a statically linked build of OpenCV core (Apache-2.0) and its bundled
120
+ third-party code; their licenses are included in the `licenses/opencv` directory of the
121
+ wheel's `.dist-info`.
122
+
123
+ ## README and COPYRIGHT by Younesse ANDAM
124
+
125
+ @Author: Younesse ANDAM
126
+
127
+ @Contact: <younesse.andam@gmail.com>
128
+
129
+ Description:
130
+
131
+ This project is a personal implementation of an algorithm called PATCHMATCH
132
+ that restores missing areas in an image. The algorithm is presented in the following
133
+ paper PatchMatch A Randomized Correspondence Algorithm for Structural Image Editing by
134
+ C.Barnes, E.Shechtman, A.Finkelstein and Dan B.Goldman ACM Transactions on Graphics
135
+ (Proc. SIGGRAPH), vol.28, aug-2009
136
+
137
+ For more information please refer to
138
+ <https://gfx.cs.princeton.edu/pubs/Barnes_2009_PAR/>
139
+
140
+ Copyright (c) 2010-2011
@@ -1,30 +1,8 @@
1
- Metadata-Version: 2.4
2
- Name: PyPatchMatch
3
- Version: 2.0.0b1
4
- Summary: This library implements the PatchMatch based inpainting algorithm.
5
- Author-Email: The InvokeAI Project <lincoln.stein@gmail.com>, Kyle Schouviller <kyle0654@hotmail.com>, Matthias Wild <mauwii@outlook.de>, Younesse ANDAM <younesse.andam@gmail.com>
6
- License-Expression: MIT
7
- License-File: LICENSE
8
- Classifier: Programming Language :: C++
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.10
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
13
- Classifier: Programming Language :: Python :: 3.13
14
- Classifier: Programming Language :: Python :: 3.14
15
- Classifier: Topic :: Multimedia :: Graphics
16
- Classifier: Topic :: Scientific/Engineering :: Image Processing
17
- Project-URL: Source Code, https://github.com/mauwii/PyPatchMatch
18
- Requires-Python: >=3.10
19
- Requires-Dist: numpy
20
- Requires-Dist: pillow
21
- Description-Content-Type: text/markdown
22
-
23
1
  # PatchMatch based Inpainting
24
2
 
25
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blueviolet.svg)](https://github.com/mauwii/PyPatchMatch/blob/main/LICENSE)
26
4
  [![PyPI](https://img.shields.io/pypi/v/PyPatchMatch)](https://pypi.org/project/PyPatchMatch/)
27
- [![Downloads](https://static.pepy.tech/badge/pypatchmatch)](https://pepy.tech/project/pypatchmatch)
5
+ [![Downloads](https://static.pepy.tech/badge/pypatchmatch)](https://pepy.tech/projects/pypatchmatch)
28
6
  [![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)
29
7
 
30
8
  This library implements the PatchMatch based inpainting algorithm. It provides both C++
@@ -38,14 +16,16 @@ with some bug fixes, and updates.
38
16
  pip install PyPatchMatch
39
17
  ```
40
18
 
41
- Wheels for Linux, macOS and Windows ship the compiled library including the required
42
- OpenCV runtime, so no compiler or OpenCV installation is needed. On other platforms pip
43
- builds from the source distribution, which requires a C++17 compiler, CMake and the
19
+ Wheels for Linux (glibc, x86_64 and aarch64), macOS 11+ (arm64 and x86_64) and Windows
20
+ (x64) ship the compiled library with a statically linked OpenCV, so no compiler or
21
+ OpenCV installation is needed. On other platforms, e.g. Alpine Linux or Windows on ARM,
22
+ pip builds from the source distribution, which requires a C++17 compiler, CMake and the
44
23
  OpenCV development files (e.g. `apt install libopencv-dev` or `brew install opencv`).
45
24
 
46
25
  ## Usage
47
26
 
48
- Python (see [examples/py_example.py](examples/py_example.py)):
27
+ Python (see
28
+ [examples/py_example.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example.py)):
49
29
 
50
30
  ```python
51
31
  import patchmatch
@@ -56,12 +36,22 @@ result = patchmatch.inpaint(image, mask, patch_size=3)
56
36
  ```
57
37
 
58
38
  The mask must have the same height and width as the image. If `mask` is omitted, all
59
- pure white pixels are treated as holes.
39
+ pure white pixels are treated as holes. The optional keyword argument `global_mask`, in
40
+ the same format, marks pixels that are neither filled nor used as a source; they keep
41
+ their values (see
42
+ [examples/py_example_global_mask.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example_global_mask.py)).
60
43
  `patchmatch.patchmatch_available` tells whether the native library could be loaded.
61
44
  The previous import path `from patchmatch import patch_match` keeps working.
62
45
 
63
- C++ (see [examples/cpp_example.cpp](examples/cpp_example.cpp), build and run it with
64
- [examples/cpp_example_run.sh](examples/cpp_example_run.sh)):
46
+ `patchmatch.set_random_seed(seed)` sets the seed of the randomized search and
47
+ `patchmatch.set_verbose(True)` prints the progress of the native code to stderr.
48
+ `patchmatch.inpaint_regularity(image, mask, ijmap)` additionally guides the search with
49
+ a regularity map, an HxWx3 float32 array with the regularity coordinates of each pixel.
50
+
51
+ C++ (see
52
+ [examples/cpp_example.cpp](https://github.com/mauwii/PyPatchMatch/blob/main/examples/cpp_example.cpp),
53
+ build and run it with
54
+ [examples/cpp_example_run.sh](https://github.com/mauwii/PyPatchMatch/blob/main/examples/cpp_example_run.sh)):
65
55
 
66
56
  ```cpp
67
57
  #include "inpaint.h"
@@ -90,13 +80,21 @@ uv run pytest -m "not e2e" # only the fast unit tests
90
80
  ```
91
81
 
92
82
  Releases are published to PyPI by creating a GitHub release; the version is taken from
93
- its tag (e.g. `v1.1.0`).
83
+ its tag (e.g. `v2.0.0`).
84
+
85
+ ## License
86
+
87
+ PyPatchMatch is released under the
88
+ [MIT License](https://github.com/mauwii/PyPatchMatch/blob/main/LICENSE). The wheels
89
+ contain a statically linked build of OpenCV core (Apache-2.0) and its bundled
90
+ third-party code; their licenses are included in the `licenses/opencv` directory of the
91
+ wheel's `.dist-info`.
94
92
 
95
93
  ## README and COPYRIGHT by Younesse ANDAM
96
94
 
97
95
  @Author: Younesse ANDAM
98
96
 
99
- @Contact: younesse.andam@gmail.com
97
+ @Contact: <younesse.andam@gmail.com>
100
98
 
101
99
  Description:
102
100
 
@@ -107,12 +105,6 @@ C.Barnes, E.Shechtman, A.Finkelstein and Dan B.Goldman ACM Transactions on Graph
107
105
  (Proc. SIGGRAPH), vol.28, aug-2009
108
106
 
109
107
  For more information please refer to
110
- http://www.cs.princeton.edu/gfx/pubs/Barnes_2009_PAR/index.php
108
+ <https://gfx.cs.princeton.edu/pubs/Barnes_2009_PAR/>
111
109
 
112
110
  Copyright (c) 2010-2011
113
-
114
- ## Requirements
115
-
116
- To run the project you need to install Opencv library and link it to your project.
117
- Opencv can be download it here
118
- http://opencv.org/downloads.html
@@ -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.0b1'
22
- __version_tuple__ = version_tuple = (2, 0, 0, 'b1')
21
+ __version__ = version = '2.0.0b2'
22
+ __version_tuple__ = version_tuple = (2, 0, 0, 'b2')
23
23
 
24
- __commit_id__ = commit_id = 'g987b0e524'
24
+ __commit_id__ = commit_id = 'gd8a13321b'
@@ -9,13 +9,11 @@
9
9
 
10
10
  namespace
11
11
  {
12
- static std::vector<double> kDistance2Similarity;
13
-
14
- void init_kDistance2Similarity()
12
+ std::vector<double> make_distance2similarity()
15
13
  {
16
14
  double base[11] = {1.0, 0.99, 0.96, 0.83, 0.38, 0.11, 0.02, 0.005, 0.0006, 0.0001, 0};
17
15
  int length = (PatchDistanceMetric::kDistanceScale + 1);
18
- kDistance2Similarity.resize(length);
16
+ std::vector<double> table(length);
19
17
  for (int i = 0; i < length; ++i)
20
18
  {
21
19
  double t = (double)i / length;
@@ -23,8 +21,17 @@ namespace
23
21
  int k = j + 1;
24
22
  double vj = (j < 11) ? base[j] : 0;
25
23
  double vk = (k < 11) ? base[k] : 0;
26
- kDistance2Similarity[i] = vj + (100 * t - j) * (vk - vj);
24
+ table[i] = vj + (100 * t - j) * (vk - vj);
27
25
  }
26
+ return table;
27
+ }
28
+
29
+ // Built on first use. The initialization of a local static is thread-safe, which
30
+ // matters because the Python bindings release the GIL during inpainting.
31
+ const std::vector<double> &distance2similarity()
32
+ {
33
+ static const std::vector<double> table = make_distance2similarity();
34
+ return table;
28
35
  }
29
36
 
30
37
  inline void _weighted_copy(const MaskedImage &source, int ys, int xs, cv::Mat &target, int yt, int xt, double weight)
@@ -69,16 +76,11 @@ void Inpainting::_initialize_pyramid()
69
76
  source = source.downsample();
70
77
  m_pyramid.push_back(source);
71
78
  }
72
-
73
- if (kDistance2Similarity.size() == 0)
74
- {
75
- init_kDistance2Similarity();
76
- }
77
79
  }
78
80
 
79
81
  cv::Mat Inpainting::run(bool verbose, bool verbose_visualize, unsigned int random_seed)
80
82
  {
81
- srand(random_seed);
83
+ NearestNeighborField::seed_random(random_seed);
82
84
  const int nr_levels = m_pyramid.size();
83
85
 
84
86
  MaskedImage source, target;
@@ -124,7 +126,12 @@ cv::Mat Inpainting::run(bool verbose, bool verbose_visualize, unsigned int rando
124
126
  target = _expectation_maximization(source, target, level, verbose);
125
127
  }
126
128
 
127
- return target.image();
129
+ // Globally masked pixels are neither filled nor used as a source, so the pyramid
130
+ // leaves them black. Keep their input values instead.
131
+ cv::Mat result = target.image();
132
+ if (!m_initial.global_mask().empty())
133
+ m_initial.image().copyTo(result, m_initial.global_mask());
134
+ return result;
128
135
  }
129
136
 
130
137
  // EM-Like algorithm (see "PatchMatch" - page 6).
@@ -211,6 +218,7 @@ void Inpainting::_expectation_step(
211
218
  auto source_size = nnf.source_size();
212
219
  auto target_size = nnf.target_size();
213
220
  const int patch_size = m_distance_metric->patch_size();
221
+ const auto &kDistance2Similarity = distance2similarity();
214
222
 
215
223
  for (int i = 0; i < source_size.height; ++i)
216
224
  {
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
 
3
+ #include <cassert>
3
4
  #include <opencv2/core.hpp>
4
5
 
5
6
  class MaskedImage
@@ -1,6 +1,7 @@
1
1
  #include <algorithm>
2
2
  #include <iostream>
3
3
  #include <cmath>
4
+ #include <random>
4
5
 
5
6
  #include "masked_image.h"
6
7
  #include "nnf.h"
@@ -17,9 +18,35 @@ T clamp(T value, T min_value, T max_value)
17
18
  return std::min(std::max(value, min_value), max_value);
18
19
  }
19
20
 
21
+ namespace
22
+ {
23
+ // One generator per thread, so concurrent inpaintings with the same seed give the
24
+ // same results. It only drives the randomized search, which has to be reproducible
25
+ // from a seed, so a standard generator is the right choice. SonarCloud's PRNG rule
26
+ // (cpp:S2245) flags every declaration of its type, hence the NOSONAR markers.
27
+ std::mt19937 &random_engine() // NOSONAR
28
+ {
29
+ thread_local std::mt19937 engine; // NOSONAR
30
+ return engine;
31
+ }
32
+
33
+ // The modulo instead of std::uniform_int_distribution keeps the sequence identical
34
+ // on all platforms.
35
+ inline int random_int(int n)
36
+ {
37
+ return static_cast<int>(random_engine()() % static_cast<unsigned int>(n));
38
+ }
39
+ }
40
+
41
+ void NearestNeighborField::seed_random(unsigned int seed)
42
+ {
43
+ random_engine().seed(seed);
44
+ }
45
+
20
46
  void NearestNeighborField::_randomize_field(int max_retry, bool reset)
21
47
  {
22
48
  auto this_size = source_size();
49
+ auto this_target_size = target_size();
23
50
  for (int i = 0; i < this_size.height; ++i)
24
51
  {
25
52
  for (int j = 0; j < this_size.width; ++j)
@@ -37,8 +64,8 @@ void NearestNeighborField::_randomize_field(int max_retry, bool reset)
37
64
  int i_target = 0, j_target = 0;
38
65
  for (int t = 0; t < max_retry; ++t)
39
66
  {
40
- i_target = rand() % this_size.height;
41
- j_target = rand() % this_size.width;
67
+ i_target = random_int(this_target_size.height);
68
+ j_target = random_int(this_target_size.width);
42
69
  if (m_target.is_globally_masked(i_target, j_target))
43
70
  continue;
44
71
 
@@ -138,8 +165,8 @@ void NearestNeighborField::_minimize_link(int y, int x, int direction)
138
165
  int random_scale = (std::min(this_target_size.height, this_target_size.width) - 1) / 2;
139
166
  while (random_scale > 0)
140
167
  {
141
- int yp = this_ptr[0] + (rand() % (2 * random_scale + 1) - random_scale);
142
- int xp = this_ptr[1] + (rand() % (2 * random_scale + 1) - random_scale);
168
+ int yp = this_ptr[0] + (random_int(2 * random_scale + 1) - random_scale);
169
+ int xp = this_ptr[1] + (random_int(2 * random_scale + 1) - random_scale);
143
170
  yp = clamp(yp, 0, target_size().height - 1);
144
171
  xp = clamp(xp, 0, target_size().width - 1);
145
172
 
@@ -201,10 +228,9 @@ namespace
201
228
  const unsigned char *p_sgm = nullptr;
202
229
  const unsigned char *p_tgm = nullptr;
203
230
  if (!source.global_mask().empty())
204
- {
205
231
  p_sgm = source.global_mask().ptr<unsigned char>(yys, 0);
232
+ if (!target.global_mask().empty())
206
233
  p_tgm = target.global_mask().ptr<unsigned char>(yyt, 0);
207
- }
208
234
 
209
235
  const auto *p_sgy = source.grady().ptr<unsigned char>(yys, 0);
210
236
  const auto *p_tgy = target.grady().ptr<unsigned char>(yyt, 0);
@@ -216,7 +242,7 @@ namespace
216
242
  int xxs = xs + dx, xxt = xt + dx;
217
243
  wsum += 1;
218
244
 
219
- if (xxs <= 0 || xxs >= source_size.width - 1 || xxt <= 0 || xxt >= source_size.width - 1)
245
+ if (xxs <= 0 || xxs >= source_size.width - 1 || xxt <= 0 || xxt >= target_size.width - 1)
220
246
  {
221
247
  distance += PatchSSDDistanceMetric::kSSDScale;
222
248
  continue;
@@ -1,5 +1,7 @@
1
1
  #pragma once
2
2
 
3
+ #include <cassert>
4
+ #include <cmath>
3
5
  #include <opencv2/core.hpp>
4
6
  #include "masked_image.h"
5
7
 
@@ -87,6 +89,9 @@ public:
87
89
 
88
90
  void minimize(int nr_pass);
89
91
 
92
+ // Seeds the random search of the calling thread.
93
+ static void seed_random(unsigned int seed);
94
+
90
95
  private:
91
96
  inline int _distance(int source_y, int source_x, int target_y, int target_x)
92
97
  {
@@ -115,8 +115,7 @@ int _dtype_cv_to_py(int dtype_cv)
115
115
 
116
116
  cv::Mat _py_to_cv2(PM_mat_t pymat)
117
117
  {
118
- int dtype = _dtype_py_to_cv(pymat.dtype);
119
- dtype = CV_MAKETYPE(pymat.dtype, pymat.shape.channels);
118
+ int dtype = CV_MAKETYPE(_dtype_py_to_cv(pymat.dtype), pymat.shape.channels);
120
119
  return cv::Mat(cv::Size(pymat.shape.width, pymat.shape.height), dtype, pymat.data_ptr).clone();
121
120
  }
122
121
 
@@ -76,14 +76,15 @@ def inpaint(
76
76
  mask: 1-channel uint8 or bool mask of the hole(s) to fill (non-zero = hole),
77
77
  with the same height and width as ``image``. If ``None``, all pure
78
78
  white pixels (255, 255, 255) are treated as holes.
79
- global_mask: mask like ``mask`` of pixels that must not be used as a source.
79
+ global_mask: mask like ``mask`` of pixels that are neither filled nor used as
80
+ a source; they keep their values from ``image``.
80
81
  patch_size: patch size for the inpainting algorithm.
81
82
 
82
83
  Returns:
83
84
  The repaired image, with the same shape as ``image``.
84
85
  """
85
86
  lib = _get_lib()
86
- image, mask, global_mask = _prepare_inputs(image, mask, global_mask)
87
+ image, mask, global_mask = _prepare_inputs(image, mask, global_mask, patch_size)
87
88
 
88
89
  if global_mask is None:
89
90
  return _call(lib.PM_inpaint, image, mask, ctypes.c_int(patch_size))
@@ -107,15 +108,16 @@ def inpaint_regularity(
107
108
  guide_weight: weight of the regularity term relative to the patch distance.
108
109
  """
109
110
  lib = _get_lib()
110
- image, mask, global_mask = _prepare_inputs(image, mask, global_mask)
111
+ image, mask, global_mask = _prepare_inputs(image, mask, global_mask, patch_size)
111
112
 
112
113
  if not (
113
114
  isinstance(ijmap, np.ndarray)
114
115
  and ijmap.ndim == 3
115
116
  and ijmap.shape[2] == 3
116
117
  and ijmap.dtype == np.float32
118
+ and ijmap.size > 0
117
119
  ):
118
- raise ValueError("ijmap must be an HxWx3 float32 array")
120
+ raise ValueError("ijmap must be a non-empty HxWx3 float32 array")
119
121
  ijmap = np.ascontiguousarray(ijmap)
120
122
 
121
123
  args = (ijmap, ctypes.c_int(patch_size), ctypes.c_float(guide_weight))
@@ -125,9 +127,16 @@ def inpaint_regularity(
125
127
 
126
128
 
127
129
  def _prepare_inputs(
128
- image: ImageLike, mask: ImageLike | None, global_mask: ImageLike | None
130
+ image: ImageLike,
131
+ mask: ImageLike | None,
132
+ global_mask: ImageLike | None,
133
+ patch_size: int,
129
134
  ) -> tuple[np.ndarray, np.ndarray, np.ndarray | None]:
130
135
  """Validate the inputs and convert them to contiguous arrays."""
136
+ # the native code crashes for 0 and never terminates for negative sizes
137
+ if patch_size < 1:
138
+ raise ValueError(f"patch_size must be at least 1, got {patch_size}")
139
+
131
140
  image = _canonize_image_array(image)
132
141
  mask = _default_mask(image) if mask is None else _canonize_mask_array(mask)
133
142
  if global_mask is not None:
@@ -10,12 +10,18 @@ license = "MIT"
10
10
  license-files = ["LICENSE"]
11
11
  authors = [
12
12
  { name = "The InvokeAI Project", email = "lincoln.stein@gmail.com" },
13
+ { name = "Jiayuan Mao", email = "maojiayuan@gmail.com" },
13
14
  { name = "Kyle Schouviller", email = "kyle0654@hotmail.com" },
14
15
  { name = "Matthias Wild", email = "mauwii@outlook.de" },
15
16
  { name = "Younesse ANDAM", email = "younesse.andam@gmail.com" },
16
17
  ]
17
18
  requires-python = ">=3.10"
19
+ keywords = ["inpainting", "patchmatch", "image-processing", "opencv"]
18
20
  classifiers = [
21
+ "Development Status :: 5 - Production/Stable",
22
+ "Operating System :: MacOS",
23
+ "Operating System :: Microsoft :: Windows",
24
+ "Operating System :: POSIX :: Linux",
19
25
  "Programming Language :: C++",
20
26
  "Programming Language :: Python :: 3",
21
27
  "Programming Language :: Python :: 3.10",
@@ -25,12 +31,15 @@ classifiers = [
25
31
  "Programming Language :: Python :: 3.14",
26
32
  "Topic :: Multimedia :: Graphics",
27
33
  "Topic :: Scientific/Engineering :: Image Processing",
34
+ "Typing :: Typed",
28
35
  ]
29
36
  dependencies = ["numpy", "pillow"]
30
37
  dynamic = ["version"]
31
38
 
32
39
  [project.urls]
33
40
  "Source Code" = "https://github.com/mauwii/PyPatchMatch"
41
+ Issues = "https://github.com/mauwii/PyPatchMatch/issues"
42
+ "Release Notes" = "https://github.com/mauwii/PyPatchMatch/releases"
34
43
 
35
44
  [dependency-groups]
36
45
  dev = [{ include-group = "test" }, "pre-commit", "ruff"]
@@ -3,7 +3,7 @@
3
3
  The patchmatch library only needs opencv_core. Linking it statically keeps the
4
4
  wheels small and free of OpenCV's GUI/codec dependencies.
5
5
 
6
- Usage: python build_opencv.py [PREFIX] (default: $OpenCV_ROOT)
6
+ Usage: python build_opencv.py (installs into $OpenCV_ROOT)
7
7
  """
8
8
 
9
9
  import os
@@ -36,9 +36,14 @@ CMAKE_OPTIONS = {
36
36
  "BUILD_opencv_apps": "OFF",
37
37
  "BUILD_opencv_python3": "OFF",
38
38
  "OPENCV_GENERATE_PKGCONFIG": "OFF",
39
+ # CMakeLists.txt ships the licenses from here with the wheels
40
+ "OPENCV_LICENSES_INSTALL_PATH": "licenses",
39
41
  # only core is built, so disable every optional backend and codec
40
42
  "WITH_ADE": "OFF",
41
43
  "WITH_AVIF": "OFF",
44
+ # ARM HAL libraries, patchmatch uses no function they accelerate
45
+ "WITH_CAROTENE": "OFF",
46
+ "WITH_KLEIDICV": "OFF",
42
47
  "WITH_EIGEN": "OFF",
43
48
  "WITH_FFMPEG": "OFF",
44
49
  "WITH_GSTREAMER": "OFF",
@@ -84,8 +89,10 @@ def find_cmake() -> str:
84
89
 
85
90
  def main() -> None:
86
91
  # OpenCV_ROOT is the variable CMake's find_package(OpenCV) looks for.
87
- default = os.environ.get("OpenCV_ROOT") # noqa: SIM112
88
- prefix = Path(sys.argv[1] if len(sys.argv) > 1 else default)
92
+ root = os.environ.get("OpenCV_ROOT") # noqa: SIM112
93
+ if not root:
94
+ sys.exit("OpenCV_ROOT must be set to the installation prefix")
95
+ prefix = Path(root)
89
96
  if any(prefix.rglob("OpenCVConfig.cmake")):
90
97
  print(f"OpenCV already installed in {prefix}")
91
98
  return
@@ -115,6 +122,11 @@ def main() -> None:
115
122
  )
116
123
  subprocess.run([cmake, "--install", build, "--config", "Release"], check=True)
117
124
 
125
+ # OpenCV installs only the licenses of its 3rdparty code
126
+ licenses = prefix / CMAKE_OPTIONS["OPENCV_LICENSES_INSTALL_PATH"]
127
+ licenses.mkdir(parents=True, exist_ok=True)
128
+ shutil.copyfile(source / "LICENSE", licenses / "opencv-LICENSE")
129
+
118
130
 
119
131
  if __name__ == "__main__":
120
132
  main()
@@ -25,9 +25,16 @@ def surrounding(mask: np.ndarray, width: int = 10) -> np.ndarray:
25
25
  return (np.array(grown) > 0) & ~mask
26
26
 
27
27
 
28
- def assert_plausible_fill(source: np.ndarray, result: np.ndarray) -> None:
29
- """Check an inpainting result of ``source`` without a ground truth image."""
30
- holes = white_pixels(source)
28
+ def assert_plausible_fill(
29
+ source: np.ndarray, result: np.ndarray, excluded: np.ndarray | None = None
30
+ ) -> None:
31
+ """Check an inpainting result of ``source`` without a ground truth image.
32
+
33
+ ``excluded`` marks the pixels of a global mask, which are not filled.
34
+ """
35
+ if excluded is None:
36
+ excluded = np.zeros(source.shape[:2], dtype=bool)
37
+ holes = white_pixels(source) & ~excluded
31
38
  assert holes.any()
32
39
  assert result.shape == source.shape
33
40
  assert result.dtype == np.uint8
@@ -39,7 +46,7 @@ def assert_plausible_fill(source: np.ndarray, result: np.ndarray) -> None:
39
46
  assert outside < 1, outside
40
47
 
41
48
  # the filling blends in with its surroundings
42
- band = surrounding(holes)
49
+ band = surrounding(holes) & ~excluded
43
50
  color_diff = np.abs(result[holes].mean(axis=0) - source[band].mean(axis=0))
44
51
  assert color_diff.max() < 10, color_diff
45
52
 
@@ -79,15 +86,37 @@ def test_remove_white_regions(pruned_image, tmp_path, as_array):
79
86
 
80
87
 
81
88
  def test_global_mask(pruned_image):
82
- """examples/py_example_global_mask.py: extra hole excluded as patch source."""
89
+ """examples/py_example_global_mask.py: the globally masked plant is kept."""
83
90
  source = np.array(pruned_image)
84
- source[:100, :100] = 255
85
91
  global_mask = np.zeros_like(source[..., 0])
86
- global_mask[:100, :100] = 1
92
+ global_mask[290:, 100:180] = 1
87
93
 
88
94
  result = patchmatch.inpaint(source, global_mask=global_mask, patch_size=3)
89
95
 
90
- assert_plausible_fill(source, result)
96
+ excluded = global_mask.astype(bool)
97
+ np.testing.assert_array_equal(result[excluded], source[excluded])
98
+ assert_plausible_fill(source, result, excluded)
99
+
100
+
101
+ def test_global_mask_is_not_used_as_source(pruned_image):
102
+ """A red block next to a hole fills it, unless the block is globally masked."""
103
+ source = np.array(pruned_image)
104
+ source[200:300, 20:120] = (255, 0, 0)
105
+ mask = np.zeros(source.shape[:2], dtype=np.uint8)
106
+ mask[210:290, 120:160] = 1
107
+ global_mask = np.zeros_like(mask)
108
+ global_mask[200:300, 20:120] = 1
109
+
110
+ def red_in_hole(result: np.ndarray) -> bool:
111
+ red = (result[..., 0] > 150) & (result[..., 1] < 80) & (result[..., 2] < 80)
112
+ return bool(red[mask == 1].any())
113
+
114
+ assert red_in_hole(patchmatch.inpaint(source, mask, patch_size=3))
115
+
116
+ result = patchmatch.inpaint(source, mask, global_mask=global_mask, patch_size=3)
117
+ assert not red_in_hole(result)
118
+ excluded = global_mask == 1
119
+ np.testing.assert_array_equal(result[excluded], source[excluded])
91
120
 
92
121
 
93
122
  @pytest.mark.parametrize("hole_value", [1, 255])
@@ -3,6 +3,7 @@
3
3
  import ctypes
4
4
  import subprocess
5
5
  import sys
6
+ from concurrent.futures import ThreadPoolExecutor
6
7
 
7
8
  import numpy as np
8
9
  import pytest
@@ -83,9 +84,10 @@ def test_inpaint_explicit_mask(image, hole_mask):
83
84
  def test_inpaint_global_mask(image, hole_mask):
84
85
  global_mask = np.zeros_like(hole_mask)
85
86
  global_mask[:8] = 1
86
- assert_filled(
87
- patchmatch.inpaint(image, global_mask=global_mask, patch_size=3), image
88
- )
87
+ result = patchmatch.inpaint(image, global_mask=global_mask, patch_size=3)
88
+ assert_filled(result, image)
89
+ # excluded pixels keep their values instead of the zeros of the pyramid
90
+ np.testing.assert_array_equal(result[:8], image[:8])
89
91
 
90
92
 
91
93
  @pytest.mark.parametrize("patch_size", [1, 3, 7])
@@ -108,6 +110,17 @@ def test_inpaint_is_deterministic_with_seed(image):
108
110
  np.testing.assert_array_equal(first, second)
109
111
 
110
112
 
113
+ def test_concurrent_inpaint_is_deterministic(image):
114
+ # the native code runs without the GIL, so the threads really overlap
115
+ expected = patchmatch.inpaint(image, patch_size=3)
116
+ with ThreadPoolExecutor(4) as pool:
117
+ results = list(
118
+ pool.map(lambda _: patchmatch.inpaint(image, patch_size=3), range(16))
119
+ )
120
+ for result in results:
121
+ np.testing.assert_array_equal(result, expected)
122
+
123
+
111
124
  @pytest.mark.parametrize("verbose", [True, False])
112
125
  def test_set_verbose_prints_progress(verbose):
113
126
  # The native code writes to its own C runtime's stderr. On Windows the DLL
@@ -230,14 +243,27 @@ def test_mask_size_must_match_image(image, ijmap, func, argument):
230
243
  func(image, **kwargs)
231
244
 
232
245
 
246
+ @pytest.mark.parametrize("patch_size", [0, -1])
247
+ @pytest.mark.parametrize(
248
+ "func",
249
+ [patchmatch.inpaint, patchmatch.inpaint_regularity],
250
+ ids=lambda f: f.__name__,
251
+ )
252
+ def test_invalid_patch_size(image, ijmap, func, patch_size):
253
+ kwargs = {"ijmap": ijmap} if func is patchmatch.inpaint_regularity else {}
254
+ with pytest.raises(ValueError, match="patch_size"):
255
+ func(image, None, patch_size=patch_size, **kwargs)
256
+
257
+
233
258
  @pytest.mark.parametrize(
234
259
  "bad_ijmap",
235
260
  [
236
261
  np.zeros((HEIGHT, WIDTH, 3), dtype=np.float64),
237
262
  np.zeros((HEIGHT, WIDTH, 2), dtype=np.float32),
263
+ np.zeros((0, WIDTH, 3), dtype=np.float32),
238
264
  [[[0.0, 0.0, 0.0]]],
239
265
  ],
240
- ids=["float64", "2-channel", "list"],
266
+ ids=["float64", "2-channel", "empty", "list"],
241
267
  )
242
268
  def test_inpaint_regularity_invalid_ijmap(image, bad_ijmap):
243
269
  with pytest.raises(ValueError, match="ijmap"):
@@ -1,96 +0,0 @@
1
- # PatchMatch based Inpainting
2
-
3
- [![License: MIT](https://img.shields.io/badge/License-MIT-blueviolet.svg)](https://github.com/mauwii/PyPatchMatch/blob/main/LICENSE)
4
- [![PyPI](https://img.shields.io/pypi/v/PyPatchMatch)](https://pypi.org/project/PyPatchMatch/)
5
- [![Downloads](https://static.pepy.tech/badge/pypatchmatch)](https://pepy.tech/project/pypatchmatch)
6
- [![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)
7
-
8
- This library implements the PatchMatch based inpainting algorithm. It provides both C++
9
- and Python interfaces. This implementation is heavily based on the implementation by
10
- Younesse ANDAM: [younesse-cv/PatchMatch](https://github.com/younesse-cv/PatchMatch),
11
- with some bug fixes, and updates.
12
-
13
- ## Installation
14
-
15
- ```sh
16
- pip install PyPatchMatch
17
- ```
18
-
19
- Wheels for Linux, macOS and Windows ship the compiled library including the required
20
- OpenCV runtime, so no compiler or OpenCV installation is needed. On other platforms pip
21
- builds from the source distribution, which requires a C++17 compiler, CMake and the
22
- OpenCV development files (e.g. `apt install libopencv-dev` or `brew install opencv`).
23
-
24
- ## Usage
25
-
26
- Python (see [examples/py_example.py](examples/py_example.py)):
27
-
28
- ```python
29
- import patchmatch
30
-
31
- image = ... # HxWx3 uint8 numpy array or PIL image
32
- mask = ... # HxW uint8 or bool numpy array or PIL image, non-zero marks the holes
33
- result = patchmatch.inpaint(image, mask, patch_size=3)
34
- ```
35
-
36
- The mask must have the same height and width as the image. If `mask` is omitted, all
37
- pure white pixels are treated as holes.
38
- `patchmatch.patchmatch_available` tells whether the native library could be loaded.
39
- The previous import path `from patchmatch import patch_match` keeps working.
40
-
41
- C++ (see [examples/cpp_example.cpp](examples/cpp_example.cpp), build and run it with
42
- [examples/cpp_example_run.sh](examples/cpp_example_run.sh)):
43
-
44
- ```cpp
45
- #include "inpaint.h"
46
-
47
- int main() {
48
- cv::Mat image = ...;
49
- cv::Mat mask = ...;
50
-
51
- auto metric = PatchSSDDistanceMetric(5);
52
- cv::Mat result = Inpainting(image, mask, &metric).run();
53
- }
54
- ```
55
-
56
- The library is built with CMake; `PATCHMATCH_BUILD_EXAMPLES` builds the example and
57
- `PATCHMATCH_WITH_HIGHGUI` enables the debug visualization of `Inpainting::run`.
58
-
59
- ## Development
60
-
61
- The project is managed with [uv](https://docs.astral.sh/uv/):
62
-
63
- ```sh
64
- uv sync # create .venv, build the library, install dev deps
65
- uv run pre-commit install # enable ruff and the other hooks on commit
66
- uv run pytest # run the test suite
67
- uv run pytest -m "not e2e" # only the fast unit tests
68
- ```
69
-
70
- Releases are published to PyPI by creating a GitHub release; the version is taken from
71
- its tag (e.g. `v1.1.0`).
72
-
73
- ## README and COPYRIGHT by Younesse ANDAM
74
-
75
- @Author: Younesse ANDAM
76
-
77
- @Contact: younesse.andam@gmail.com
78
-
79
- Description:
80
-
81
- This project is a personal implementation of an algorithm called PATCHMATCH
82
- that restores missing areas in an image. The algorithm is presented in the following
83
- paper PatchMatch A Randomized Correspondence Algorithm for Structural Image Editing by
84
- C.Barnes, E.Shechtman, A.Finkelstein and Dan B.Goldman ACM Transactions on Graphics
85
- (Proc. SIGGRAPH), vol.28, aug-2009
86
-
87
- For more information please refer to
88
- http://www.cs.princeton.edu/gfx/pubs/Barnes_2009_PAR/index.php
89
-
90
- Copyright (c) 2010-2011
91
-
92
- ## Requirements
93
-
94
- To run the project you need to install Opencv library and link it to your project.
95
- Opencv can be download it here
96
- http://opencv.org/downloads.html
File without changes