prpy 0.4.4__tar.gz → 0.4.5__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 (69) hide show
  1. {prpy-0.4.4 → prpy-0.4.5}/PKG-INFO +2 -4
  2. {prpy-0.4.4 → prpy-0.4.5}/README.md +0 -2
  3. {prpy-0.4.4 → prpy-0.4.5}/prpy/ffmpeg/probe.py +46 -15
  4. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/filters.py +9 -6
  5. {prpy-0.4.4 → prpy-0.4.5}/prpy.egg-info/PKG-INFO +2 -4
  6. prpy-0.4.5/prpy.egg-info/scm_version.json +8 -0
  7. {prpy-0.4.4 → prpy-0.4.5}/pyproject.toml +5 -1
  8. {prpy-0.4.4 → prpy-0.4.5}/tests/conftest.py +14 -0
  9. {prpy-0.4.4 → prpy-0.4.5}/tests/test_ffmpeg.py +27 -1
  10. {prpy-0.4.4 → prpy-0.4.5}/tests/test_numpy_filters.py +11 -0
  11. prpy-0.4.4/prpy.egg-info/scm_version.json +0 -8
  12. {prpy-0.4.4 → prpy-0.4.5}/.gitignore +0 -0
  13. {prpy-0.4.4 → prpy-0.4.5}/CONTRIBUTING.md +0 -0
  14. {prpy-0.4.4 → prpy-0.4.5}/LICENSE +0 -0
  15. {prpy-0.4.4 → prpy-0.4.5}/MANIFEST.in +0 -0
  16. {prpy-0.4.4 → prpy-0.4.5}/prpy/__init__.py +0 -0
  17. {prpy-0.4.4 → prpy-0.4.5}/prpy/constants.py +0 -0
  18. {prpy-0.4.4 → prpy-0.4.5}/prpy/ffmpeg/__init__.py +0 -0
  19. {prpy-0.4.4 → prpy-0.4.5}/prpy/ffmpeg/readwrite.py +0 -0
  20. {prpy-0.4.4 → prpy-0.4.5}/prpy/ffmpeg/utils.py +0 -0
  21. {prpy-0.4.4 → prpy-0.4.5}/prpy/helpers.py +0 -0
  22. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/__init__.py +0 -0
  23. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/core.py +0 -0
  24. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/detect.py +0 -0
  25. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/face.py +0 -0
  26. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/freq.py +0 -0
  27. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/image.py +0 -0
  28. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/image_ops.c +0 -0
  29. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/interp.py +0 -0
  30. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/metric.py +0 -0
  31. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/physio.py +0 -0
  32. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/rolling.py +0 -0
  33. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/stride_tricks.py +0 -0
  34. {prpy-0.4.4 → prpy-0.4.5}/prpy/numpy/utils.py +0 -0
  35. {prpy-0.4.4 → prpy-0.4.5}/prpy/tensorflow/__init__.py +0 -0
  36. {prpy-0.4.4 → prpy-0.4.5}/prpy/tensorflow/image.py +0 -0
  37. {prpy-0.4.4 → prpy-0.4.5}/prpy/tensorflow/loss.py +0 -0
  38. {prpy-0.4.4 → prpy-0.4.5}/prpy/tensorflow/lr_schedule.py +0 -0
  39. {prpy-0.4.4 → prpy-0.4.5}/prpy/tensorflow/model_saver.py +0 -0
  40. {prpy-0.4.4 → prpy-0.4.5}/prpy/tensorflow/nan.py +0 -0
  41. {prpy-0.4.4 → prpy-0.4.5}/prpy/tensorflow/optimizer.py +0 -0
  42. {prpy-0.4.4 → prpy-0.4.5}/prpy/tensorflow/signal.py +0 -0
  43. {prpy-0.4.4 → prpy-0.4.5}/prpy/torch/__init__.py +0 -0
  44. {prpy-0.4.4 → prpy-0.4.5}/prpy/torch/loss.py +0 -0
  45. {prpy-0.4.4 → prpy-0.4.5}/prpy/torch/lr_schedule.py +0 -0
  46. {prpy-0.4.4 → prpy-0.4.5}/prpy/torch/model_saver.py +0 -0
  47. {prpy-0.4.4 → prpy-0.4.5}/prpy/torch/nan.py +0 -0
  48. {prpy-0.4.4 → prpy-0.4.5}/prpy.egg-info/SOURCES.txt +0 -0
  49. {prpy-0.4.4 → prpy-0.4.5}/prpy.egg-info/dependency_links.txt +0 -0
  50. {prpy-0.4.4 → prpy-0.4.5}/prpy.egg-info/requires.txt +0 -0
  51. {prpy-0.4.4 → prpy-0.4.5}/prpy.egg-info/scm_file_list.json +0 -0
  52. {prpy-0.4.4 → prpy-0.4.5}/prpy.egg-info/top_level.txt +0 -0
  53. {prpy-0.4.4 → prpy-0.4.5}/setup.cfg +0 -0
  54. {prpy-0.4.4 → prpy-0.4.5}/setup.py +0 -0
  55. {prpy-0.4.4 → prpy-0.4.5}/tests/test_numpy_core.py +0 -0
  56. {prpy-0.4.4 → prpy-0.4.5}/tests/test_numpy_detect.py +0 -0
  57. {prpy-0.4.4 → prpy-0.4.5}/tests/test_numpy_face.py +0 -0
  58. {prpy-0.4.4 → prpy-0.4.5}/tests/test_numpy_freq.py +0 -0
  59. {prpy-0.4.4 → prpy-0.4.5}/tests/test_numpy_image.py +0 -0
  60. {prpy-0.4.4 → prpy-0.4.5}/tests/test_numpy_interp.py +0 -0
  61. {prpy-0.4.4 → prpy-0.4.5}/tests/test_numpy_metric.py +0 -0
  62. {prpy-0.4.4 → prpy-0.4.5}/tests/test_numpy_rolling.py +0 -0
  63. {prpy-0.4.4 → prpy-0.4.5}/tests/test_numpy_stride_tricks.py +0 -0
  64. {prpy-0.4.4 → prpy-0.4.5}/tests/test_numpy_utils.py +0 -0
  65. {prpy-0.4.4 → prpy-0.4.5}/tests/test_tensorflow.py +0 -0
  66. {prpy-0.4.4 → prpy-0.4.5}/tests/test_torch.py +0 -0
  67. {prpy-0.4.4 → prpy-0.4.5}/tests/test_torch_loss.py +0 -0
  68. {prpy-0.4.4 → prpy-0.4.5}/tests/test_torch_lr_schedule.py +0 -0
  69. {prpy-0.4.4 → prpy-0.4.5}/tests/test_torch_nan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prpy
