prpy 0.2.4__tar.gz → 0.2.6__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 (51) hide show
  1. {prpy-0.2.4 → prpy-0.2.6}/PKG-INFO +4 -2
  2. {prpy-0.2.4 → prpy-0.2.6}/README.md +3 -1
  3. {prpy-0.2.4 → prpy-0.2.6}/prpy/ffmpeg/readwrite.py +4 -4
  4. prpy-0.2.6/prpy/helpers.py +33 -0
  5. prpy-0.2.6/prpy/numpy/.DS_Store +0 -0
  6. {prpy-0.2.4 → prpy-0.2.6}/prpy/numpy/image.py +68 -6
  7. prpy-0.2.6/prpy/numpy/image_ops.c +332 -0
  8. prpy-0.2.6/prpy/numpy/image_ops.cpython-310-darwin.so +0 -0
  9. {prpy-0.2.4 → prpy-0.2.6}/prpy/numpy/metric.py +13 -8
  10. prpy-0.2.6/prpy/numpy/spam.cpython-310-darwin.so +0 -0
  11. {prpy-0.2.4 → prpy-0.2.6}/prpy.egg-info/PKG-INFO +4 -2
  12. {prpy-0.2.4 → prpy-0.2.6}/prpy.egg-info/SOURCES.txt +5 -0
  13. {prpy-0.2.4 → prpy-0.2.6}/pyproject.toml +4 -1
  14. prpy-0.2.6/setup.py +12 -0
  15. {prpy-0.2.4 → prpy-0.2.6}/tests/test_numpy_image.py +44 -2
  16. {prpy-0.2.4 → prpy-0.2.6}/tests/test_numpy_metric.py +6 -2
  17. prpy-0.2.4/setup.py +0 -3
  18. {prpy-0.2.4 → prpy-0.2.6}/.github/workflows/main.yml +0 -0
  19. {prpy-0.2.4 → prpy-0.2.6}/.gitignore +0 -0
  20. {prpy-0.2.4 → prpy-0.2.6}/LICENSE +0 -0
  21. {prpy-0.2.4 → prpy-0.2.6}/MANIFEST.in +0 -0
  22. {prpy-0.2.4 → prpy-0.2.6}/prpy/__init__.py +0 -0
  23. {prpy-0.2.4 → prpy-0.2.6}/prpy/constants.py +0 -0
  24. {prpy-0.2.4 → prpy-0.2.6}/prpy/ffmpeg/__init__.py +0 -0
  25. {prpy-0.2.4 → prpy-0.2.6}/prpy/ffmpeg/probe.py +0 -0
  26. {prpy-0.2.4 → prpy-0.2.6}/prpy/ffmpeg/utils.py +0 -0
  27. {prpy-0.2.4 → prpy-0.2.6}/prpy/numpy/__init__.py +0 -0
  28. {prpy-0.2.4 → prpy-0.2.6}/prpy/numpy/face.py +0 -0
  29. {prpy-0.2.4 → prpy-0.2.6}/prpy/numpy/signal.py +0 -0
  30. {prpy-0.2.4 → prpy-0.2.6}/prpy/numpy/stride_tricks.py +0 -0
  31. {prpy-0.2.4 → prpy-0.2.6}/prpy/tensorflow/__init__.py +0 -0
  32. {prpy-0.2.4 → prpy-0.2.6}/prpy/tensorflow/image.py +0 -0
  33. {prpy-0.2.4 → prpy-0.2.6}/prpy/tensorflow/loss.py +0 -0
  34. {prpy-0.2.4 → prpy-0.2.6}/prpy/tensorflow/lr_schedule.py +0 -0
  35. {prpy-0.2.4 → prpy-0.2.6}/prpy/tensorflow/model_saver.py +0 -0
  36. {prpy-0.2.4 → prpy-0.2.6}/prpy/tensorflow/nan.py +0 -0
  37. {prpy-0.2.4 → prpy-0.2.6}/prpy/tensorflow/optimizer.py +0 -0
  38. {prpy-0.2.4 → prpy-0.2.6}/prpy/tensorflow/signal.py +0 -0
  39. {prpy-0.2.4 → prpy-0.2.6}/prpy/torch/__init__.py +0 -0
  40. {prpy-0.2.4 → prpy-0.2.6}/prpy/torch/model_saver.py +0 -0
  41. {prpy-0.2.4 → prpy-0.2.6}/prpy.egg-info/dependency_links.txt +0 -0
  42. {prpy-0.2.4 → prpy-0.2.6}/prpy.egg-info/requires.txt +0 -0
  43. {prpy-0.2.4 → prpy-0.2.6}/prpy.egg-info/top_level.txt +0 -0
  44. {prpy-0.2.4 → prpy-0.2.6}/setup.cfg +0 -0
  45. {prpy-0.2.4 → prpy-0.2.6}/tests/conftest.py +0 -0
  46. {prpy-0.2.4 → prpy-0.2.6}/tests/test_ffmpeg.py +0 -0
  47. {prpy-0.2.4 → prpy-0.2.6}/tests/test_numpy_face.py +0 -0
  48. {prpy-0.2.4 → prpy-0.2.6}/tests/test_numpy_signal.py +0 -0
  49. {prpy-0.2.4 → prpy-0.2.6}/tests/test_numpy_stride_tricks.py +0 -0
  50. {prpy-0.2.4 → prpy-0.2.6}/tests/test_tensorflow.py +0 -0
  51. {prpy-0.2.4 → prpy-0.2.6}/tests/test_torch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prpy
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Collection of Python utils for signal, image, and video processing
5
5
  Author-email: Philipp Rouast <philipp@rouast.com>
