patchmatch-cython 0.2.0__tar.gz → 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {patchmatch_cython-0.2.0/src/patchmatch_cython.egg-info → patchmatch_cython-1.0.0}/PKG-INFO +113 -43
  2. patchmatch_cython-1.0.0/README.md +254 -0
  3. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/local_build.py +29 -0
  4. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/pyproject.toml +2 -2
  5. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/setup.py +44 -8
  6. patchmatch_cython-1.0.0/src/patchmatch_cython/__init__.py +51 -0
  7. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/src/patchmatch_cython/cython_solver.py +29 -0
  8. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/src/patchmatch_cython/cython_solver_impl.cpp +1604 -1567
  9. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/src/patchmatch_cython/cython_solver_impl.pyx +29 -0
  10. patchmatch_cython-1.0.0/src/patchmatch_cython/exceptions.py +46 -0
  11. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/src/patchmatch_cython/imageops.py +29 -0
  12. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/src/patchmatch_cython/inpaint.py +173 -63
  13. patchmatch_cython-1.0.0/src/patchmatch_cython/philippeau.cpp +65351 -0
  14. patchmatch_cython-1.0.0/src/patchmatch_cython/philippeau.pyx +1612 -0
  15. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/src/patchmatch_cython/python_solver.py +29 -0
  16. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/src/patchmatch_cython/solver.py +29 -0
  17. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0/src/patchmatch_cython.egg-info}/PKG-INFO +113 -43
  18. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/src/patchmatch_cython.egg-info/SOURCES.txt +4 -0
  19. patchmatch_cython-1.0.0/tests/test_basic_functionality.py +58 -0
  20. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/tests/test_imageops.py +29 -0
  21. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/tests/test_inpaint_api.py +29 -0
  22. patchmatch_cython-1.0.0/tests/test_philippeau.py +171 -0
  23. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/tests/test_quality_options.py +29 -0
  24. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/tests/test_questionable_behaviors.py +29 -0
  25. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/tests/test_solvers.py +29 -0
  26. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/tests/test_validation.py +29 -0
  27. patchmatch_cython-0.2.0/README.md +0 -184
  28. patchmatch_cython-0.2.0/src/patchmatch_cython/__init__.py +0 -19
  29. patchmatch_cython-0.2.0/tests/test_basic_functionality.py +0 -29
  30. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/LICENSE +0 -0
  31. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/MANIFEST.in +0 -0
  32. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/setup.cfg +0 -0
  33. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/src/patchmatch_cython.egg-info/dependency_links.txt +0 -0
  34. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/src/patchmatch_cython.egg-info/not-zip-safe +0 -0
  35. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/src/patchmatch_cython.egg-info/requires.txt +0 -0
  36. {patchmatch_cython-0.2.0 → patchmatch_cython-1.0.0}/src/patchmatch_cython.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchmatch-cython
3
- Version: 0.2.0
3
+ Version: 1.0.0
4
4
  Summary: High-performance PatchMatch implementation for image inpainting using Cython
5
5
  Home-page: https://github.com/Teriks/patchmatch-cython
6
6
  Author: Teriks
@@ -37,7 +37,7 @@ Project-URL: Homepage, https://github.com/Teriks/patchmatch-cython
37
37
  Project-URL: Repository, https://github.com/Teriks/patchmatch-cython
38
38
  Project-URL: Issues, https://github.com/Teriks/patchmatch-cython/issues
39
39
  Keywords: image-processing,inpainting,patchmatch,cython,computer-vision
40
- Classifier: Development Status :: 4 - Beta
40
+ Classifier: Development Status :: 5 - Production/Stable
41
41
  Classifier: Intended Audience :: Developers
42
42
  Classifier: Intended Audience :: Science/Research
43
43
  Classifier: Programming Language :: Python :: 3
@@ -70,7 +70,7 @@ Dynamic: requires-python
70
70
 
71
71
  # PatchMatch Cython
72
72
 
73
- High-performance PatchMatch implementation for pyramidical image inpainting in Cython.
73
+ High-performance PatchMatch implementation for pyramidical image inpainting in Cython. The default solver is the Barnes-style pyramid below. `algorithm="philippeau"` runs the Xavier Philippeau pyramid, also in Cython.
74
74
 
75
75
  This package can function without OpenCV (`opencv-python` / `opencv-python-headless`) with only slightly reduced quality and speed in a few helpers. When OpenCV is installed it is used for resize (`INTER_LINEAR` / `INTER_AREA`), morphology for valid/query centers, and distance transforms for seam feathering.
76
76
 
@@ -91,69 +91,101 @@ pip install .
91
91
 
92
92
  ## Usage
93
93
 
94
- ### With OpenCV
94
+ Images are `HxWxC`. Masks are `HxW`: `True` or any non-zero value is the hole. The result is `uint8`, and pixels outside the hole are unchanged. `inpaint_pyramid` picks `CythonSolver` when the extension is built, and `PythonSolver` otherwise. Pass `solver_class=PythonSolver` to force the pure Python path.
95
+
96
+ ### Remove a region from a photo
95
97
 
96
98
  ```python
97
99
  import cv2
98
100
  from patchmatch_cython import inpaint_pyramid
99
101
 
100
- image = cv2.imread('image.jpg')
101
- mask = cv2.imread('mask.png', 0) > 128 # True = hole to fill
102
+ image = cv2.imread("photo.jpg") # BGR uint8
103
+ mask = cv2.imread("hole.png", cv2.IMREAD_GRAYSCALE) > 128
102
104
 
103
- result = inpaint_pyramid(image, mask)
104
- cv2.imwrite('result.jpg', result)
105
+ result = inpaint_pyramid(image, mask, seed=42)
106
+ cv2.imwrite("filled.jpg", result)
105
107
  ```
106
108
 
107
- ### With Pillow
109
+ The same job with Pillow. Convert to RGB so a PNG with an alpha channel stays three channels.
108
110
 
109
111
  ```python
110
112
  import numpy as np
111
113
  from PIL import Image
112
114
  from patchmatch_cython import inpaint_pyramid
113
115
 
114
- image = np.array(Image.open('image.jpg'))
115
- mask = np.array(Image.open('mask.png').convert('L')) > 128
116
+ image = np.array(Image.open("photo.jpg").convert("RGB"))
117
+ mask = np.array(Image.open("hole.png").convert("L")) > 128
116
118
 
117
- result = inpaint_pyramid(image, mask) # auto-selects fastest available solver
118
- Image.fromarray(result).save('result.jpg')
119
+ result = inpaint_pyramid(image, mask, seed=42)
120
+ Image.fromarray(result).save("filled.jpg")
119
121
  ```
120
122
 
121
- ### Explicit solver selection
123
+ A rectangle is enough when you do not have a mask file. `seed` makes the fill repeatable; omit it and each run can differ.
122
124
 
123
125
  ```python
124
- from patchmatch_cython import PythonSolver, CythonSolver, inpaint_pyramid
126
+ import numpy as np
127
+ from PIL import Image
128
+ from patchmatch_cython import inpaint_pyramid
125
129
 
126
- result = inpaint_pyramid(image, mask, solver_class=CythonSolver) # Fast
127
- result = inpaint_pyramid(image, mask, solver_class=PythonSolver) # Fallback
130
+ image = np.array(Image.open("photo.jpg").convert("RGB"))
131
+ mask = np.zeros(image.shape[:2], dtype=bool)
132
+ mask[120:200, 80:160] = True
133
+
134
+ result = inpaint_pyramid(image, mask, seed=42)
135
+ Image.fromarray(result).save("filled.jpg")
128
136
  ```
129
137
 
130
- ### Quality / compose options
138
+ ### Thin scratch, one scale
131
139
 
132
- Both `inpaint_pyramid` and `inpaint_single` accept the same optional knobs.
133
- Defaults preserve a sensible baseline; turn options on when you need more structure,
134
- completeness, or softer seams.
140
+ `inpaint_single` stays at full resolution. Use it for a narrow mark, where a pyramid has little to add. `iterations` is the number of match passes.
135
141
 
136
142
  ```python
137
- from patchmatch_cython import inpaint_pyramid
143
+ import numpy as np
144
+ from PIL import Image
145
+ from patchmatch_cython import inpaint_single
146
+
147
+ image = np.array(Image.open("photo.jpg").convert("RGB"))
148
+ mask = np.zeros(image.shape[:2], dtype=bool)
149
+ mask[240:244, 40:400] = True
150
+
151
+ result = inpaint_single(image, mask, iterations=5, seed=42)
152
+ Image.fromarray(result).save("scratch.jpg")
153
+ ```
154
+
155
+ ### Lines, texture, and a soft rim
156
+
157
+ These keywords apply to `algorithm="patchmatch"`. Passing any of them with `algorithm="philippeau"` raises `UnsupportedOption`, including when the value is the usual default. An unknown keyword raises `TypeError`.
138
158
 
159
+ Turn on structure matching when the hole crosses an edge or a repeating texture. `edge_weighted` matches gradients as well as color. `reverse_search` pulls in source patches the first pass underused. `hole_refine` runs a few extra full-resolution passes on the hole only.
160
+
161
+ ```python
139
162
  result = inpaint_pyramid(
140
163
  image,
141
164
  mask,
142
- patch_size=5, # odd int; square patch side
143
- seed=0, # reproducible RNG (optional)
144
- em_loops=2, # coarse match→vote→rematch loops (pyramid: coarse levels only)
145
- edge_weighted=True, # structure-aware distance (boosted gradients)
146
- reverse_search=True, # source→query completeness pass + stronger usage penalty
147
- poisson_blend=True, # harmonic seam soften (default ~8px band)
148
- hole_refine=3, # extra full-res PatchMatch iterations after the main solve
149
- mask_feather=6.0, # soft compose width in px from the hole boundary
165
+ seed=42,
166
+ edge_weighted=True,
167
+ reverse_search=True,
168
+ hole_refine=2,
150
169
  )
151
170
  ```
152
171
 
172
+ The default already blends a narrow band along the hole rim (`poisson_blend=True`). Widen that band when the seam is still visible. `mask_feather` is a width in pixels and replaces the default rim blend.
173
+
174
+ ```python
175
+ result = inpaint_pyramid(image, mask, seed=42, mask_feather=8.0)
176
+ ```
177
+
178
+ `patch_size` is the square patch side and must be odd. A larger side sees more context and costs more. `em_loops` repeats match and vote on the coarse pyramid levels.
179
+
180
+ ```python
181
+ result = inpaint_pyramid(image, mask, patch_size=7, em_loops=2, seed=42)
182
+ ```
183
+
153
184
  | Parameter | Default | Description |
154
185
  |---|---|---|
155
- | `patch_size` | `5` | Odd positive int; square patch size for matching / voting. |
156
- | `seed` | `None` | RNG seed for reproducible NNF init / search. |
186
+ | `patch_size` | `5` | Odd positive int; square patch side for matching and voting. |
187
+ | `seed` | `None` | RNG seed. `None` leaves the default solver unseeded. |
188
+ | `solver_class` | `None` | `None` uses `CythonSolver` when the extension is available, otherwise `PythonSolver`. |
157
189
  | `em_loops` | `1` | Outer match→paint→rematch loops. On pyramids, applied at coarse levels only (finest stays `1`). |
158
190
  | `edge_weighted` | `False` | `False` = color-only matching. `True` = color + boosted gradient channels. Hole-touching gradients are suppressed so zeroed holes do not create fake edges. |
159
191
  | `reverse_search` | `False` | After each EM loop, try assigning underused near-hole sources to better queries (completeness). |
@@ -161,25 +193,63 @@ result = inpaint_pyramid(
161
193
  | `hole_refine` | `0` | Extra full-resolution PatchMatch iterations after the main solve (`0` disables). Only hole pixels are painted; the user mask is not modified. |
162
194
  | `mask_feather` | `0.0` | Soft compose width in pixels from the hole boundary. Uses a distance ramp toward a harmonic rim pull (not the original hole content). Does **not** change the mask used for matching. |
163
195
 
164
- `inpaint_single` is the same API plus `iterations` (PatchMatch passes at one scale):
196
+ `inpaint_single` takes the same keywords, plus `iterations` (default `5`).
197
+
198
+ Masks may be `bool`, `{0,1}`, or `{0,255}`. The library does not dilate or erode the mask used for matching. `mask_feather` only changes the final composite.
199
+
200
+ ### Xavier Philippeau
201
+
202
+ `algorithm="philippeau"`, or `inpaint_philippeau`, runs Xavier Philippeau's pyramid: bidirectional nearest-neighbor fields, a texture-energy channel, split votes on the fine levels, and a short border blend. The implementation is Cython and does not use OpenCV. Known pixels stay exact. Passing a PatchMatch keyword (`solver_class`, `em_loops`, `edge_weighted`, `reverse_search`, `poisson_blend`, `hole_refine`, `mask_feather`, or `iterations`) raises `UnsupportedOption`.
203
+
204
+ In this mode `patch_size` is a **radius**: each patch spans `2 * patch_size + 1` pixels. Any integer from 1 up is valid, including even values. A radius of 3 is a 7×7 patch. `inpaint_philippeau` defaults to radius 15. `inpaint_pyramid` and `inpaint_single` still default `patch_size` to 5, and that 5 is a radius here.
205
+
206
+ Pass `seed` for a repeatable fill. If it is omitted, a 32-bit seed is drawn for that run.
207
+
208
+ `inpaint_philippeau` requires an HxWx3 `uint8` image. `inpaint_pyramid` and `inpaint_single` round other numeric images into `uint8` and clip them to 0–255 before filling. The same `uint8` image, mask, radius, and seed produce the same fill from either call.
209
+
210
+ Fill a photo. Radius 7 is a 15×15 patch, a practical size for a region a few dozen pixels across. Radius 15 is the default on `inpaint_philippeau` and suits a larger hole.
165
211
 
166
212
  ```python
167
- from patchmatch_cython import inpaint_single
213
+ import numpy as np
214
+ from PIL import Image
215
+ from patchmatch_cython import inpaint_philippeau
216
+
217
+ image = np.array(Image.open("photo.jpg").convert("RGB"))
218
+ mask = np.array(Image.open("hole.png").convert("L")) > 0
219
+
220
+ result = inpaint_philippeau(image, mask, patch_size=7, seed=42)
221
+ Image.fromarray(result).save("filled.jpg")
222
+ ```
168
223
 
169
- result = inpaint_single(
224
+ `inpaint_pyramid(..., algorithm="philippeau")` is the same fill when the image is already `uint8` and `patch_size` and `seed` match.
225
+
226
+ ```python
227
+ from patchmatch_cython import inpaint_pyramid
228
+
229
+ result = inpaint_pyramid(image, mask, patch_size=7, seed=42, algorithm="philippeau")
230
+ ```
231
+
232
+ If `mask` is omitted, pure white pixels (`255, 255, 255`) are the holes. Use that for a plate or sky that was painted white.
233
+
234
+ ```python
235
+ result = inpaint_philippeau(image, patch_size=7, seed=42)
236
+ ```
237
+
238
+ `global_mask` marks pixels that must not be filled and must not be copied from, such as a logo or a frame. `ijmap` is an optional HxWx3 `float32` regularity map; the first two channels are read, and `guide_weight` (default `0.25`) sets its weight. `inpaint_pyramid` and `inpaint_single` do not take these arguments.
239
+
240
+ ```python
241
+ keep_out = np.zeros(image.shape[:2], dtype=bool)
242
+ keep_out[0:40, 0:120] = True # logo in the corner
243
+
244
+ result = inpaint_philippeau(
170
245
  image,
171
246
  mask,
172
- iterations=7,
173
- em_loops=2,
174
- reverse_search=True,
175
- hole_refine=2,
176
- mask_feather=4.0,
177
- poisson_blend=False, # feather handles seams when mask_feather > 0
247
+ global_mask=keep_out,
248
+ patch_size=7,
249
+ seed=42,
178
250
  )
179
251
  ```
180
252
 
181
- **Masks:** accept `bool`, `{0,1}`, or `{0,255}`. `True` / non-zero = region to inpaint. Known (unmasked) pixels are preserved in the output. The library never dilates/erodes the user mask for matching; `mask_feather` only affects final compositing.
182
-
183
253
  **OpenCV runtime toggle** (optional):
184
254
 
185
255
  ```python
@@ -0,0 +1,254 @@
1
+ # PatchMatch Cython
2
+
3
+ High-performance PatchMatch implementation for pyramidical image inpainting in Cython. The default solver is the Barnes-style pyramid below. `algorithm="philippeau"` runs the Xavier Philippeau pyramid, also in Cython.
4
+
5
+ This package can function without OpenCV (`opencv-python` / `opencv-python-headless`) with only slightly reduced quality and speed in a few helpers. When OpenCV is installed it is used for resize (`INTER_LINEAR` / `INTER_AREA`), morphology for valid/query centers, and distance transforms for seam feathering.
6
+
7
+ The package provides a pure Python solver and a Cython solver; the Cython solver is typically about 30–50× faster depending on patch size.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install patchmatch-cython
13
+ ```
14
+
15
+ **From source:**
16
+ ```bash
17
+ git clone <repository-url>
18
+ cd patchmatch-cython
19
+ pip install .
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ Images are `HxWxC`. Masks are `HxW`: `True` or any non-zero value is the hole. The result is `uint8`, and pixels outside the hole are unchanged. `inpaint_pyramid` picks `CythonSolver` when the extension is built, and `PythonSolver` otherwise. Pass `solver_class=PythonSolver` to force the pure Python path.
25
+
26
+ ### Remove a region from a photo
27
+
28
+ ```python
29
+ import cv2
30
+ from patchmatch_cython import inpaint_pyramid
31
+
32
+ image = cv2.imread("photo.jpg") # BGR uint8
33
+ mask = cv2.imread("hole.png", cv2.IMREAD_GRAYSCALE) > 128
34
+
35
+ result = inpaint_pyramid(image, mask, seed=42)
36
+ cv2.imwrite("filled.jpg", result)
37
+ ```
38
+
39
+ The same job with Pillow. Convert to RGB so a PNG with an alpha channel stays three channels.
40
+
41
+ ```python
42
+ import numpy as np
43
+ from PIL import Image
44
+ from patchmatch_cython import inpaint_pyramid
45
+
46
+ image = np.array(Image.open("photo.jpg").convert("RGB"))
47
+ mask = np.array(Image.open("hole.png").convert("L")) > 128
48
+
49
+ result = inpaint_pyramid(image, mask, seed=42)
50
+ Image.fromarray(result).save("filled.jpg")
51
+ ```
52
+
53
+ A rectangle is enough when you do not have a mask file. `seed` makes the fill repeatable; omit it and each run can differ.
54
+
55
+ ```python
56
+ import numpy as np
57
+ from PIL import Image
58
+ from patchmatch_cython import inpaint_pyramid
59
+
60
+ image = np.array(Image.open("photo.jpg").convert("RGB"))
61
+ mask = np.zeros(image.shape[:2], dtype=bool)
62
+ mask[120:200, 80:160] = True
63
+
64
+ result = inpaint_pyramid(image, mask, seed=42)
65
+ Image.fromarray(result).save("filled.jpg")
66
+ ```
67
+
68
+ ### Thin scratch, one scale
69
+
70
+ `inpaint_single` stays at full resolution. Use it for a narrow mark, where a pyramid has little to add. `iterations` is the number of match passes.
71
+
72
+ ```python
73
+ import numpy as np
74
+ from PIL import Image
75
+ from patchmatch_cython import inpaint_single
76
+
77
+ image = np.array(Image.open("photo.jpg").convert("RGB"))
78
+ mask = np.zeros(image.shape[:2], dtype=bool)
79
+ mask[240:244, 40:400] = True
80
+
81
+ result = inpaint_single(image, mask, iterations=5, seed=42)
82
+ Image.fromarray(result).save("scratch.jpg")
83
+ ```
84
+
85
+ ### Lines, texture, and a soft rim
86
+
87
+ These keywords apply to `algorithm="patchmatch"`. Passing any of them with `algorithm="philippeau"` raises `UnsupportedOption`, including when the value is the usual default. An unknown keyword raises `TypeError`.
88
+
89
+ Turn on structure matching when the hole crosses an edge or a repeating texture. `edge_weighted` matches gradients as well as color. `reverse_search` pulls in source patches the first pass underused. `hole_refine` runs a few extra full-resolution passes on the hole only.
90
+
91
+ ```python
92
+ result = inpaint_pyramid(
93
+ image,
94
+ mask,
95
+ seed=42,
96
+ edge_weighted=True,
97
+ reverse_search=True,
98
+ hole_refine=2,
99
+ )
100
+ ```
101
+
102
+ The default already blends a narrow band along the hole rim (`poisson_blend=True`). Widen that band when the seam is still visible. `mask_feather` is a width in pixels and replaces the default rim blend.
103
+
104
+ ```python
105
+ result = inpaint_pyramid(image, mask, seed=42, mask_feather=8.0)
106
+ ```
107
+
108
+ `patch_size` is the square patch side and must be odd. A larger side sees more context and costs more. `em_loops` repeats match and vote on the coarse pyramid levels.
109
+
110
+ ```python
111
+ result = inpaint_pyramid(image, mask, patch_size=7, em_loops=2, seed=42)
112
+ ```
113
+
114
+ | Parameter | Default | Description |
115
+ |---|---|---|
116
+ | `patch_size` | `5` | Odd positive int; square patch side for matching and voting. |
117
+ | `seed` | `None` | RNG seed. `None` leaves the default solver unseeded. |
118
+ | `solver_class` | `None` | `None` uses `CythonSolver` when the extension is available, otherwise `PythonSolver`. |
119
+ | `em_loops` | `1` | Outer match→paint→rematch loops. On pyramids, applied at coarse levels only (finest stays `1`). |
120
+ | `edge_weighted` | `False` | `False` = color-only matching. `True` = color + boosted gradient channels. Hole-touching gradients are suppressed so zeroed holes do not create fake edges. |
121
+ | `reverse_search` | `False` | After each EM loop, try assigning underused near-hole sources to better queries (completeness). |
122
+ | `poisson_blend` | `True` | Soften seams by blending the PatchMatch fill with a harmonic membrane near the hole rim. Ignored when `mask_feather > 0` (feather supplies the band). |
123
+ | `hole_refine` | `0` | Extra full-resolution PatchMatch iterations after the main solve (`0` disables). Only hole pixels are painted; the user mask is not modified. |
124
+ | `mask_feather` | `0.0` | Soft compose width in pixels from the hole boundary. Uses a distance ramp toward a harmonic rim pull (not the original hole content). Does **not** change the mask used for matching. |
125
+
126
+ `inpaint_single` takes the same keywords, plus `iterations` (default `5`).
127
+
128
+ Masks may be `bool`, `{0,1}`, or `{0,255}`. The library does not dilate or erode the mask used for matching. `mask_feather` only changes the final composite.
129
+
130
+ ### Xavier Philippeau
131
+
132
+ `algorithm="philippeau"`, or `inpaint_philippeau`, runs Xavier Philippeau's pyramid: bidirectional nearest-neighbor fields, a texture-energy channel, split votes on the fine levels, and a short border blend. The implementation is Cython and does not use OpenCV. Known pixels stay exact. Passing a PatchMatch keyword (`solver_class`, `em_loops`, `edge_weighted`, `reverse_search`, `poisson_blend`, `hole_refine`, `mask_feather`, or `iterations`) raises `UnsupportedOption`.
133
+
134
+ In this mode `patch_size` is a **radius**: each patch spans `2 * patch_size + 1` pixels. Any integer from 1 up is valid, including even values. A radius of 3 is a 7×7 patch. `inpaint_philippeau` defaults to radius 15. `inpaint_pyramid` and `inpaint_single` still default `patch_size` to 5, and that 5 is a radius here.
135
+
136
+ Pass `seed` for a repeatable fill. If it is omitted, a 32-bit seed is drawn for that run.
137
+
138
+ `inpaint_philippeau` requires an HxWx3 `uint8` image. `inpaint_pyramid` and `inpaint_single` round other numeric images into `uint8` and clip them to 0–255 before filling. The same `uint8` image, mask, radius, and seed produce the same fill from either call.
139
+
140
+ Fill a photo. Radius 7 is a 15×15 patch, a practical size for a region a few dozen pixels across. Radius 15 is the default on `inpaint_philippeau` and suits a larger hole.
141
+
142
+ ```python
143
+ import numpy as np
144
+ from PIL import Image
145
+ from patchmatch_cython import inpaint_philippeau
146
+
147
+ image = np.array(Image.open("photo.jpg").convert("RGB"))
148
+ mask = np.array(Image.open("hole.png").convert("L")) > 0
149
+
150
+ result = inpaint_philippeau(image, mask, patch_size=7, seed=42)
151
+ Image.fromarray(result).save("filled.jpg")
152
+ ```
153
+
154
+ `inpaint_pyramid(..., algorithm="philippeau")` is the same fill when the image is already `uint8` and `patch_size` and `seed` match.
155
+
156
+ ```python
157
+ from patchmatch_cython import inpaint_pyramid
158
+
159
+ result = inpaint_pyramid(image, mask, patch_size=7, seed=42, algorithm="philippeau")
160
+ ```
161
+
162
+ If `mask` is omitted, pure white pixels (`255, 255, 255`) are the holes. Use that for a plate or sky that was painted white.
163
+
164
+ ```python
165
+ result = inpaint_philippeau(image, patch_size=7, seed=42)
166
+ ```
167
+
168
+ `global_mask` marks pixels that must not be filled and must not be copied from, such as a logo or a frame. `ijmap` is an optional HxWx3 `float32` regularity map; the first two channels are read, and `guide_weight` (default `0.25`) sets its weight. `inpaint_pyramid` and `inpaint_single` do not take these arguments.
169
+
170
+ ```python
171
+ keep_out = np.zeros(image.shape[:2], dtype=bool)
172
+ keep_out[0:40, 0:120] = True # logo in the corner
173
+
174
+ result = inpaint_philippeau(
175
+ image,
176
+ mask,
177
+ global_mask=keep_out,
178
+ patch_size=7,
179
+ seed=42,
180
+ )
181
+ ```
182
+
183
+ **OpenCV runtime toggle** (optional):
184
+
185
+ ```python
186
+ from patchmatch_cython.imageops import set_use_opencv, opencv_available
187
+
188
+ set_use_opencv(False) # force NumPy fallbacks even if OpenCV is installed
189
+ ```
190
+
191
+ ## Package extras
192
+
193
+ **For examples:**
194
+
195
+ ```bash
196
+ pip install patchmatch-cython[visualization]
197
+ ```
198
+
199
+ Adds: `matplotlib`, `Pillow`
200
+
201
+ **For development:**
202
+
203
+ ```bash
204
+ pip install patchmatch-cython[dev]
205
+ ```
206
+
207
+ Adds: `pytest`, `build`, `cibuildwheel`, `setuptools`, `wheel`
208
+
209
+ ## Building
210
+
211
+ To build wheels locally for distribution or development:
212
+
213
+ ### Prerequisites
214
+
215
+ ```bash
216
+ pip install cibuildwheel
217
+ ```
218
+
219
+ ### Build wheels
220
+
221
+ Use the included `local_build.py` script:
222
+
223
+ ```bash
224
+ # Build for all supported Python versions (3.10-3.14)
225
+ python local_build.py
226
+
227
+ # Build for specific Python versions
228
+ python local_build.py --python 3.11 3.12
229
+
230
+ # Build for specific platform
231
+ python local_build.py --platform linux
232
+
233
+ # Build and test wheel installation
234
+ python local_build.py --test
235
+
236
+ # Build without cleaning previous artifacts
237
+ python local_build.py --no-clean
238
+
239
+ # View all options
240
+ python local_build.py --help
241
+ ```
242
+
243
+ ### Build options
244
+
245
+ - `--python`: Specify Python versions to build for (e.g., `3.11 3.12`)
246
+ - `--platform`: Target platform (`auto`, `linux`, `macos`, `windows`)
247
+ - `--test`: Test wheel installation after building
248
+ - `--no-clean`: Skip cleaning build artifacts before building
249
+
250
+ ## Requirements
251
+
252
+ - Python 3.10+
253
+ - NumPy (auto-installed)
254
+ - C++ compiler (for building from source)
@@ -1,4 +1,33 @@
1
1
  #!/usr/bin/env python3
2
+ # BSD 3-Clause License
3
+ #
4
+ # Copyright (c) 2025, Teriks
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
2
31
  """
3
32
  Local wheel building script
4
33
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "patchmatch-cython"
7
- version = "0.2.0"
7
+ version = "1.0.0"
8
8
  description = "High-performance PatchMatch implementation for image inpainting using Cython"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -12,7 +12,7 @@ authors = [
12
12
  ]
13
13
  license = {file = "LICENSE"}
14
14
  classifiers = [
15
- "Development Status :: 4 - Beta",
15
+ "Development Status :: 5 - Production/Stable",
16
16
  "Intended Audience :: Developers",
17
17
  "Intended Audience :: Science/Research",
18
18
  "Programming Language :: Python :: 3",
@@ -1,3 +1,32 @@
1
+ # BSD 3-Clause License
2
+ #
3
+ # Copyright (c) 2025, Teriks
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice, this
9
+ # list of conditions and the following disclaimer.
10
+ #
11
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+ #
15
+ # 3. Neither the name of the copyright holder nor the names of its
16
+ # contributors may be used to endorse or promote products derived from
17
+ # this software without specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
1
30
  from setuptools import setup, Extension, find_packages
2
31
  import numpy as np
3
32
  import os
@@ -39,21 +68,21 @@ def macos_target_archs():
39
68
  def get_compiler_args():
40
69
  """Get platform-appropriate compiler arguments"""
41
70
  if os.name == 'nt': # Windows
42
- return ["/O2"]
71
+ return ["/O2", "/std:c++17", "/EHsc"]
43
72
  elif platform.system() == 'Darwin': # macOS
44
73
  archs = macos_target_archs()
45
74
  # -mcpu=apple-m1 is valid only for arm64. cibuildwheel's x86_64 builds
46
75
  # run on Apple Silicon runners, and passing that flag makes clang fail
47
76
  # with "unsupported option '-mcpu='".
48
77
  if archs == ["arm64"]:
49
- return ["-O3", "-mcpu=apple-m1"]
78
+ return ["-O3", "-std=c++17", "-mcpu=apple-m1"]
50
79
  # Avoid -march=native so wheels remain portable across CPUs.
51
- return ["-O3"]
80
+ return ["-O3", "-std=c++17"]
52
81
  else: # Linux and other Unix-like
53
82
  # Portable default; set PATCHMATCH_NATIVE=1 for local native builds.
54
83
  if os.environ.get("PATCHMATCH_NATIVE") == "1":
55
- return ["-O3", "-march=native"]
56
- return ["-O3"]
84
+ return ["-O3", "-std=c++17", "-march=native"]
85
+ return ["-O3", "-std=c++17"]
57
86
 
58
87
  # Build from Cython source only
59
88
  extensions = [
@@ -63,7 +92,14 @@ extensions = [
63
92
  include_dirs=[np.get_include()],
64
93
  language="c++",
65
94
  extra_compile_args=get_compiler_args(),
66
- )
95
+ ),
96
+ Extension(
97
+ "patchmatch_cython.philippeau",
98
+ [os.path.join("src", "patchmatch_cython", "philippeau.pyx")],
99
+ include_dirs=[np.get_include()],
100
+ language="c++",
101
+ extra_compile_args=get_compiler_args(),
102
+ ),
67
103
  ]
68
104
 
69
105
  ext_modules = cythonize(
@@ -83,7 +119,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
83
119
 
84
120
  setup(
85
121
  name="patchmatch-cython",
86
- version="0.2.0",
122
+ version="1.0.0",
87
123
  author="Teriks",
88
124
  author_email="Teriks999@gmail.com",
89
125
  description="High-performance PatchMatch implementation for image inpainting using Cython",
@@ -94,7 +130,7 @@ setup(
94
130
  package_dir={"": "src"},
95
131
  ext_modules=ext_modules,
96
132
  classifiers=[
97
- "Development Status :: 4 - Beta",
133
+ "Development Status :: 5 - Production/Stable",
98
134
  "Intended Audience :: Developers",
99
135
  "Intended Audience :: Science/Research",
100
136
  "Programming Language :: Python :: 3",