3
- Version: 0.4.4
3
+ Version: 0.4.5
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
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: License :: OSI Approved :: MIT License
14
- Requires-Python: >=3.10
14
+ Requires-Python: <3.13,>=3.10
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: numpy>=1.24
@@ -50,8 +50,6 @@ It contains subpackages for working with `numpy`, `ffmpeg`, `tensorflow`, and `t
50
50
 
51
51
  General prerequisites are `python>=3.10` and `ffmpeg` installed and accessible via the `$PATH` environment variable.
52
52
 
53
- - Please note: If using `numpy` or `tensorflow` options, we only support Python `<3.12` because of the dependencies.
54
-
55
53
  The easiest way to install the latest version of `prpy`:
56
54
 
57
55
  ```
@@ -10,8 +10,6 @@ It contains subpackages for working with `numpy`, `ffmpeg`, `tensorflow`, and `t
10
10
 
11
11
  General prerequisites are `python>=3.10` and `ffmpeg` installed and accessible via the `$PATH` environment variable.
12
12
 
13
- - Please note: If using `numpy` or `tensorflow` options, we only support Python `<3.12` because of the dependencies.
14
-
15
13
  The easiest way to install the latest version of `prpy`:
16
14
 
17
15
  ```
@@ -172,9 +172,18 @@ def probe_video(
172
172
 
173
173
  return fps, total_frames, width, height, codec, bitrate, rotation, issues
174
174
 
175
+ def _parse_ffprobe_csv_floats(raw: str) -> np.ndarray:
176
+ """Parse ffprobe's `-of csv=p=0` stdout (one value per line, possibly with a
177
+ trailing comma) into a float array."""
178
+ cleaned = "\n".join(line.rstrip(",") for line in raw.splitlines())
179
+ return np.fromstring(cleaned, dtype=float, sep="\n")
180
+
175
181
  def probe_video_frame_timestamps(path: str, sanity_check: bool = False) -> list:
176
182
  """Probe a video file for a best effort estimate of its frame timestamps.
177
183
 
184
+ Tries reading each packet's pts directly and sorting (no decode needed).
185
+ Falls back to decoding every frame otherwise.
186
+
178
187
  Args:
179
188
  path: The path of the video.
180
189
  sanity_check: Whether to check result against ffprobe count
@@ -184,23 +193,45 @@ def probe_video_frame_timestamps(path: str, sanity_check: bool = False) -> list:
184
193
  if not os.path.exists(path):
185
194
  raise FileNotFoundError(f"File {path} does not exist")
186
195
 
187
- # Build the ffprobe command to extract the best effort timestamp for each frame.
188
- ts_cmd = [
189
- "ffprobe", "-v", "error",
190
- "-select_streams", "v:0",
191
- "-show_entries", "frame=best_effort_timestamp_time",
192
- "-of", "csv=p=0",
193
- path
194
- ]
196
+ timestamps = None
195
197
  try:
196
- proc = subprocess.run(ts_cmd, capture_output=True, text=True, check=True)
197
- raw = proc.stdout
198
+ proc = subprocess.run([
199
+ "ffprobe", "-v", "error",
200
+ "-select_streams", "v:0",
201
+ "-show_entries", "packet=pts_time",
202
+ "-of", "csv=p=0",
203
+ path
204
+ ], capture_output=True, text=True, check=True)
205
+ lines = proc.stdout.splitlines()
206
+ if lines and all(line.rstrip(",") for line in lines):
207
+ candidate = np.sort(_parse_ffprobe_csv_floats(proc.stdout))
208
+ # A handful of tied pts (e.g. a GOP-boundary rounding artifact) is fine
209
+ # -- sorting already guarantees non-decreasing order, so the only real
210
+ # question is whether too many packets are tied to trust the result.
211
+ n_dupes = int(np.sum(np.diff(candidate) <= 0)) if candidate.size else 0
212
+ if candidate.size == len(lines) and n_dupes <= max(1, int(0.01 * candidate.size)):
213
+ timestamps = candidate
214
+ else:
215
+ logging.debug("Packet-level pts unusable (incomplete or too many duplicates); falling back.")
198
216
  except subprocess.CalledProcessError as e:
199
- logging.error("ffprobe error: %s", e)
200
- return []
201
- # Clean trailing commas and process with numpy
202
- cleaned = "\n".join(line.rstrip(",") for line in raw.splitlines())
203
- timestamps = np.fromstring(cleaned, dtype=float, sep="\n")
217
+ logging.debug("Fast packet-level probe failed, falling back: %s", e)
218
+
219
+ if timestamps is None:
220
+ # Fallback: decode every frame to read its best-effort timestamp.
221
+ ts_cmd = [
222
+ "ffprobe", "-v", "error",
223
+ "-select_streams", "v:0",
224
+ "-show_entries", "frame=best_effort_timestamp_time",
225
+ "-of", "csv=p=0",
226
+ path
227
+ ]
228
+ try:
229
+ proc = subprocess.run(ts_cmd, capture_output=True, text=True, check=True)
230
+ raw = proc.stdout
231
+ except subprocess.CalledProcessError as e:
232
+ logging.error("ffprobe error: %s", e)
233
+ return []
234
+ timestamps = _parse_ffprobe_csv_floats(raw)
204
235
 
205
236
  if sanity_check:
206
237
  # Grab the frame count
@@ -21,6 +21,7 @@
21
21
  import math
22
22
  import numpy as np
23
23
  from scipy import signal
24
+ from scipy.linalg import solveh_banded
24
25
  from scipy.ndimage import uniform_filter1d
25
26
  from scipy.sparse import spdiags
26
27
  from typing import Union, Tuple
@@ -151,18 +152,20 @@ def detrend(
151
152
  T = z.shape[axis]
152
153
  if T < 3:
153
154
  return z
154
- # Identity matrix
155
- I = np.identity(T)
156
155
  # Regularization matrix
157
156
  D2 = spdiags(
158
157
  [np.ones(T), -2*np.ones(T), np.ones(T)],
159
- [0, 1, 2], (T-2), T).toarray()
160
- # Inverse of I+lambda^2*D2’*D2
161
- A = I + (Lambda**2) * np.dot(D2.T, D2)
158
+ [0, 1, 2], (T-2), T)
159
+ # I+lambda^2*D2’*D2 is symmetric pentadiagonal, so solve it in banded form
160
+ DTD = (D2.T @ D2).todia()
161
+ ab = np.zeros((3, T))
162
+ ab[2] = 1. + (Lambda**2) * DTD.diagonal(0)
163
+ ab[1, 1:] = (Lambda**2) * DTD.diagonal(1)
164
+ ab[0, 2:] = (Lambda**2) * DTD.diagonal(2)
162
165
  # Compute the detrending operation (vectorized)
163
166
  if axis == 0:
164
167
  z = np.transpose(z)
165
- z_trend = np.linalg.solve(A, z.T)
168
+ z_trend = solveh_banded(ab, z.T)
166
169
  proc_z = (z.T - z_trend)
167
170
  if axis == 1:
168
171
  proc_z = np.transpose(proc_z)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prpy
3
- Version: 0.4.4
3
+ Version: 0.4.5
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
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: License :: OSI Approved :: MIT License
14
- Requires-Python: >=3.10
14
+ Requires-Python: <3.13,>=3.10
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: numpy>=1.24
@@ -50,8 +50,6 @@ It contains subpackages for working with `numpy`, `ffmpeg`, `tensorflow`, and `t
50
50
 
51
51
  General prerequisites are `python>=3.10` and `ffmpeg` installed and accessible via the `$PATH` environment variable.
52
52
 
53
- - Please note: If using `numpy` or `tensorflow` options, we only support Python `<3.12` because of the dependencies.
54
-
55
53
  The easiest way to install the latest version of `prpy`:
56
54
 
57
55
  ```
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.4.5",
3
+ "distance": 0,
4
+ "node": "g063e0301dacb74684528594aff483fb30e31ea65",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-11-08"
8
+ }
@@ -15,7 +15,11 @@ authors = [
15
15
  ]
16
16
  description = "Collection of Python utils for signal, image, and video processing"
17
17
  readme = "README.md"
18
- requires-python = ">=3.10"
18
+ # Upper bound must match the newest version actually tested (main.yml's
19
+ # matrix) and built (release.yml's CIBW_BUILD) - keep all three in sync
20
+ # when adding/dropping a Python version. Without this ceiling, pip will
21
+ # silently attempt an untested sdist build on any newer Python.
22
+ requires-python = ">=3.10,<3.13"
19
23
  keywords = ["python", "numpy", "ffmpeg", "tensorflow", "torch"]
20
24
  license = {text = "MIT License"}
21
25
  classifiers = [
@@ -23,6 +23,7 @@ from PIL import Image
23
23
  import os
24
24
  import pytest
25
25
  import random as rand
26
+ import subprocess
26
27
  import tempfile
27
28
  from typing import Optional, Union
28
29
 
@@ -138,6 +139,19 @@ def sample_video_file(temp_dir):
138
139
  _ffmpeg_output_to_file(stream, output_dir=temp_dir, output_file=filename, overwrite=True)
139
140
  yield os.path.join(temp_dir, filename)
140
141
 
142
+ @pytest.fixture(scope='session')
143
+ def sample_video_file_no_bframes(temp_dir):
144
+ # H264 with B-frames disabled, for probe_video_frame_timestamps' fast path.
145
+ filename = 'test_no_bframes.mp4'
146
+ path = os.path.join(temp_dir, filename)
147
+ subprocess.run([
148
+ 'ffmpeg', '-y', '-v', 'error',
149
+ '-f', 'lavfi', '-i', 'testsrc=size=320x240:rate=25:duration=2',
150
+ '-c:v', 'libx264', '-bf', '0', '-pix_fmt', 'yuv420p',
151
+ path
152
+ ], check=True)
153
+ yield path
154
+
141
155
  @pytest.fixture(scope='session')
142
156
  def sample_video_data():
143
157
  # Create sample stream
@@ -26,7 +26,7 @@ import pytest
26
26
  import sys
27
27
  sys.path.append('../prpy')
28
28
 
29
- from prpy.ffmpeg.probe import probe_video
29
+ from prpy.ffmpeg.probe import probe_video, probe_video_frame_timestamps
30
30
  from prpy.ffmpeg.readwrite import read_video_from_path
31
31
  from prpy.ffmpeg.readwrite import write_video_from_path, write_video_from_numpy
32
32
 
@@ -49,6 +49,32 @@ def test_probe_video(sample_video_file):
49
49
  assert r == SAMPLE_ROTATION
50
50
  assert i == SAMPLE_ISSUES
51
51
 
52
+ def test_probe_video_frame_timestamps_no_bframes_uses_fast_path(sample_video_file_no_bframes, caplog):
53
+ with caplog.at_level("DEBUG"):
54
+ timestamps = probe_video_frame_timestamps(sample_video_file_no_bframes)
55
+ assert "falling back" not in caplog.text
56
+ assert len(timestamps) == 50 # 2s at 25fps
57
+ assert np.all(np.diff(timestamps) > 0)
58
+ np.testing.assert_allclose(timestamps, np.arange(50) / 25., atol=1e-3)
59
+
60
+ def test_probe_video_frame_timestamps_bframes_still_uses_fast_path(sample_video_file, caplog):
61
+ # sample_video_file has B-frames (libx264 defaults); sorting packet pts
62
+ # handles that without needing the decode fallback.
63
+ with caplog.at_level("DEBUG"):
64
+ timestamps = probe_video_frame_timestamps(sample_video_file)
65
+ assert "falling back" not in caplog.text
66
+ assert len(timestamps) == SAMPLE_FRAMES
67
+ assert np.all(np.diff(timestamps) > 0)
68
+ np.testing.assert_allclose(timestamps, np.arange(SAMPLE_FRAMES) / SAMPLE_FPS, atol=1e-3)
69
+
70
+ def test_probe_video_frame_timestamps_sanity_check(sample_video_file_no_bframes, sample_video_file):
71
+ assert len(probe_video_frame_timestamps(sample_video_file_no_bframes, sanity_check=True)) == 50
72
+ assert len(probe_video_frame_timestamps(sample_video_file, sanity_check=True)) == SAMPLE_FRAMES
73
+
74
+ def test_probe_video_frame_timestamps_missing_file():
75
+ with pytest.raises(FileNotFoundError):
76
+ probe_video_frame_timestamps("does_not_exist.mp4")
77
+
52
78
  @pytest.mark.parametrize("target_fps", [25., 8.])
53
79
  @pytest.mark.parametrize("crop", [None, (256, 94, 416, 214)])
54
80
  @pytest.mark.parametrize("scale", [None, 30, (40, 40)])
@@ -134,6 +134,17 @@ def test_detrend():
134
134
  [-.0582996, .03747831, -.27848467, .31075766, .33470214]]),
135
135
  rtol=1e-6)
136
136
 
137
+ @pytest.mark.parametrize("T", [3, 4, 10, 500])
138
+ def test_detrend_matches_dense_solution(T):
139
+ rng = np.random.default_rng(T)
140
+ z = np.cumsum(rng.normal(size=(2, T)), axis=1)
141
+ D2 = np.diff(np.identity(T), n=2, axis=0)
142
+ A = np.identity(T) + 25. * D2.T @ D2
143
+ expected = z - np.linalg.solve(A, z.T).T
144
+ np.testing.assert_allclose(detrend(z=z, Lambda=5., axis=-1), expected, rtol=1e-8, atol=1e-10)
145
+ np.testing.assert_allclose(detrend(z=z.T, Lambda=5., axis=0), expected.T, rtol=1e-8, atol=1e-10)
146
+ np.testing.assert_allclose(detrend(z=z[0], Lambda=5.), expected[0], rtol=1e-8, atol=1e-10)
147
+
137
148
  def test_detrend_frequency_response():
138
149
  # Example from the paper
139
150
  f_s = 4
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.4.4",
3
- "distance": 0,
4
- "node": "g47e51af67da79b0b9e7a69027b4f7406cbf6fd63",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-24"
8
- }
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
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