PyPatchMatch 2.0.0b2__tar.gz → 2.0.0rc1__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.0b2 → pypatchmatch-2.0.0rc1}/PKG-INFO +19 -5
  2. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/README.md +18 -4
  3. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/_lib.py +8 -2
  4. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/_version.py +3 -3
  5. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/inpaint.cpp +8 -4
  6. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/nnf.cpp +7 -23
  7. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/nnf.h +5 -31
  8. pypatchmatch-2.0.0rc1/patchmatch/csrc/pyinterface.cpp +214 -0
  9. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/pyinterface.h +6 -0
  10. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/patch_match.py +51 -13
  11. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/pyproject.toml +9 -0
  12. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/scripts/build_opencv.py +17 -0
  13. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/tests/test_e2e.py +2 -3
  14. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/tests/test_patch_match.py +82 -2
  15. pypatchmatch-2.0.0b2/patchmatch/csrc/pyinterface.cpp +0 -132
  16. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/CMakeLists.txt +0 -0
  17. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/LICENSE +0 -0
  18. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/examples/images/forest_pruned.bmp +0 -0
  19. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/__init__.py +0 -0
  20. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/inpaint.h +0 -0
  21. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/masked_image.cpp +0 -0
  22. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/csrc/masked_image.h +0 -0
  23. {pypatchmatch-2.0.0b2 → pypatchmatch-2.0.0rc1}/patchmatch/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPatchMatch
3
- Version: 2.0.0b2
3
+ Version: 2.0.0rc1
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>
@@ -52,6 +52,15 @@ OpenCV installation is needed. On other platforms, e.g. Alpine Linux or Windows
52
52
  pip builds from the source distribution, which requires a C++17 compiler, CMake and the
53
53
  OpenCV development files (e.g. `apt install libopencv-dev` or `brew install opencv`).
54
54
 
55
+ If that build fails, PyPatchMatch is installed without the native library instead of
56
+ blocking the installation: `patchmatch.patchmatch_available` is `False` and the
57
+ inpainting functions raise `RuntimeError`. After installing the missing tools, reinstall
58
+ it without the cached build:
59
+
60
+ ```sh
61
+ pip install --force-reinstall --no-deps --no-cache-dir PyPatchMatch
62
+ ```
63
+
55
64
  ## Usage
56
65
 
57
66
  Python (see
@@ -66,17 +75,22 @@ result = patchmatch.inpaint(image, mask, patch_size=3)
66
75
  ```
67
76
 
68
77
  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
78
+ pure white pixels are treated as holes. Only the holes are filled; all other pixels keep
79
+ their values. The optional keyword argument `global_mask`, in the same format, marks
80
+ pixels that are neither filled nor used as a source; they keep their values as well (see
72
81
  [examples/py_example_global_mask.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example_global_mask.py)).
73
82
  `patchmatch.patchmatch_available` tells whether the native library could be loaded.
74
83
  The previous import path `from patchmatch import patch_match` keeps working.
75
84
 
85
+ `patch_size` is the radius of the compared patches, which span `2 * patch_size + 1`
86
+ pixels in each direction. Larger patches follow larger structures but are much slower;
87
+ the default of 15 compares 31x31 patches, the examples use 3.
88
+
76
89
  `patchmatch.set_random_seed(seed)` sets the seed of the randomized search and
77
90
  `patchmatch.set_verbose(True)` prints the progress of the native code to stderr.
78
91
  `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.
92
+ a regularity map, an HxWx3 float32 array with the regularity coordinates of each pixel
93
+ in its first two channels. Its `guide_weight` must be at least 0.
80
94
 
81
95
  C++ (see
82
96
  [examples/cpp_example.cpp](https://github.com/mauwii/PyPatchMatch/blob/main/examples/cpp_example.cpp),
@@ -22,6 +22,15 @@ OpenCV installation is needed. On other platforms, e.g. Alpine Linux or Windows
22
22
  pip builds from the source distribution, which requires a C++17 compiler, CMake and the
23
23
  OpenCV development files (e.g. `apt install libopencv-dev` or `brew install opencv`).
24
24
 
25
+ If that build fails, PyPatchMatch is installed without the native library instead of
26
+ blocking the installation: `patchmatch.patchmatch_available` is `False` and the
27
+ inpainting functions raise `RuntimeError`. After installing the missing tools, reinstall
28
+ it without the cached build:
29
+
30
+ ```sh
31
+ pip install --force-reinstall --no-deps --no-cache-dir PyPatchMatch
32
+ ```
33
+
25
34
  ## Usage
26
35
 
27
36
  Python (see
@@ -36,17 +45,22 @@ result = patchmatch.inpaint(image, mask, patch_size=3)
36
45
  ```
37
46
 
38
47
  The mask must have the same height and width as the image. If `mask` is omitted, all
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
48
+ pure white pixels are treated as holes. Only the holes are filled; all other pixels keep
49
+ their values. The optional keyword argument `global_mask`, in the same format, marks
50
+ pixels that are neither filled nor used as a source; they keep their values as well (see
42
51
  [examples/py_example_global_mask.py](https://github.com/mauwii/PyPatchMatch/blob/main/examples/py_example_global_mask.py)).
43
52
  `patchmatch.patchmatch_available` tells whether the native library could be loaded.
44
53
  The previous import path `from patchmatch import patch_match` keeps working.
45
54
 
55
+ `patch_size` is the radius of the compared patches, which span `2 * patch_size + 1`
56
+ pixels in each direction. Larger patches follow larger structures but are much slower;
57
+ the default of 15 compares 31x31 patches, the examples use 3.
58
+
46
59
  `patchmatch.set_random_seed(seed)` sets the seed of the randomized search and
47
60
  `patchmatch.set_verbose(True)` prints the progress of the native code to stderr.
48
61
  `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.
62
+ a regularity map, an HxWx3 float32 array with the regularity coordinates of each pixel
63
+ in its first two channels. Its `guide_weight` must be at least 0.
50
64
 
51
65
  C++ (see
52
66
  [examples/cpp_example.cpp](https://github.com/mauwii/PyPatchMatch/blob/main/examples/cpp_example.cpp),
@@ -80,9 +80,13 @@ def find_library() -> Path:
80
80
  candidate = Path(directory) / LIBRARY_NAME
81
81
  if candidate.is_file():
82
82
  return candidate
83
+ # Without a wheel for the platform, a failed build from the sdist installs the
84
+ # package without the library, see the overrides in pyproject.toml.
83
85
  raise OSError(
84
- f"{LIBRARY_NAME} not found in {list(package.__path__)}; "
85
- "reinstall PyPatchMatch or build it from source with OpenCV available"
86
+ f"{LIBRARY_NAME} not found in {list(package.__path__)}. Without a wheel for "
87
+ "this platform, PyPatchMatch is built from source, which needs a C++ "
88
+ "compiler, CMake and OpenCV. Install them and reinstall it without the "
89
+ "cache: pip install --force-reinstall --no-deps --no-cache-dir pypatchmatch"
86
90
  )
87
91
 
88
92
 
@@ -95,6 +99,8 @@ def load_library() -> ctypes.CDLL:
95
99
  lib.PM_set_verbose.restype = None
96
100
  lib.PM_free_pymat.argtypes = [CMatT]
97
101
  lib.PM_free_pymat.restype = None
102
+ lib.PM_last_error.argtypes = []
103
+ lib.PM_last_error.restype = ctypes.c_char_p
98
104
 
99
105
  lib.PM_inpaint.argtypes = [CMatT, CMatT, ctypes.c_int]
100
106
  lib.PM_inpaint2.argtypes = [CMatT, CMatT, CMatT, ctypes.c_int]
@@ -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.0b2'
22
- __version_tuple__ = version_tuple = (2, 0, 0, 'b2')
21
+ __version__ = version = '2.0.0rc1'
22
+ __version_tuple__ = version_tuple = (2, 0, 0, 'rc1')
23
23
 
24
- __commit_id__ = commit_id = 'gd8a13321b'
24
+ __commit_id__ = commit_id = 'g75098309c'
@@ -44,7 +44,6 @@ namespace
44
44
  auto source_ptr = source.get_image(ys, xs);
45
45
  auto target_ptr = target.ptr<double>(yt, xt);
46
46
 
47
- #pragma unroll
48
47
  for (int c = 0; c < 3; ++c)
49
48
  target_ptr[c] += static_cast<double>(source_ptr[c]) * weight;
50
49
  target_ptr[3] += weight;
@@ -126,11 +125,16 @@ cv::Mat Inpainting::run(bool verbose, bool verbose_visualize, unsigned int rando
126
125
  target = _expectation_maximization(source, target, level, verbose);
127
126
  }
128
127
 
129
- // Globally masked pixels are neither filled nor used as a source, so the pyramid
130
- // leaves them black. Keep their input values instead.
128
+ // Only the holes are filled. The votes of overlapping patches also change the known
129
+ // pixels near the holes, and the pyramid leaves globally masked pixels black, which
130
+ // are neither filled nor used as a source. Keep the input values of both.
131
131
  cv::Mat result = target.image();
132
+ if (result.empty()) // OpenCV rejects empty operands in matrix expressions
133
+ return result;
134
+ cv::Mat keep = m_initial.mask() == 0;
132
135
  if (!m_initial.global_mask().empty())
133
- m_initial.image().copyTo(result, m_initial.global_mask());
136
+ keep |= m_initial.global_mask() != 0;
137
+ m_initial.image().copyTo(result, keep);
134
138
  return result;
135
139
  }
136
140
 
@@ -287,27 +287,6 @@ int PatchSSDDistanceMetric::operator()(const MaskedImage &source, int source_y,
287
287
  return distance_masked_images(source, source_y, source_x, target, target_y, target_x, m_patch_size);
288
288
  }
289
289
 
290
- int DebugPatchSSDDistanceMetric::operator()(const MaskedImage &source, int source_y, int source_x, const MaskedImage &target, int target_y, int target_x) const
291
- {
292
- fprintf(stderr, "DebugPatchSSDDistanceMetric: %d %d %d %d\n", source.size().width, source.size().height, m_width, m_height);
293
- return distance_masked_images(source, source_y, source_x, target, target_y, target_x, m_patch_size);
294
- }
295
-
296
- int RegularityGuidedPatchDistanceMetricV1::operator()(const MaskedImage &source, int source_y, int source_x, const MaskedImage &target, int target_y, int target_x) const
297
- {
298
- double dx = remainder(double(source_x - target_x) / source.size().width, m_dx1);
299
- double dy = remainder(double(source_y - target_y) / source.size().height, m_dy2);
300
-
301
- double score1 = sqrt(dx * dx + dy * dy) / m_scale;
302
- if (score1 < 0 || score1 > 1)
303
- score1 = 1;
304
- score1 *= PatchDistanceMetric::kDistanceScale;
305
-
306
- double score2 = distance_masked_images(source, source_y, source_x, target, target_y, target_x, m_patch_size);
307
- double score = score1 * m_weight + score2 / (1 + m_weight);
308
- return static_cast<int>(score / (1 + m_weight));
309
- }
310
-
311
290
  int RegularityGuidedPatchDistanceMetricV2::operator()(const MaskedImage &source, int source_y, int source_x, const MaskedImage &target, int target_y, int target_x) const
312
291
  {
313
292
  if (target_y < 0 || target_y >= target.size().height || target_x < 0 || target_x >= target.size().width)
@@ -338,6 +317,11 @@ int RegularityGuidedPatchDistanceMetricV2::operator()(const MaskedImage &source,
338
317
  }
339
318
 
340
319
  double score2 = distance_masked_images(source, source_y, source_x, target, target_y, target_x, m_patch_size);
341
- double score = score1 * m_weight + score2;
342
- return int(score / (1 + m_weight));
320
+ double score = (score1 * m_weight + score2) / (1 + m_weight);
321
+ // The distance indexes the similarity table in Inpainting, so keep it in range.
322
+ if (!(score > 0))
323
+ return 0;
324
+ if (score >= PatchDistanceMetric::kDistanceScale)
325
+ return PatchDistanceMetric::kDistanceScale;
326
+ return static_cast<int>(score);
343
327
  }
@@ -1,7 +1,6 @@
1
1
  #pragma once
2
2
 
3
- #include <cassert>
4
- #include <cmath>
3
+ #include <stdexcept>
5
4
  #include <opencv2/core.hpp>
6
5
  #include "masked_image.h"
7
6
 
@@ -116,44 +115,19 @@ public:
116
115
  static const int kSSDScale;
117
116
  };
118
117
 
119
- class DebugPatchSSDDistanceMetric : public PatchDistanceMetric
120
- {
121
- public:
122
- DebugPatchSSDDistanceMetric(int patch_size, int width, int height) : PatchDistanceMetric(patch_size), m_width(width), m_height(height) {}
123
- virtual int operator()(const MaskedImage &source, int source_y, int source_x, const MaskedImage &target, int target_y, int target_x) const;
124
-
125
- protected:
126
- int m_width, m_height;
127
- };
128
-
129
- class RegularityGuidedPatchDistanceMetricV1 : public PatchDistanceMetric
130
- {
131
- public:
132
- RegularityGuidedPatchDistanceMetricV1(int patch_size, double dx1, double dy1, double dx2, double dy2, double weight)
133
- : PatchDistanceMetric(patch_size), m_dx1(dx1), m_dy1(dy1), m_dx2(dx2), m_dy2(dy2), m_weight(weight)
134
- {
135
-
136
- assert(m_dy1 == 0);
137
- assert(m_dx2 == 0);
138
- m_scale = sqrt(m_dx1 * m_dx1 + m_dy2 * m_dy2) / 4;
139
- }
140
- virtual int operator()(const MaskedImage &source, int source_y, int source_x, const MaskedImage &target, int target_y, int target_x) const;
141
-
142
- protected:
143
- double m_dx1, m_dy1, m_dx2, m_dy2;
144
- double m_scale, m_weight;
145
- };
146
-
147
118
  class RegularityGuidedPatchDistanceMetricV2 : public PatchDistanceMetric
148
119
  {
149
120
  public:
150
121
  RegularityGuidedPatchDistanceMetricV2(int patch_size, cv::Mat ijmap, double weight)
151
122
  : PatchDistanceMetric(patch_size), m_ijmap(ijmap), m_weight(weight)
152
123
  {
124
+ // The distance is divided by 1 + weight, so a negative weight flips its sign.
125
+ if (!(weight >= 0))
126
+ throw std::invalid_argument("the guide weight must be >= 0");
153
127
  }
154
128
  virtual int operator()(const MaskedImage &source, int source_y, int source_x, const MaskedImage &target, int target_y, int target_x) const;
155
129
 
156
130
  protected:
157
131
  cv::Mat m_ijmap;
158
- double m_width, m_height, m_weight;
132
+ double m_weight;
159
133
  };
@@ -0,0 +1,214 @@
1
+ #include <atomic>
2
+ #include <cstring>
3
+ #include <exception>
4
+ #include <memory>
5
+ #include <string>
6
+
7
+ #include "pyinterface.h"
8
+ #include "inpaint.h"
9
+
10
+ int _dtype_py_to_cv(int dtype_py);
11
+ int _dtype_cv_to_py(int dtype_cv);
12
+ cv::Mat _py_to_cv2(PM_mat_t pymat);
13
+ PM_mat_t _cv2_to_py(cv::Mat cvmat);
14
+
15
+ namespace
16
+ {
17
+ // The settings are set by one thread while others may be inpainting, since ctypes
18
+ // releases the GIL.
19
+ std::atomic<unsigned int> &random_seed()
20
+ {
21
+ static std::atomic value{1212u};
22
+ return value;
23
+ }
24
+
25
+ std::atomic<bool> &verbose()
26
+ {
27
+ static std::atomic value{false};
28
+ return value;
29
+ }
30
+
31
+ // Message of the last failed call on the calling thread, see PM_last_error.
32
+ std::string &last_error()
33
+ {
34
+ thread_local std::string message;
35
+ return message;
36
+ }
37
+
38
+ void set_last_error(const char *message) noexcept
39
+ {
40
+ try
41
+ {
42
+ last_error() = message;
43
+ }
44
+ catch (...)
45
+ {
46
+ last_error().clear();
47
+ }
48
+ }
49
+
50
+ // A C++ exception must not propagate through the C interface into ctypes, which
51
+ // would abort the process. Report it with an empty result instead.
52
+ template <typename Func>
53
+ PM_mat_t guarded(Func &&func) noexcept
54
+ {
55
+ try
56
+ {
57
+ return func();
58
+ }
59
+ catch (const std::exception &e)
60
+ {
61
+ set_last_error(e.what());
62
+ }
63
+ catch (...)
64
+ {
65
+ set_last_error("unknown error");
66
+ }
67
+ return PM_mat_t{nullptr, {0, 0, 0}, 0};
68
+ }
69
+ }
70
+
71
+ const char *PM_last_error()
72
+ {
73
+ return last_error().c_str();
74
+ }
75
+
76
+ void PM_set_random_seed(unsigned int seed)
77
+ {
78
+ random_seed().store(seed);
79
+ }
80
+
81
+ void PM_set_verbose(int value)
82
+ {
83
+ verbose().store(value != 0);
84
+ }
85
+
86
+ void PM_free_pymat(PM_mat_t pymat)
87
+ {
88
+ // Takes back the ownership of the buffer that _cv2_to_py released. The buffer
89
+ // crosses the C interface as a plain pointer, so it cannot be a std::vector
90
+ // (SonarCloud cpp:S5945).
91
+ std::unique_ptr<unsigned char[]> data(static_cast<unsigned char *>(pymat.data_ptr)); // NOSONAR
92
+ }
93
+
94
+ PM_mat_t PM_inpaint(PM_mat_t source_py, PM_mat_t mask_py, int patch_size)
95
+ {
96
+ return guarded([source_py, mask_py, patch_size] {
97
+ cv::Mat source = _py_to_cv2(source_py);
98
+ cv::Mat mask = _py_to_cv2(mask_py);
99
+ auto metric = PatchSSDDistanceMetric(patch_size);
100
+ cv::Mat result = Inpainting(source, mask, &metric).run(verbose().load(), false, random_seed().load());
101
+ return _cv2_to_py(result);
102
+ });
103
+ }
104
+
105
+ PM_mat_t PM_inpaint_regularity(PM_mat_t source_py, PM_mat_t mask_py, PM_mat_t ijmap_py, int patch_size, float guide_weight)
106
+ {
107
+ return guarded([source_py, mask_py, ijmap_py, patch_size, guide_weight] {
108
+ cv::Mat source = _py_to_cv2(source_py);
109
+ cv::Mat mask = _py_to_cv2(mask_py);
110
+ cv::Mat ijmap = _py_to_cv2(ijmap_py);
111
+
112
+ auto metric = RegularityGuidedPatchDistanceMetricV2(patch_size, ijmap, guide_weight);
113
+ cv::Mat result = Inpainting(source, mask, &metric).run(verbose().load(), false, random_seed().load());
114
+ return _cv2_to_py(result);
115
+ });
116
+ }
117
+
118
+ PM_mat_t PM_inpaint2(PM_mat_t source_py, PM_mat_t mask_py, PM_mat_t global_mask_py, int patch_size)
119
+ {
120
+ return guarded([source_py, mask_py, global_mask_py, patch_size] {
121
+ cv::Mat source = _py_to_cv2(source_py);
122
+ cv::Mat mask = _py_to_cv2(mask_py);
123
+ cv::Mat global_mask = _py_to_cv2(global_mask_py);
124
+
125
+ auto metric = PatchSSDDistanceMetric(patch_size);
126
+ cv::Mat result = Inpainting(source, mask, global_mask, &metric).run(verbose().load(), false, random_seed().load());
127
+ return _cv2_to_py(result);
128
+ });
129
+ }
130
+
131
+ PM_mat_t PM_inpaint2_regularity(PM_mat_t source_py, PM_mat_t mask_py, PM_mat_t global_mask_py, PM_mat_t ijmap_py, int patch_size, float guide_weight)
132
+ {
133
+ return guarded([source_py, mask_py, global_mask_py, ijmap_py, patch_size, guide_weight] {
134
+ cv::Mat source = _py_to_cv2(source_py);
135
+ cv::Mat mask = _py_to_cv2(mask_py);
136
+ cv::Mat global_mask = _py_to_cv2(global_mask_py);
137
+ cv::Mat ijmap = _py_to_cv2(ijmap_py);
138
+
139
+ auto metric = RegularityGuidedPatchDistanceMetricV2(patch_size, ijmap, guide_weight);
140
+ cv::Mat result = Inpainting(source, mask, global_mask, &metric).run(verbose().load(), false, random_seed().load());
141
+ return _cv2_to_py(result);
142
+ });
143
+ }
144
+
145
+ int _dtype_py_to_cv(int dtype_py)
146
+ {
147
+ switch (dtype_py)
148
+ {
149
+ case PM_UINT8:
150
+ return CV_8U;
151
+ case PM_INT8:
152
+ return CV_8S;
153
+ case PM_UINT16:
154
+ return CV_16U;
155
+ case PM_INT16:
156
+ return CV_16S;
157
+ case PM_INT32:
158
+ return CV_32S;
159
+ case PM_FLOAT32:
160
+ return CV_32F;
161
+ case PM_FLOAT64:
162
+ return CV_64F;
163
+ }
164
+
165
+ return CV_8U;
166
+ }
167
+
168
+ int _dtype_cv_to_py(int dtype_cv)
169
+ {
170
+ switch (dtype_cv)
171
+ {
172
+ case CV_8U:
173
+ return PM_UINT8;
174
+ case CV_8S:
175
+ return PM_INT8;
176
+ case CV_16U:
177
+ return PM_UINT16;
178
+ case CV_16S:
179
+ return PM_INT16;
180
+ case CV_32S:
181
+ return PM_INT32;
182
+ case CV_32F:
183
+ return PM_FLOAT32;
184
+ case CV_64F:
185
+ return PM_FLOAT64;
186
+ }
187
+
188
+ return PM_UINT8;
189
+ }
190
+
191
+ cv::Mat _py_to_cv2(PM_mat_t pymat)
192
+ {
193
+ int dtype = CV_MAKETYPE(_dtype_py_to_cv(pymat.dtype), pymat.shape.channels);
194
+ return cv::Mat(cv::Size(pymat.shape.width, pymat.shape.height), dtype, pymat.data_ptr).clone();
195
+ }
196
+
197
+ PM_mat_t _cv2_to_py(cv::Mat cvmat)
198
+ {
199
+ if (!cvmat.isContinuous())
200
+ cvmat = cvmat.clone();
201
+
202
+ PM_shape_t shape = {cvmat.size().width, cvmat.size().height, cvmat.channels()};
203
+ int dtype = _dtype_cv_to_py(cvmat.depth());
204
+ size_t dsize = cvmat.total() * cvmat.elemSize();
205
+
206
+ // A null pointer reports an error to Python, so allocate at least one byte. A failed
207
+ // allocation throws std::bad_alloc, which guarded() reports.
208
+ auto data = std::make_unique<unsigned char[]>(dsize > 0 ? dsize : 1);
209
+ if (dsize > 0)
210
+ std::memcpy(data.get(), cvmat.data, dsize);
211
+
212
+ // Python passes the buffer back to PM_free_pymat, which frees it.
213
+ return PM_mat_t{data.release(), shape, dtype};
214
+ }
@@ -1,3 +1,5 @@
1
+ #pragma once
2
+
1
3
  #include <opencv2/core.hpp>
2
4
  #include <cstdlib>
3
5
  #include <cstdio>
@@ -29,6 +31,10 @@ extern "C"
29
31
  int dtype;
30
32
  };
31
33
 
34
+ // The PM_inpaint* functions return a null data_ptr on failure; this returns the
35
+ // error message of the last failure on the calling thread.
36
+ const char *PM_last_error(void);
37
+
32
38
  void PM_set_random_seed(unsigned int seed);
33
39
  void PM_set_verbose(int value);
34
40
 
@@ -17,6 +17,8 @@ from __future__ import annotations
17
17
 
18
18
  import ctypes
19
19
  import logging
20
+ import math
21
+ import operator
20
22
  from collections.abc import Callable
21
23
  from typing import TypeAlias
22
24
 
@@ -47,6 +49,9 @@ except OSError as e:
47
49
  # True if the native library was loaded successfully.
48
50
  patchmatch_available = _lib is not None
49
51
 
52
+ # The native code computes 2 * patch_size + 1 as a C int.
53
+ _MAX_PATCH_SIZE = (2**31 - 2) // 2
54
+
50
55
 
51
56
  def _get_lib() -> ctypes.CDLL:
52
57
  if _lib is None:
@@ -78,13 +83,17 @@ def inpaint(
78
83
  white pixels (255, 255, 255) are treated as holes.
79
84
  global_mask: mask like ``mask`` of pixels that are neither filled nor used as
80
85
  a source; they keep their values from ``image``.
81
- patch_size: patch size for the inpainting algorithm.
86
+ patch_size: radius of the compared patches, which span
87
+ ``(2 * patch_size + 1) ** 2`` pixels. Larger patches follow larger
88
+ structures but are much slower; the examples use 3.
82
89
 
83
90
  Returns:
84
- The repaired image, with the same shape as ``image``.
91
+ The repaired image, with the same shape as ``image``. Only the holes are
92
+ filled; all other pixels keep their values from ``image``.
85
93
  """
86
94
  lib = _get_lib()
87
- image, mask, global_mask = _prepare_inputs(image, mask, global_mask, patch_size)
95
+ patch_size = _check_patch_size(patch_size)
96
+ image, mask, global_mask = _prepare_inputs(image, mask, global_mask)
88
97
 
89
98
  if global_mask is None:
90
99
  return _call(lib.PM_inpaint, image, mask, ctypes.c_int(patch_size))
@@ -103,12 +112,15 @@ def inpaint_regularity(
103
112
  """Like :func:`inpaint`, additionally guided by a regularity map.
104
113
 
105
114
  Args:
106
- ijmap: HxWx3 float32 array with the regularity coordinates of each pixel;
107
- a map of a different size is scaled to the image.
108
- guide_weight: weight of the regularity term relative to the patch distance.
115
+ ijmap: HxWx3 float32 array of finite values with the regularity coordinates
116
+ of each pixel in the first two channels; the third channel is unused.
117
+ A map of a different size is scaled to the image.
118
+ guide_weight: non-negative weight of the regularity term relative to the
119
+ patch distance.
109
120
  """
110
121
  lib = _get_lib()
111
- image, mask, global_mask = _prepare_inputs(image, mask, global_mask, patch_size)
122
+ patch_size = _check_patch_size(patch_size)
123
+ image, mask, global_mask = _prepare_inputs(image, mask, global_mask)
112
124
 
113
125
  if not (
114
126
  isinstance(ijmap, np.ndarray)
@@ -118,25 +130,46 @@ def inpaint_regularity(
118
130
  and ijmap.size > 0
119
131
  ):
120
132
  raise ValueError("ijmap must be a non-empty HxWx3 float32 array")
133
+ # NaN and infinity turn into out-of-range patch distances in the native code
134
+ if not np.isfinite(ijmap).all():
135
+ raise ValueError("ijmap must only contain finite values")
121
136
  ijmap = np.ascontiguousarray(ijmap)
122
137
 
138
+ # the native code divides by 1 + guide_weight and indexes a table with the result
139
+ guide_weight = float(guide_weight)
140
+ if not (math.isfinite(guide_weight) and guide_weight >= 0):
141
+ raise ValueError(
142
+ f"guide_weight must be a finite number >= 0, got {guide_weight}"
143
+ )
144
+
123
145
  args = (ijmap, ctypes.c_int(patch_size), ctypes.c_float(guide_weight))
124
146
  if global_mask is None:
125
147
  return _call(lib.PM_inpaint_regularity, image, mask, *args)
126
148
  return _call(lib.PM_inpaint2_regularity, image, mask, global_mask, *args)
127
149
 
128
150
 
151
+ def _check_patch_size(patch_size: int) -> int:
152
+ try:
153
+ patch_size = operator.index(patch_size)
154
+ except TypeError:
155
+ raise TypeError(
156
+ f"patch_size must be an integer, got {type(patch_size).__name__}"
157
+ ) from None
158
+ # The native code crashes for 0 and never terminates for negative sizes. ctypes
159
+ # silently wraps larger values into the C int range, e.g. 2**31 to a negative size.
160
+ if not 1 <= patch_size <= _MAX_PATCH_SIZE:
161
+ raise ValueError(
162
+ f"patch_size must be between 1 and {_MAX_PATCH_SIZE}, got {patch_size}"
163
+ )
164
+ return patch_size
165
+
166
+
129
167
  def _prepare_inputs(
130
168
  image: ImageLike,
131
169
  mask: ImageLike | None,
132
170
  global_mask: ImageLike | None,
133
- patch_size: int,
134
171
  ) -> tuple[np.ndarray, np.ndarray, np.ndarray | None]:
135
172
  """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
-
140
173
  image = _canonize_image_array(image)
141
174
  mask = _default_mask(image) if mask is None else _canonize_mask_array(mask)
142
175
  if global_mask is not None:
@@ -154,12 +187,17 @@ def _prepare_inputs(
154
187
 
155
188
  def _call(func: Callable[..., CMatT], *args: object) -> np.ndarray:
156
189
  """Call ``func`` with arrays converted to pymats and copy the result."""
190
+ lib = _get_lib()
157
191
  c_args = [np_to_pymat(a) if isinstance(a, np.ndarray) else a for a in args]
158
192
  ret = func(*c_args)
193
+ if not ret.data_ptr:
194
+ # the error message is kept per thread, like the call itself
195
+ message = lib.PM_last_error().decode(errors="replace").strip()
196
+ raise RuntimeError(f"patchmatch failed: {message}")
159
197
  try:
160
198
  return pymat_to_np(ret)
161
199
  finally:
162
- _get_lib().PM_free_pymat(ret)
200
+ lib.PM_free_pymat(ret)
163
201
 
164
202
 
165
203
  def _canonize_image_array(image: ImageLike) -> np.ndarray:
@@ -59,6 +59,15 @@ metadata.version.provider = "scikit_build_core.metadata.setuptools_scm"
59
59
  sdist.include = ["examples/images/forest_pruned.bmp", "patchmatch/_version.py"]
60
60
  sdist.exclude = [".*", "examples", "uv.lock"]
61
61
 
62
+ # Without a matching wheel, pip builds the sdist, which needs a C++ compiler, CMake and
63
+ # OpenCV. If that fails, build a wheel without the native library instead of failing
64
+ # the installation; patchmatch_available is then False, as with 1.x. Builds from the
65
+ # source tree still fail, and cibuildwheel rejects the pure wheel of a failed build.
66
+ [[tool.scikit-build.overrides]]
67
+ if.from-sdist = true
68
+ if.failed = true
69
+ wheel.cmake = false
70
+
62
71
  [tool.setuptools_scm]
63
72
  version_file = "patchmatch/_version.py"
64
73
  # used when building without git metadata, e.g. from a GitHub source archive
@@ -6,6 +6,7 @@ wheels small and free of OpenCV's GUI/codec dependencies.
6
6
  Usage: python build_opencv.py (installs into $OpenCV_ROOT)
7
7
  """
8
8
 
9
+ import hashlib
9
10
  import os
10
11
  import shutil
11
12
  import subprocess
@@ -19,6 +20,9 @@ OPENCV_VERSION = "4.14.0"
19
20
  OPENCV_URL = (
20
21
  f"https://github.com/opencv/opencv/archive/refs/tags/{OPENCV_VERSION}.tar.gz"
21
22
  )
23
+ # The wheels link this code statically, so the archive is pinned by its SHA-256.
24
+ # Update it together with the version, e.g. with `sha256sum` or `shasum -a 256`.
25
+ OPENCV_SHA256 = "ee8fb9b30eb60850431b4656447080e3737b56e45719c92b67f245950609f86e"
22
26
 
23
27
  CMAKE_OPTIONS = {
24
28
  "CMAKE_BUILD_TYPE": "Release",
@@ -87,6 +91,18 @@ def find_cmake() -> str:
87
91
  return str(scripts / ("cmake.exe" if os.name == "nt" else "cmake"))
88
92
 
89
93
 
94
+ def verify_archive(archive: Path) -> None:
95
+ sha256 = hashlib.sha256()
96
+ with archive.open("rb") as f:
97
+ for chunk in iter(lambda: f.read(1 << 20), b""):
98
+ sha256.update(chunk)
99
+ if sha256.hexdigest() != OPENCV_SHA256:
100
+ sys.exit(
101
+ f"SHA-256 mismatch for {OPENCV_URL}: "
102
+ f"expected {OPENCV_SHA256}, got {sha256.hexdigest()}"
103
+ )
104
+
105
+
90
106
  def main() -> None:
91
107
  # OpenCV_ROOT is the variable CMake's find_package(OpenCV) looks for.
92
108
  root = os.environ.get("OpenCV_ROOT") # noqa: SIM112
@@ -102,6 +118,7 @@ def main() -> None:
102
118
  archive = Path(tmp) / "opencv.tar.gz"
103
119
  print(f"Downloading {OPENCV_URL}", flush=True)
104
120
  urllib.request.urlretrieve(OPENCV_URL, archive)
121
+ verify_archive(archive)
105
122
  with tarfile.open(archive) as tar:
106
123
  # extraction filters are missing on older patch releases, e.g. the
107
124
  # last Windows installer of Python 3.10 (3.10.11)
@@ -41,9 +41,8 @@ def assert_plausible_fill(
41
41
 
42
42
  assert not white_pixels(result)[holes].any(), "holes were not filled"
43
43
 
44
- # the known part of the image is reconstructed almost unchanged
45
- outside = np.abs(result[~holes].astype(float) - source[~holes]).mean()
46
- assert outside < 1, outside
44
+ # only the holes are filled
45
+ np.testing.assert_array_equal(result[~holes], source[~holes])
47
46
 
48
47
  # the filling blends in with its surroundings
49
48
  band = surrounding(holes) & ~excluded
@@ -45,9 +45,13 @@ def seed():
45
45
 
46
46
 
47
47
  def assert_filled(result: np.ndarray, source: np.ndarray) -> None:
48
+ """The hole is filled and all other pixels keep their values."""
48
49
  assert result.shape == source.shape
49
50
  assert result.dtype == np.uint8
50
51
  assert not (result[HOLE] == 255).all(axis=-1).any()
52
+ known = np.ones(source.shape[:2], dtype=bool)
53
+ known[HOLE] = False
54
+ np.testing.assert_array_equal(result[known], source[known])
51
55
 
52
56
 
53
57
  # --- package ----------------------------------------------------------------
@@ -79,6 +83,8 @@ def test_inpaint_explicit_mask(image, hole_mask):
79
83
  result = patchmatch.inpaint(image, hole_mask, patch_size=3)
80
84
  assert result.shape == image.shape
81
85
  assert (result[HOLE] != 0).any()
86
+ known = hole_mask == 0
87
+ np.testing.assert_array_equal(result[known], image[known])
82
88
 
83
89
 
84
90
  def test_inpaint_global_mask(image, hole_mask):
@@ -90,6 +96,14 @@ def test_inpaint_global_mask(image, hole_mask):
90
96
  np.testing.assert_array_equal(result[:8], image[:8])
91
97
 
92
98
 
99
+ @pytest.mark.parametrize("shape", [(0, 0, 3), (0, 10, 3), (10, 0, 3)])
100
+ def test_inpaint_empty_image(shape):
101
+ empty = np.zeros(shape, dtype=np.uint8)
102
+ global_mask = np.zeros(shape[:2], dtype=np.uint8)
103
+ assert patchmatch.inpaint(empty, patch_size=3).shape == shape
104
+ assert patchmatch.inpaint(empty, global_mask=global_mask).shape == shape
105
+
106
+
93
107
  @pytest.mark.parametrize("patch_size", [1, 3, 7])
94
108
  def test_inpaint_patch_sizes(image, patch_size):
95
109
  assert_filled(patchmatch.inpaint(image, patch_size=patch_size), image)
@@ -243,7 +257,8 @@ def test_mask_size_must_match_image(image, ijmap, func, argument):
243
257
  func(image, **kwargs)
244
258
 
245
259
 
246
- @pytest.mark.parametrize("patch_size", [0, -1])
260
+ # 2**31 wrapped to a negative C int, which never terminated
261
+ @pytest.mark.parametrize("patch_size", [0, -1, 2**30, 2**31])
247
262
  @pytest.mark.parametrize(
248
263
  "func",
249
264
  [patchmatch.inpaint, patchmatch.inpaint_regularity],
@@ -255,6 +270,18 @@ def test_invalid_patch_size(image, ijmap, func, patch_size):
255
270
  func(image, None, patch_size=patch_size, **kwargs)
256
271
 
257
272
 
273
+ def test_patch_size_must_be_an_integer(image):
274
+ with pytest.raises(TypeError, match="patch_size must be an integer"):
275
+ patchmatch.inpaint(image, patch_size=3.0)
276
+
277
+
278
+ def test_patch_size_accepts_numpy_integers(image):
279
+ expected = patchmatch.inpaint(image, patch_size=3)
280
+ np.testing.assert_array_equal(
281
+ patchmatch.inpaint(image, patch_size=np.int64(3)), expected
282
+ )
283
+
284
+
258
285
  @pytest.mark.parametrize(
259
286
  "bad_ijmap",
260
287
  [
@@ -262,14 +289,46 @@ def test_invalid_patch_size(image, ijmap, func, patch_size):
262
289
  np.zeros((HEIGHT, WIDTH, 2), dtype=np.float32),
263
290
  np.zeros((0, WIDTH, 3), dtype=np.float32),
264
291
  [[[0.0, 0.0, 0.0]]],
292
+ np.full((HEIGHT, WIDTH, 3), np.nan, dtype=np.float32),
293
+ np.full((HEIGHT, WIDTH, 3), np.inf, dtype=np.float32),
265
294
  ],
266
- ids=["float64", "2-channel", "empty", "list"],
295
+ ids=["float64", "2-channel", "empty", "list", "nan", "inf"],
267
296
  )
268
297
  def test_inpaint_regularity_invalid_ijmap(image, bad_ijmap):
269
298
  with pytest.raises(ValueError, match="ijmap"):
270
299
  patchmatch.inpaint_regularity(image, None, bad_ijmap)
271
300
 
272
301
 
302
+ @pytest.mark.parametrize("guide_weight", [-1.0, -0.5, float("nan"), float("inf")])
303
+ def test_inpaint_regularity_invalid_guide_weight(image, ijmap, guide_weight):
304
+ # a weight of -1 used to crash the native code, other negative weights made it
305
+ # read outside of the similarity table
306
+ with pytest.raises(ValueError, match="guide_weight"):
307
+ patchmatch.inpaint_regularity(image, None, ijmap, guide_weight=guide_weight)
308
+
309
+
310
+ # --- native errors ----------------------------------------------------------
311
+
312
+
313
+ def test_native_exception_raises_runtime_error():
314
+ # A null data pointer with a non-empty shape fails an OpenCV assertion. The
315
+ # exception must not propagate through the C interface and abort the process.
316
+ lib = patch_match._get_lib()
317
+ bad = _lib.CMatT(None, _lib.CShapeT(4, 4, 3), 0)
318
+ args = (bad, bad, ctypes.c_int(3))
319
+ with pytest.raises(RuntimeError, match=r"patchmatch failed: .*Assertion failed"):
320
+ patch_match._call(lib.PM_inpaint, *args)
321
+
322
+
323
+ def test_native_rejects_negative_guide_weight(image, hole_mask, ijmap):
324
+ """The C++ metric checks the weight as well, for callers that bypass Python."""
325
+ lib = patch_match._get_lib()
326
+ mask = hole_mask[..., np.newaxis]
327
+ args = (image, mask, ijmap, ctypes.c_int(3), ctypes.c_float(-1))
328
+ with pytest.raises(RuntimeError, match="guide weight must be >= 0"):
329
+ patch_match._call(lib.PM_inpaint_regularity, *args)
330
+
331
+
273
332
  # --- missing native library -------------------------------------------------
274
333
 
275
334
 
@@ -297,6 +356,27 @@ def test_find_library_reports_missing_file(monkeypatch):
297
356
  _lib.find_library()
298
357
 
299
358
 
359
+ def test_import_without_native_library():
360
+ """A failed build from the sdist installs the package without the library.
361
+
362
+ The import has to work and explain how to fix it, see the overrides in
363
+ pyproject.toml. The module is reloaded in a separate process, so the other
364
+ tests keep the loaded library.
365
+ """
366
+ code = (
367
+ "import importlib\n"
368
+ "from patchmatch import _lib, patch_match\n"
369
+ "_lib.LIBRARY_NAME = 'missing-library'\n"
370
+ "importlib.reload(patch_match)\n"
371
+ "assert not patch_match.patchmatch_available\n"
372
+ )
373
+ process = subprocess.run(
374
+ [sys.executable, "-c", code], capture_output=True, text=True, check=True
375
+ )
376
+ assert "missing-library not found" in process.stderr
377
+ assert "--no-cache-dir" in process.stderr
378
+
379
+
300
380
  # --- ctypes conversion ------------------------------------------------------
301
381
 
302
382
 
@@ -1,132 +0,0 @@
1
- #include "pyinterface.h"
2
- #include "inpaint.h"
3
-
4
- static unsigned int PM_seed = 1212;
5
- static bool PM_verbose = false;
6
-
7
- int _dtype_py_to_cv(int dtype_py);
8
- int _dtype_cv_to_py(int dtype_cv);
9
- cv::Mat _py_to_cv2(PM_mat_t pymat);
10
- PM_mat_t _cv2_to_py(cv::Mat cvmat);
11
-
12
- void PM_set_random_seed(unsigned int seed)
13
- {
14
- PM_seed = seed;
15
- }
16
-
17
- void PM_set_verbose(int value)
18
- {
19
- PM_verbose = static_cast<bool>(value);
20
- }
21
-
22
- void PM_free_pymat(PM_mat_t pymat)
23
- {
24
- free(pymat.data_ptr);
25
- }
26
-
27
- PM_mat_t PM_inpaint(PM_mat_t source_py, PM_mat_t mask_py, int patch_size)
28
- {
29
- cv::Mat source = _py_to_cv2(source_py);
30
- cv::Mat mask = _py_to_cv2(mask_py);
31
- auto metric = PatchSSDDistanceMetric(patch_size);
32
- cv::Mat result = Inpainting(source, mask, &metric).run(PM_verbose, false, PM_seed);
33
- return _cv2_to_py(result);
34
- }
35
-
36
- PM_mat_t PM_inpaint_regularity(PM_mat_t source_py, PM_mat_t mask_py, PM_mat_t ijmap_py, int patch_size, float guide_weight)
37
- {
38
- cv::Mat source = _py_to_cv2(source_py);
39
- cv::Mat mask = _py_to_cv2(mask_py);
40
- cv::Mat ijmap = _py_to_cv2(ijmap_py);
41
-
42
- auto metric = RegularityGuidedPatchDistanceMetricV2(patch_size, ijmap, guide_weight);
43
- cv::Mat result = Inpainting(source, mask, &metric).run(PM_verbose, false, PM_seed);
44
- return _cv2_to_py(result);
45
- }
46
-
47
- PM_mat_t PM_inpaint2(PM_mat_t source_py, PM_mat_t mask_py, PM_mat_t global_mask_py, int patch_size)
48
- {
49
- cv::Mat source = _py_to_cv2(source_py);
50
- cv::Mat mask = _py_to_cv2(mask_py);
51
- cv::Mat global_mask = _py_to_cv2(global_mask_py);
52
-
53
- auto metric = PatchSSDDistanceMetric(patch_size);
54
- cv::Mat result = Inpainting(source, mask, global_mask, &metric).run(PM_verbose, false, PM_seed);
55
- return _cv2_to_py(result);
56
- }
57
-
58
- PM_mat_t PM_inpaint2_regularity(PM_mat_t source_py, PM_mat_t mask_py, PM_mat_t global_mask_py, PM_mat_t ijmap_py, int patch_size, float guide_weight)
59
- {
60
- cv::Mat source = _py_to_cv2(source_py);
61
- cv::Mat mask = _py_to_cv2(mask_py);
62
- cv::Mat global_mask = _py_to_cv2(global_mask_py);
63
- cv::Mat ijmap = _py_to_cv2(ijmap_py);
64
-
65
- auto metric = RegularityGuidedPatchDistanceMetricV2(patch_size, ijmap, guide_weight);
66
- cv::Mat result = Inpainting(source, mask, global_mask, &metric).run(PM_verbose, false, PM_seed);
67
- return _cv2_to_py(result);
68
- }
69
-
70
- int _dtype_py_to_cv(int dtype_py)
71
- {
72
- switch (dtype_py)
73
- {
74
- case PM_UINT8:
75
- return CV_8U;
76
- case PM_INT8:
77
- return CV_8S;
78
- case PM_UINT16:
79
- return CV_16U;
80
- case PM_INT16:
81
- return CV_16S;
82
- case PM_INT32:
83
- return CV_32S;
84
- case PM_FLOAT32:
85
- return CV_32F;
86
- case PM_FLOAT64:
87
- return CV_64F;
88
- }
89
-
90
- return CV_8U;
91
- }
92
-
93
- int _dtype_cv_to_py(int dtype_cv)
94
- {
95
- switch (dtype_cv)
96
- {
97
- case CV_8U:
98
- return PM_UINT8;
99
- case CV_8S:
100
- return PM_INT8;
101
- case CV_16U:
102
- return PM_UINT16;
103
- case CV_16S:
104
- return PM_INT16;
105
- case CV_32S:
106
- return PM_INT32;
107
- case CV_32F:
108
- return PM_FLOAT32;
109
- case CV_64F:
110
- return PM_FLOAT64;
111
- }
112
-
113
- return PM_UINT8;
114
- }
115
-
116
- cv::Mat _py_to_cv2(PM_mat_t pymat)
117
- {
118
- int dtype = CV_MAKETYPE(_dtype_py_to_cv(pymat.dtype), pymat.shape.channels);
119
- return cv::Mat(cv::Size(pymat.shape.width, pymat.shape.height), dtype, pymat.data_ptr).clone();
120
- }
121
-
122
- PM_mat_t _cv2_to_py(cv::Mat cvmat)
123
- {
124
- PM_shape_t shape = {cvmat.size().width, cvmat.size().height, cvmat.channels()};
125
- int dtype = _dtype_cv_to_py(cvmat.depth());
126
- size_t dsize = cvmat.total() * cvmat.elemSize();
127
-
128
- void *data_ptr = reinterpret_cast<void *>(malloc(dsize));
129
- memcpy(data_ptr, reinterpret_cast<void *>(cvmat.data), dsize);
130
-
131
- return PM_mat_t{data_ptr, shape, dtype};
132
- }
File without changes