6
6
  License: MIT License
@@ -36,10 +36,12 @@ Provides-Extra: test
36
36
  Requires-Dist: pytest; extra == "test"
37
37
  Requires-Dist: flake8; extra == "test"
38
38
 
39
- [![Tests](https://github.com/prouast/prpy/actions/workflows/main.yml/badge.svg)](https://github.com/prouast/prpy/actions/workflows/main.yml)
40
39
 
41
40
  # prpy
42
41
 
42
+ [![Tests](https://github.com/prouast/prpy/actions/workflows/main.yml/badge.svg)](https://github.com/prouast/prpy/actions/workflows/main.yml)
43
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/prpy?period=total&units=international_system&left_color=grey&right_color=blue&left_text=pip%20downloads)](https://pypi.org/project/prpy/)
44
+
43
45
  A collection of Python utilities for signal, image, and video processing.
44
46
  It contains subpackages for working with `numpy`, `ffmpeg`, `tensorflow`, and `torch`.
45
47
 
@@ -1,7 +1,9 @@
1
- [![Tests](https://github.com/prouast/prpy/actions/workflows/main.yml/badge.svg)](https://github.com/prouast/prpy/actions/workflows/main.yml)
2
1
 
3
2
  # prpy
4
3
 
4
+ [![Tests](https://github.com/prouast/prpy/actions/workflows/main.yml/badge.svg)](https://github.com/prouast/prpy/actions/workflows/main.yml)
5
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/prpy?period=total&units=international_system&left_color=grey&right_color=blue&left_text=pip%20downloads)](https://pypi.org/project/prpy/)
6
+
5
7
  A collection of Python utilities for signal, image, and video processing.
6
8
  It contains subpackages for working with `numpy`, `ffmpeg`, `tensorflow`, and `torch`.
7
9
 
@@ -170,8 +170,8 @@ def _ffmpeg_output_to_numpy(
170
170
  r: int,
171
171
  fps: Union[float, int, None],
172
172
  n: int,
173
- w: int,
174
- h: int,
173
+ w: Union[int, np.int64],
174
+ h: Union[int, np.int64],
175
175
  scale: Union[tuple, int, None] = None,
176
176
  crf: Union[int, None] = None,
177
177
  pix_fmt: str = 'bgr24',
@@ -203,8 +203,8 @@ def _ffmpeg_output_to_numpy(
203
203
  assert isinstance(r, int)
204
204
  assert fps is None or isinstance(fps, (float, int))
205
205
  assert isinstance(n, int)
206
- assert isinstance(w, int)
207
- assert isinstance(h, int)
206
+ assert isinstance(w, (int, np.int64))
207
+ assert isinstance(h, (int, np.int64))
208
208
  assert crf is None or isinstance(crf, int)
209
209
  assert isinstance(pix_fmt, str)
210
210
  assert isinstance(dim_deltas, tuple) and len(dim_deltas) == 3 and all(isinstance(i, int) for i in dim_deltas)
@@ -0,0 +1,33 @@
1
+ # Copyright (c) 2024 Philipp Rouast
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ import argparse
22
+
23
+ def str2bool(v):
24
+ """Boolean type for argparse"""
25
+ if isinstance(v, bool):
26
+ return v
27
+ if v.lower() in ('yes', 'true', 't', 'y', '1'):
28
+ return True
29
+ elif v.lower() in ('no', 'false', 'f', 'n', '0'):
30
+ return False
31
+ else:
32
+ raise argparse.ArgumentTypeError('Boolean value expected.')
33
+
Binary file
@@ -19,8 +19,11 @@
19
19
  # SOFTWARE.
20
20
 
21
21
  import numpy as np
22
+ import logging
22
23
  from typing import Union
23
24
 
25
+ from .image_ops import resample_bilinear_op, resample_box_op, reduce_roi_op
26
+
24
27
  def crop_slice_resize(
25
28
  inputs: np.ndarray,
26
29
  target_size: Union[int, tuple, list],
@@ -34,17 +37,18 @@ def crop_slice_resize(
34
37
  """Crop, slice, and resize image(s) with all same settings.
35
38
 
36
39
  Args:
37
- inputs: The inputs as uint8 shape (h, w, 3) or (n_frames, h, w, 3)
40
+ inputs: The inputs of shape (h, w, 3) or (n_frames, h, w, 3)
38
41
  target_size: The target size; scalar or (H, W) if preserve_aspect_ratio=False
39
42
  roi: The region of interest in format (x0, y0, x1, y1). Use None to keep all.
40
43
  target_idxs: The frame indices to be used. Use None to keep all.
41
- preserve_aspect_ratio: Preserve the aspect ratio?
44
+ preserve_aspect_ratio: Preserve the aspect ratio? (must be False for library=prpy)
42
45
  keepdims: If True, always keep n_frames dim. Otherwise, may drop n_frames dim.
43
- library: The library to use. `cv2` or `PIL` (return np ndarray), or `tf` (returns tf Tensor)
46
+ library: The library to use: `cv2`, `PIL`, `prpy` (return np.ndarray), or `tf` (returns tf.Tensor)
47
+ prpy is experimental and only supports bilinear scale_algorithm.
44
48
  scale_algorithm: The algorithm used for scaling.
45
49
  Supports: bicubic, bilinear, area (not for PIL!), lanczos. Default: bicubic
46
50
  Returns:
47
- result: The processed frame(s) as float32 shape (h, w, 3) or (n_frames, h, w, 3)
51
+ result: The processed frame(s) of shape (h_new, w_new, 3) or (n_frames_new, h_new, w_new, 3)
48
52
  """
49
53
  assert isinstance(inputs, np.ndarray) and (len(inputs.shape) == 3 or len(inputs.shape) == 4)
50
54
  assert isinstance(target_size, int) or (isinstance(target_size, (tuple, list)) and len(target_size) == 2 and all(isinstance(i, int) for i in target_size))
@@ -86,9 +90,9 @@ def crop_slice_resize(
86
90
  # No resizing necessary
87
91
  if library == 'tf':
88
92
  import tensorflow as tf
89
- out = tf.convert_to_tensor(inputs, dtype=tf.float32)
93
+ out = tf.convert_to_tensor(inputs)
90
94
  else:
91
- out = inputs.astype(np.float32)
95
+ out = inputs
92
96
  else:
93
97
  # Resize to out_size
94
98
  if library == 'tf':
@@ -127,6 +131,17 @@ def crop_slice_resize(
127
131
  out_size = (out_size[1], out_size[0])
128
132
  out = np.asarray([
129
133
  cv2.resize(src=f, dsize=out_size, interpolation=library_algorithm) for f in inputs])
134
+ elif library == 'prpy':
135
+ if preserve_aspect_ratio:
136
+ raise ValueError("preserve_aspect_ratio=True is not supported for library=prpy")
137
+ if scale_algorithm == 'bilinear':
138
+ if inputs.shape[1] / target_height > 2 and inputs.shape[2] / target_width > 2:
139
+ logging.debug("Switching from bilinear to box by default because we are downsampling significantly.")
140
+ out = resample_box(im=inputs, size=(target_height, target_width))
141
+ else:
142
+ out = resample_bilinear(im=inputs, size=(target_height, target_width))
143
+ else:
144
+ raise ValueError("Scaling algorithm {} is not supported by {}".format(scale_algorithm, library))
130
145
  else:
131
146
  raise ValueError("Library {} not supported".format(library))
132
147
  if keepdims and len(out.shape) == 3 and len(inputs_shape) == 4:
@@ -139,3 +154,50 @@ def crop_slice_resize(
139
154
  if out.shape[0] == 1:
140
155
  out = squeeze(out, axis=0)
141
156
  return out
157
+
158
+ def resample_bilinear(
159
+ im: np.ndarray,
160
+ size: Union[int, tuple]
161
+ ):
162
+ """Compute bilinear resampling with batch dimension
163
+
164
+ Args:
165
+ im: The image(s) to be resized. Shape (n, h, w, c) or (h, w, c)
166
+ size: The new size either as scalar or (new_h, new_w)
167
+ Returns:
168
+ out: The resized image(s). Shape (n, new_h, new_w, c) or (new_h, new_w, c)
169
+ """
170
+ if isinstance(size, int): size = (size, size)
171
+ return resample_bilinear_op(im, size)
172
+
173
+ def resample_box(
174
+ im: np.ndarray,
175
+ size: Union[int, tuple]
176
+ ):
177
+ """Compute box resampling with batch dimension
178
+
179
+ - Note: This implementation only works for downsampling at least 2x in each dimension
180
+ I.e., h/new_h >= 2 and w/new_w >= 2
181
+
182
+ Args:
183
+ im: The image(s) to be resized. Shape (n, h, w, c) or (h, w, c)
184
+ size: The new size either as scalar or (new_h, new_w)
185
+ Returns:
186
+ out: The resized image(s). Shape (n, new_h, new_w, c) or (new_h, new_w, c)
187
+ """
188
+ if isinstance(size, int): size = (size, size)
189
+ return resample_box_op(im, size)
190
+
191
+ def reduce_roi(
192
+ video: np.ndarray,
193
+ roi: np.ndarray
194
+ ) -> np.ndarray:
195
+ """Reduce the spatial dimensions of a video by mean using ROI.
196
+
197
+ Args:
198
+ video: The video to be reduced. Shape (n, h, w, 3)
199
+ roi: The roi in form (x0, y0, x1, y1). Shape (n, 4)
200
+ Returns:
201
+ out: The reduced vals. Shape (n, 3)
202
+ """
203
+ return reduce_roi_op(video, roi)
@@ -0,0 +1,332 @@
1
+ // Copyright (c) 2024 Philipp Rouast
2
+
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ #include <Python.h>
22
+ #include <numpy/arrayobject.h>
23
+
24
+ #define DIV_ROUND_CLOSEST(n, d) ((((n) < 0) == ((d) < 0)) ? (((n) + (d)/2)/(d)) : (((n) - (d)/2)/(d)))
25
+
26
+ static PyObject*
27
+ resample_bilinear_op(PyObject* self, PyObject* args) {
28
+ PyArrayObject* input_array;
29
+ PyObject* size_tuple;
30
+ int new_height, new_width;
31
+
32
+ // Parse the arguments
33
+ if (!PyArg_ParseTuple(args, "O!O", &PyArray_Type, &input_array, &size_tuple)) {
34
+ return NULL;
35
+ }
36
+
37
+ // Convert the size tuple to new_height and new_width
38
+ if (!PyArg_ParseTuple(size_tuple, "ii", &new_height, &new_width)) {
39
+ return NULL;
40
+ }
41
+
42
+ // Check input array type and dimensions
43
+ if (PyArray_NDIM(input_array) != 4 || PyArray_TYPE(input_array) != NPY_UINT8 || PyArray_DIM(input_array, 3) != 3) {
44
+ PyErr_SetString(PyExc_ValueError, "Input array must be 4-dimensional with shape (n, h, w, 3) and of type uint8.");
45
+ return NULL;
46
+ }
47
+
48
+ // Get pointers to the data
49
+ unsigned char* input_data = (unsigned char*)PyArray_DATA(input_array);
50
+
51
+ // Get the dimensions of the input array
52
+ npy_intp* dims = PyArray_DIMS(input_array);
53
+ int n_frames = dims[0];
54
+ int height = dims[1];
55
+ int width = dims[2];
56
+
57
+ // Get the strides
58
+ npy_intp* input_strides = PyArray_STRIDES(input_array);
59
+
60
+ // Precompute the positions and weights
61
+ int pos[new_width][new_height][2];
62
+ int weights[new_width][new_height][2][2];
63
+ for (int y = 0; y < new_height; ++y) {
64
+ for (int x = 0; x < new_width; ++x) {
65
+ // Determine the position of the current pixel in the input image
66
+ float src_x = (x * (width - 1)) / (float)(new_width - 1);
67
+ float src_y = (y * (height - 1)) / (float)(new_height - 1);
68
+ // The four neighboring pixels
69
+ int x0 = (int)src_x;
70
+ int y0 = (int)src_y;
71
+ // Calculate the weights for each pixel
72
+ float dx = src_x - x0;
73
+ float dy = src_y - y0;
74
+ float dx1 = 1.0f - dx;
75
+ float dy1 = 1.0f - dy;
76
+ // Store the positions
77
+ pos[x][y][0] = x0;
78
+ pos[x][y][1] = y0;
79
+ // Store the weights
80
+ weights[x][y][0][0] = dx1 * dy1 * 256.0f;
81
+ weights[x][y][0][1] = dx * dy1 * 256.0f;
82
+ weights[x][y][1][0] = dx1 * dy * 256.0f;
83
+ weights[x][y][1][1] = dx * dy * 256.0f;
84
+ }
85
+ }
86
+
87
+ // Create an output array
88
+ npy_intp output_dims[4] = {n_frames, new_height, new_width, 3};
89
+ PyArrayObject* output_array_np = (PyArrayObject*)PyArray_EMPTY(4, output_dims, NPY_UINT8, 0);
90
+ if (output_array_np == NULL) {
91
+ return NULL;
92
+ }
93
+
94
+ // Get a pointer to the output data
95
+ unsigned char* output_data = (unsigned char*)PyArray_DATA(output_array_np);
96
+
97
+ // Resample each frame
98
+ for (int i = 0; i < n_frames; ++i) {
99
+ // Extract the current frame
100
+ unsigned char* current_frame = input_data + i * input_strides[0];
101
+ // Resize the frame using bilinear resampling
102
+ for (int y = 0; y < new_height; ++y) {
103
+ for (int x = 0; x < new_width; ++x) {
104
+ unsigned char* p1 = current_frame + pos[x][y][1] * input_strides[1] + pos[x][y][0] * input_strides[2];
105
+ unsigned char* p2 = p1 + input_strides[2];
106
+ unsigned char* p3 = p1 + input_strides[1];
107
+ unsigned char* p4 = p3 + input_strides[2];
108
+
109
+ // Calculate the weighted sum of pixels (for each color channel)
110
+ int outr = p1[0] * weights[x][y][0][0] + p2[0] * weights[x][y][0][1] +
111
+ p3[0] * weights[x][y][1][0] + p4[0] * weights[x][y][1][1];
112
+ int outg = p1[1] * weights[x][y][0][0] + p2[1] * weights[x][y][0][1] +
113
+ p3[1] * weights[x][y][1][0] + p4[1] * weights[x][y][1][1];
114
+ int outb = p1[2] * weights[x][y][0][0] + p2[2] * weights[x][y][0][1] +
115
+ p3[2] * weights[x][y][1][0] + p4[2] * weights[x][y][1][1];
116
+ // Save
117
+ int idx = i * new_height * new_width * 3 + y * new_width * 3 + x * 3;
118
+ output_data[idx + 0] = (unsigned char)(outr >> 8);
119
+ output_data[idx + 1] = (unsigned char)(outg >> 8);
120
+ output_data[idx + 2] = (unsigned char)(outb >> 8);
121
+ }
122
+ }
123
+ }
124
+
125
+ Py_INCREF(output_array_np);
126
+ return (PyObject*)output_array_np;
127
+ }
128
+
129
+ static PyObject*
130
+ resample_box_op(PyObject* self, PyObject* args) {
131
+ PyArrayObject* input_array;
132
+ PyObject* size_tuple;
133
+ int new_height, new_width;
134
+
135
+ // Parse the arguments
136
+ if (!PyArg_ParseTuple(args, "O!O", &PyArray_Type, &input_array, &size_tuple)) {
137
+ return NULL;
138
+ }
139
+
140
+ // Convert the size tuple to new_height and new_width
141
+ if (!PyArg_ParseTuple(size_tuple, "ii", &new_height, &new_width)) {
142
+ return NULL;
143
+ }
144
+
145
+ // Check input array type and dimensions
146
+ if (PyArray_NDIM(input_array) != 4 || PyArray_TYPE(input_array) != NPY_UINT8 || PyArray_DIM(input_array, 3) != 3) {
147
+ PyErr_SetString(PyExc_ValueError, "Input array must be 4-dimensional with shape (n, h, w, 3) and of type uint8.");
148
+ return NULL;
149
+ }
150
+
151
+ // Get pointers to the data
152
+ unsigned char* input_data = (unsigned char*)PyArray_DATA(input_array);
153
+
154
+ // Get the dimensions of the input array
155
+ npy_intp* dims = PyArray_DIMS(input_array);
156
+ int n_frames = dims[0];
157
+ int height = dims[1];
158
+ int width = dims[2];
159
+
160
+ // Make sure that we are downsampling
161
+ if (height / new_height < 2 || width / new_width < 2) {
162
+ PyErr_SetString(PyExc_ValueError, "This implementation of box resampling can only be used for downsampling.");
163
+ return NULL;
164
+ }
165
+
166
+ // Get the strides
167
+ npy_intp* input_strides = PyArray_STRIDES(input_array);
168
+
169
+ // Create an output array
170
+ npy_intp output_dims[4] = {n_frames, new_height, new_width, 3};
171
+ PyArrayObject* output_array_np = (PyArrayObject*)PyArray_EMPTY(4, output_dims, NPY_UINT8, 0);
172
+ if (output_array_np == NULL) {
173
+ return NULL;
174
+ }
175
+
176
+ // Get a pointer to the output data
177
+ unsigned char* output_data = (unsigned char*)PyArray_DATA(output_array_np);
178
+
179
+ // Precompute start and end indices for rows and columns and num_pixels
180
+ int start_x[new_width];
181
+ int end_x[new_width];
182
+ int start_y[new_height];
183
+ int end_y[new_height];
184
+ int num_pixels_[new_width][new_height];
185
+ for (int x = 0; x < new_width; ++x) {
186
+ start_x[x] = DIV_ROUND_CLOSEST(x * width, new_width);
187
+ end_x[x] = DIV_ROUND_CLOSEST((x + 1) * width, new_width);
188
+ }
189
+ for (int y = 0; y < new_height; ++y) {
190
+ start_y[y] = DIV_ROUND_CLOSEST(y * height, new_height);
191
+ end_y[y] = DIV_ROUND_CLOSEST((y + 1) * height, new_height);
192
+ for (int x = 0; x < new_width; ++x) {
193
+ num_pixels_[x][y] = (end_y[y] - start_y[y]) * (end_x[x] - start_x[x]);
194
+ }
195
+ }
196
+
197
+ // Resample each frame
198
+ for (int i = 0; i < n_frames; ++i) {
199
+ // Extract the current frame
200
+ unsigned char* current_frame = input_data + i * input_strides[0];
201
+ // Resize the frame using box resampling (average pooling)
202
+ for (int y = 0; y < new_height; ++y) {
203
+ for (int x = 0; x < new_width; ++x) {
204
+ // Compute the mean of pixels in the input image block
205
+ int sum_r = 0, sum_g = 0, sum_b = 0;
206
+ for (int src_y = start_y[y]; src_y < end_y[y]; ++src_y) {
207
+ for (int src_x = start_x[x]; src_x < end_x[x]; ++src_x) {
208
+ unsigned char* pixel = current_frame + src_y * input_strides[1] + src_x * input_strides[2];
209
+ sum_r += pixel[0];
210
+ sum_g += pixel[1];
211
+ sum_b += pixel[2];
212
+ }
213
+ }
214
+ // Calculate the average value of the pixels
215
+ int num_pixels = num_pixels_[x][y];
216
+ unsigned char avg_r = (unsigned char)(sum_r / num_pixels);
217
+ unsigned char avg_g = (unsigned char)(sum_g / num_pixels);
218
+ unsigned char avg_b = (unsigned char)(sum_b / num_pixels);
219
+ // Save the average value to the output image
220
+ int idx = i * new_height * new_width * 3 + y * new_width * 3 + x * 3;
221
+ output_data[idx + 0] = avg_r;
222
+ output_data[idx + 1] = avg_g;
223
+ output_data[idx + 2] = avg_b;
224
+ }
225
+ }
226
+ }
227
+
228
+ Py_INCREF(output_array_np);
229
+ return (PyObject*)output_array_np;
230
+ }
231
+
232
+ static PyObject*
233
+ reduce_roi_op(PyObject* self, PyObject* args) {
234
+ PyArrayObject* input_array_video;
235
+ PyArrayObject* input_array_roi;
236
+
237
+ // Parse the arguments
238
+ if (!PyArg_ParseTuple(args, "O!O!", &PyArray_Type, &input_array_video, &PyArray_Type, &input_array_roi)) {
239
+ return NULL;
240
+ }
241
+
242
+ // Check input video array type and dimensions
243
+ if (PyArray_NDIM(input_array_video) != 4 || PyArray_TYPE(input_array_video) != NPY_UINT8 || PyArray_DIM(input_array_video, 3) != 3) {
244
+ PyErr_SetString(PyExc_ValueError, "Input video array must be 4-dimensional with shape (n, h, w, 3) and of type uint8.");
245
+ return NULL;
246
+ }
247
+
248
+ // Check input roi array type and dimensions
249
+ if (PyArray_NDIM(input_array_roi) != 2 || PyArray_TYPE(input_array_roi) != NPY_INT64 || PyArray_DIM(input_array_roi, 1) != 4 || PyArray_DIM(input_array_roi, 0) != PyArray_DIM(input_array_video, 0)) {
250
+ PyErr_SetString(PyExc_ValueError, "Input roi array must be 2-dimensional with shape (n, 4) and of type int64. First dim must match first dim of video array.");
251
+ return NULL;
252
+ }
253
+
254
+ // Get pointers to the data
255
+ unsigned char* video_data = (unsigned char*)PyArray_DATA(input_array_video);
256
+ npy_int64* roi_data = (npy_int64*)PyArray_DATA(input_array_roi);
257
+
258
+ // Get the strides
259
+ npy_intp* video_strides = PyArray_STRIDES(input_array_video);
260
+ npy_intp* roi_strides = PyArray_STRIDES(input_array_roi);
261
+
262
+ // Get the dimensions of the input video array
263
+ npy_intp* video_dims = PyArray_DIMS(input_array_video);
264
+ int n_frames = video_dims[0];
265
+
266
+ // Create an output array
267
+ npy_intp output_dims[2] = {n_frames, 3};
268
+ PyArrayObject* output_array_np = (PyArrayObject*)PyArray_EMPTY(2, output_dims, NPY_FLOAT32, 0);
269
+ if (output_array_np == NULL) {
270
+ return NULL;
271
+ }
272
+ // Get a pointer to the output data
273
+ float* output_data = (float*)PyArray_DATA(output_array_np);
274
+
275
+ // Reduce each frame
276
+ for (int i = 0; i < n_frames; ++i) {
277
+ // Extract the current frame
278
+ unsigned char* current_frame_video = video_data + i * video_strides[0];
279
+ npy_int64* current_frame_roi = roi_data + i * 4; // TODO: Why doesn't roi_strides[0] work?
280
+ // Compute the mean of pixels within the ROI
281
+ int sum_r = 0, sum_g = 0, sum_b = 0;
282
+ int x0 = current_frame_roi[0];
283
+ int y0 = current_frame_roi[1];
284
+ int x1 = current_frame_roi[2];
285
+ int y1 = current_frame_roi[3];
286
+ for (int src_y = y0; src_y < y1; ++src_y) {
287
+ for (int src_x = x0; src_x < x1; ++src_x) {
288
+ unsigned char* pixel = current_frame_video + src_y * video_strides[1] + src_x * video_strides[2];
289
+ sum_r += pixel[0];
290
+ sum_g += pixel[1];
291
+ sum_b += pixel[2];
292
+ }
293
+ }
294
+ // Calculate the average value of the pixels
295
+ int num_pixels = (y1 - y0) * (x1 - x0);
296
+ float avg_r = sum_r / (float)num_pixels;
297
+ float avg_g = sum_g / (float)num_pixels;
298
+ float avg_b = sum_b / (float)num_pixels;
299
+ // Save the average value to the output image
300
+ int idx = i * 3;
301
+ output_data[idx + 0] = avg_r;
302
+ output_data[idx + 1] = avg_g;
303
+ output_data[idx + 2] = avg_b;
304
+ }
305
+
306
+ Py_INCREF(output_array_np);
307
+ return (PyObject*)output_array_np;
308
+ }
309
+
310
+ static PyMethodDef image_ops_methods[] = {
311
+ {"resample_bilinear_op", resample_bilinear_op, METH_VARARGS, PyDoc_STR("Resize video frames using bilinear resampling.")},
312
+ {"resample_box_op", resample_box_op, METH_VARARGS, PyDoc_STR("Resize video frames using box resampling.")},
313
+ {"reduce_roi_op", reduce_roi_op, METH_VARARGS, PyDoc_STR("Reduce spatial dimensions of video by mean using ROI.")},
314
+ {NULL, NULL, 0, NULL} /* Sentinel */
315
+ };
316
+
317
+ PyDoc_STRVAR(image_ops_doc,
318
+ "Image ops implemented in C.");
319
+
320
+ static struct PyModuleDef image_ops = {
321
+ PyModuleDef_HEAD_INIT,
322
+ "image_ops",
323
+ image_ops_doc,
324
+ -1,
325
+ image_ops_methods
326
+ };
327
+
328
+ PyMODINIT_FUNC
329
+ PyInit_image_ops(void) {
330
+ import_array();
331
+ return PyModule_Create(&image_ops);
332
+ }
@@ -135,11 +135,12 @@ def cor(
135
135
  def snr(
136
136
  f_true: np.ndarray,
137
137
  y_pred: np.ndarray,
138
- f_s: float,
138
+ f_s: Union[float, int],
139
139
  f_res: float,
140
- tol: float = .1,
141
- f_min: float = .5,
142
- f_max: float = 4.):
140
+ tol: Union[float, int] = .1,
141
+ f_min: Union[float, int] = .5,
142
+ f_max: Union[float, int] = 4.
143
+ ):
143
144
  """Signal-to-noise ratio
144
145
  Args:
145
146
  f_true: The true frequencies. Shape (b,) or ()
@@ -152,11 +153,15 @@ def snr(
152
153
  Returns:
153
154
  snr: The signal to noise ratio. Shape (b,) or ()
154
155
  """
155
- assert isinstance(f_s, float)
156
+ assert isinstance(f_s, (float, int))
156
157
  assert isinstance(f_res, float)
157
- assert isinstance(tol, float)
158
- assert isinstance(f_min, float)
159
- assert isinstance(f_max, float)
158
+ assert isinstance(tol, (float, int))
159
+ assert isinstance(f_min, (float, int))
160
+ assert isinstance(f_max, (float, int))
161
+ f_s = float(f_s)
162
+ tol = float(tol)
163
+ f_min = float(f_min)
164
+ f_max = float(f_max)
160
165
  f_true = np.asarray(f_true)
161
166
  y_pred = np.asarray(y_pred)
162
167
  assert len(y_pred.shape) == 1 or len(y_pred.shape) == 2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prpy
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Collection of Python utils for signal, image, and video processing
5
5
  Author-email: Philipp Rouast <philipp@rouast.com>
6
6
  License: MIT License
@@ -36,10 +36,12 @@ Provides-Extra: test
36
36
  Requires-Dist: pytest; extra == "test"
37
37
  Requires-Dist: flake8; extra == "test"
38
38
 
39
- [![Tests](https://github.com/prouast/prpy/actions/workflows/main.yml/badge.svg)](https://github.com/prouast/prpy/actions/workflows/main.yml)
40
39
 
41
40
  # prpy
42
41
 
42
+ [![Tests](https://github.com/prouast/prpy/actions/workflows/main.yml/badge.svg)](https://github.com/prouast/prpy/actions/workflows/main.yml)
43
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/prpy?period=total&units=international_system&left_color=grey&right_color=blue&left_text=pip%20downloads)](https://pypi.org/project/prpy/)
44
+
43
45
  A collection of Python utilities for signal, image, and video processing.
44
46
  It contains subpackages for working with `numpy`, `ffmpeg`, `tensorflow`, and `torch`.
45
47
 
@@ -7,6 +7,7 @@ setup.py
7
7
  .github/workflows/main.yml
8
8
  prpy/__init__.py
9
9
  prpy/constants.py
10
+ prpy/helpers.py
10
11
  prpy.egg-info/PKG-INFO
11
12
  prpy.egg-info/SOURCES.txt
12
13
  prpy.egg-info/dependency_links.txt
@@ -16,11 +17,15 @@ prpy/ffmpeg/__init__.py
16
17
  prpy/ffmpeg/probe.py
17
18
  prpy/ffmpeg/readwrite.py
18
19
  prpy/ffmpeg/utils.py
20
+ prpy/numpy/.DS_Store
19
21
  prpy/numpy/__init__.py
20
22
  prpy/numpy/face.py
21
23
  prpy/numpy/image.py
24
+ prpy/numpy/image_ops.c
25
+ prpy/numpy/image_ops.cpython-310-darwin.so
22
26
  prpy/numpy/metric.py
23
27
  prpy/numpy/signal.py
28
+ prpy/numpy/spam.cpython-310-darwin.so
24
29
  prpy/numpy/stride_tricks.py
25
30
  prpy/tensorflow/__init__.py
26
31
  prpy/tensorflow/image.py
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["setuptools>=61", "setuptools-scm"]
2
+ requires = ["setuptools>=61", "setuptools-scm", "numpy"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
@@ -34,4 +34,7 @@ test = ["pytest", "flake8"]
34
34
  Repository = "https://github.com/prouast/prpy.git"
35
35
  Issues = "https://github.com/prouast/prpy/issues"
36
36
 
37
+ [tools.setuptools]
38
+ packages = ["prpy"]
39
+
37
40
  [tool.setuptools_scm]
prpy-0.2.6/setup.py ADDED
@@ -0,0 +1,12 @@
1
+ from setuptools import setup, Extension
2
+ import numpy as np
3
+
4
+ setup(
5
+ ext_modules=[
6
+ Extension(
7
+ name="prpy.numpy.image_ops",
8
+ sources=["prpy/numpy/image_ops.c"],
9
+ include_dirs=[np.get_include()]
10
+ ),
11
+ ]
12
+ )
@@ -19,6 +19,8 @@
19
19
  # SOFTWARE.
20
20
 
21
21
  from prpy.numpy.image import crop_slice_resize
22
+ from prpy.numpy.image import resample_bilinear, resample_box
23
+ from prpy.numpy.image import reduce_roi
22
24
 
23
25
  import numpy as np
24
26
  import pytest
@@ -29,12 +31,14 @@ import tensorflow as tf
29
31
  @pytest.mark.parametrize("roi", [(2, 2, 7, 7), [2, 2, 7, 7], None])
30
32
  @pytest.mark.parametrize("target_idxs", [None, [0, 2], (0, 2), np.asarray([0, 2])])
31
33
  @pytest.mark.parametrize("preserve_aspect_ratio", [True, False])
32
- @pytest.mark.parametrize("library", ["cv2", "tf", "PIL"])
34
+ @pytest.mark.parametrize("library", ["cv2", "tf", "PIL", "prpy"])
33
35
  @pytest.mark.parametrize("scale_algorithm", ["bicubic", "area", "lanczos", "bilinear"])
34
36
  @pytest.mark.parametrize("keepdims", [True, False])
35
37
  def test_crop_slice_resize(target_size, n_frames, roi, target_idxs, preserve_aspect_ratio, library, scale_algorithm, keepdims):
36
38
  if (n_frames is None and target_idxs is not None) or \
37
- (library == "PIL" and scale_algorithm == "area"):
39
+ (library == "PIL" and scale_algorithm == "area") or \
40
+ (library == "prpy" and scale_algorithm in ["bicubic", "area", "lanczos"]) or \
41
+ (library == "prpy" and preserve_aspect_ratio):
38
42
  pytest.skip("Skip because parameter combination does not work")
39
43
  if n_frames is None:
40
44
  images_in = np.random.uniform(size=(8, 12, 3), low=0, high=255)
@@ -74,3 +78,41 @@ def test_crop_slice_resize_retinaface():
74
78
  inputs=images_in, target_size=224, roi=(0, 0, 480, 640), target_idxs=None,
75
79
  library='tf', preserve_aspect_ratio=True, keepdims=True, scale_algorithm='bicubic')
76
80
  assert images_out.shape == (1, 224, 224, 3)
81
+
82
+ @pytest.mark.parametrize("n_frames", [1, 3])
83
+ @pytest.mark.parametrize("size", [4, 12, (12, 16)])
84
+ def test_resample_bilinear(n_frames, size):
85
+ # Note: Only tests for correct shape, not for correct pixel vals
86
+ im = np.random.uniform(size=(n_frames, 8, 12, 3), low=0, high=255).astype(np.uint8)
87
+ out = resample_bilinear(im=im, size=size)
88
+ if isinstance(size, int): size = (size, size)
89
+ assert out.shape == (n_frames, size[0], size[1], 3)
90
+
91
+ @pytest.mark.parametrize("n_frames", [1, 3])
92
+ @pytest.mark.parametrize("size", [4, (2, 3)])
93
+ def test_resample_box(n_frames, size):
94
+ # Note: Only tests for correct shape, not for correct pixel vals
95
+ im = np.random.uniform(size=(n_frames, 8, 12, 3), low=0, high=255).astype(np.uint8)
96
+ out = resample_box(im=im, size=size)
97
+ if isinstance(size, int): size = (size, size)
98
+ assert out.shape == (n_frames, size[0], size[1], 3)
99
+
100
+ def test_resample_box_only_downsampling():
101
+ im = np.random.uniform(size=(3, 8, 12, 3), low=0, high=255).astype(np.uint8)
102
+ with pytest.raises(Exception):
103
+ _ = resample_box(im=im, size=5)
104
+
105
+ @pytest.mark.parametrize("scenario", [(1, [[2, 3, 4, 7]]),
106
+ (3, [[3, 4, 7, 12], [3, 5, 7, 12], [3, 5, 6, 10]])])
107
+ def test_reduce_roi(scenario):
108
+ n_frames = scenario[0]
109
+ roi = np.asarray(scenario[1])
110
+ video = np.random.uniform(size=(n_frames, 12, 8, 3), low=0, high=255).astype(np.uint8)
111
+ out = reduce_roi(video=video, roi=roi)
112
+ exp = np.asarray([np.mean(video[i, roi[i,1]:roi[i,3], roi[i,0]:roi[i,2]], axis=(0,1)) for i in range(n_frames)])
113
+ assert out.shape == (n_frames, 3)
114
+ np.testing.assert_allclose(
115
+ out,
116
+ exp,
117
+ atol=1e-4, rtol=1e-4)
118
+
@@ -56,8 +56,12 @@ def test_cor(shape):
56
56
  np.testing.assert_allclose(out, np.corrcoef(y_true, y_pred)[0,1])
57
57
 
58
58
  @pytest.mark.parametrize("shape", [(6,), (10, 6)])
59
- def test_snr(shape):
59
+ @pytest.mark.parametrize("f_s", [5., 5])
60
+ @pytest.mark.parametrize("tol", [.2, 1])
61
+ @pytest.mark.parametrize("f_min", [.5, 1])
62
+ @pytest.mark.parametrize("f_max", [4, 4.])
63
+ def test_snr(shape, f_s, tol, f_min, f_max):
60
64
  f_true = np.random.uniform(size=shape[:-1])
61
65
  y_pred = np.random.uniform(size=shape)
62
- out = snr(f_true, y_pred, f_s=5., f_res=.1)
66
+ out = snr(f_true, y_pred, f_s=f_s, f_res=.1, tol=tol, f_min=f_min, f_max=f_max)
63
67
  assert out.shape == shape[:-1]
prpy-0.2.4/setup.py DELETED
@@ -1,3 +0,0 @@
1
- from setuptools import setup
2
-
3
- setup()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes