prpy 0.2.2__py3-none-any.whl

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.
prpy/__init__.py ADDED
@@ -0,0 +1,19 @@
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.
prpy/constants.py ADDED
@@ -0,0 +1,24 @@
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
+ NANOS_PER_SECOND = 1000000000.0
22
+ MICROS_PER_SECOND = 1000000.0
23
+ MILLIS_PER_SECOND = 1000.0
24
+ SECONDS_PER_MINUTE = 60.0
@@ -0,0 +1,19 @@
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.
prpy/ffmpeg/probe.py ADDED
@@ -0,0 +1,86 @@
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 ffmpeg
22
+ from fractions import Fraction
23
+ import logging
24
+ from typing import Tuple
25
+ import os
26
+
27
+ def probe_video(
28
+ path: str
29
+ ) -> Tuple[float, int, int, int, str, float, int]:
30
+ """Probe a video file for metadata.
31
+
32
+ Args:
33
+ path: The path of the video.
34
+ Returns:
35
+ fps: The frame rate of the video
36
+ total_frames: The total number of frames
37
+ width: The width dimension of the video
38
+ height: The height dimension of the video
39
+ codec: The codec of the video
40
+ bitrate: The bitrate of the video
41
+ rotation: The rotation of the video
42
+ """
43
+ # Check if file exists
44
+ assert isinstance(path, str)
45
+ if not os.path.exists(path):
46
+ raise FileNotFoundError("File {} does not exist".format(path))
47
+ # ffprobe -show_streams -count_frames -pretty video.mp4
48
+ try:
49
+ probe = ffmpeg.probe(filename=path)
50
+ except Exception as e:
51
+ # The exception returned by `ffprobe` is in bytes
52
+ logging.warn("Exception probing video: {}".format(e))
53
+ else:
54
+ video_stream = next(
55
+ (
56
+ stream
57
+ for stream in probe["streams"]
58
+ if stream["codec_type"] == "video"
59
+ ),
60
+ None,
61
+ )
62
+ try:
63
+ fps = float(Fraction(video_stream["avg_frame_rate"]))
64
+ except Exception as e:
65
+ fps = 0
66
+ try:
67
+ total_frames = int(video_stream["nb_frames"])
68
+ except Exception as e:
69
+ duration = float(video_stream['duration'])
70
+ total_frames = int(duration*fps)
71
+ width = video_stream["width"]
72
+ height = video_stream["height"]
73
+ codec = video_stream["codec_name"]
74
+ try:
75
+ bitrate = float(video_stream["bit_rate"])/1000.0
76
+ except Exception as e:
77
+ bitrate = 0.0
78
+ rotation = 0
79
+ if 'tags' in video_stream and 'rotate' in video_stream['tags']:
80
+ # Regular
81
+ rotation = int(video_stream['tags']['rotate'])
82
+ elif 'side_data_list' in video_stream and 'rotation' in video_stream['side_data_list'][0]:
83
+ # iPhone
84
+ rotation = int(video_stream['side_data_list'][0]['rotation'])
85
+ return fps, total_frames, width, height, codec, bitrate, rotation
86
+
@@ -0,0 +1,412 @@
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 ffmpeg
22
+ import logging
23
+ import numpy as np
24
+ import os
25
+ from typing import Tuple, Union
26
+
27
+ from prpy.ffmpeg.probe import probe_video
28
+ from prpy.ffmpeg.utils import find_factors_near
29
+
30
+ def _ffmpeg_input_from_path(
31
+ path: str,
32
+ fps: Union[float, int],
33
+ trim: tuple
34
+ ) -> ffmpeg.nodes.FilterableStream:
35
+ """Use file as input part of ffmpeg command.
36
+
37
+ Args:
38
+ path: The path from which video will be read.
39
+ fps: The framerate of the input video.
40
+ trim: Frame numbers for temporal trimming (start, end).
41
+ Returns:
42
+ stream: ffmpeg input stream from file
43
+ """
44
+ assert isinstance(path, str)
45
+ assert isinstance(fps, (float, int))
46
+ assert trim is None or (isinstance(trim, tuple) and all(isinstance(i, int) for i in trim))
47
+ trim_start = 0 if trim is None else trim[0]
48
+ # Create the stream
49
+ stream = ffmpeg.input(filename=path, ss=trim_start/fps)
50
+ # Return
51
+ return stream
52
+
53
+ def _ffmpeg_input_from_pipe() -> ffmpeg.nodes.FilterableStream:
54
+ """Use pipe as input part of ffmpeg command.
55
+
56
+ Returns:
57
+ stream: ffmpeg input stream from pipe
58
+ """
59
+ stream = ffmpeg.input("pipe:")
60
+ return stream
61
+
62
+ def _ffmpeg_input_from_numpy(
63
+ w: int,
64
+ h: int,
65
+ fps: Union[float, int],
66
+ pix_fmt: str
67
+ ) -> ffmpeg.nodes.FilterableStream:
68
+ """Use file as input part of ffmpeg command.
69
+
70
+ Args:
71
+ w: The width dimension of the video data from numpy.
72
+ h: The height dimension of the video data from numpy.
73
+ fps: The framerate of the video data from numpy.
74
+ pix_fmt: The pixel format of the video data from numpy (e.g., `bgr24`).
75
+ Returns:
76
+ stream: ffmpeg input stream from pipe (numpy)
77
+ """
78
+ assert isinstance(w, int)
79
+ assert isinstance(h, int)
80
+ assert isinstance(fps, (float, int))
81
+ assert isinstance(pix_fmt, str)
82
+ stream = ffmpeg.input('pipe:', format='rawvideo', pix_fmt=pix_fmt, s='{}x{}'.format(w, h), r=fps)
83
+ return stream
84
+
85
+ def _ffmpeg_filtering(
86
+ stream: ffmpeg.nodes.FilterableStream,
87
+ fps: Union[float, int],
88
+ n: int,
89
+ w: int,
90
+ h: int,
91
+ target_fps: Union[float, int, None] = None,
92
+ crop: Union[tuple, None] = None,
93
+ scale: Union[Tuple[int, tuple], None] = None,
94
+ trim: Union[tuple, None] = None,
95
+ preserve_aspect_ratio: bool = False,
96
+ scale_algorithm: str = 'bicubic'
97
+ ) -> Tuple[ffmpeg.nodes.FilterableStream, int, int, int, int]:
98
+ """Take an ffmpeg stream and optionally add filtering operations.
99
+ Downsampling, spatial cropping, spatial scaling (applied to result of
100
+ cropping if specified) and temporal trimming.
101
+
102
+ Args:
103
+ stream: The ffmpeg stream
104
+ fps: The existing frame rate
105
+ n: The existing number of frames
106
+ w: The existing width
107
+ h: The existing height
108
+ target_fps: Try to downsample frames to achieve this framerate (optional)
109
+ crop: Coords and sizes for spatial cropping (x, y, width, height) (optional)
110
+ scale: Size(s) for spatial scaling. Scalar or (width, height) (optional)
111
+ trim: Frame numbers for temporal trimming (start, end) (optional)
112
+ preserve_aspect_ratio: Preserve the aspect ratio if scaling
113
+ scale_algorithm: The algorithm used for scaling.
114
+ Supported: bicubic, bilinear, area, lanczos. Default: bicubic
115
+ Returns:
116
+ stream: The modified ffmpeg stream
117
+ target_n: The target number of frames
118
+ target_w: The target width
119
+ target_h: The target shape
120
+ ds_factor: The applied downsampling factor
121
+ """
122
+ assert isinstance(stream, ffmpeg.nodes.FilterableStream)
123
+ assert isinstance(fps, (float, int))
124
+ assert isinstance(n, int)
125
+ assert isinstance(w, int)
126
+ assert isinstance(h, int)
127
+ assert target_fps is None or isinstance(target_fps, (float, int))
128
+ assert crop is None or (isinstance(crop, tuple) and len(crop) == 4 and all(isinstance(i, int) for i in crop))
129
+ assert scale is None or isinstance(scale, int) or (isinstance(scale, tuple) and len(scale) == 2 and all(isinstance(i, int) for i in scale))
130
+ assert trim is None or (isinstance(trim, tuple) and len(trim) == 2 and all(isinstance(i, int) for i in trim))
131
+ assert isinstance(preserve_aspect_ratio, bool)
132
+ assert isinstance(scale_algorithm, str)
133
+ ds_factor = 1
134
+ if target_fps is not None and target_fps > fps: logging.warn("target_fps should not be greater than fps. Ignoring.")
135
+ elif target_fps is not None: ds_factor = int(fps // target_fps)
136
+ # Target number of frames
137
+ target_n = trim[1] - trim[0] if trim is not None else n
138
+ target_n = int(target_n / ds_factor)
139
+ # Target size after taking into account cropping
140
+ target_w = crop[2] if crop is not None else w
141
+ target_h = crop[3] if crop is not None else h
142
+ # Target size after taking into account scaling
143
+ if scale not in [None, 0]:
144
+ if isinstance(scale, int): scale = (scale, scale)
145
+ if preserve_aspect_ratio:
146
+ scale_ratio = max(scale) / max(target_w, target_h)
147
+ target_w = int(target_w * scale_ratio)
148
+ target_h = int(target_h * scale_ratio)
149
+ else:
150
+ target_w, target_h = scale
151
+ # Trimming
152
+ if trim is not None:
153
+ stream = stream.trim(start_frame=0, end_frame=trim[1]-trim[0])
154
+ stream = stream.setpts('PTS-STARTPTS')
155
+ # Downsampling
156
+ if ds_factor > 1:
157
+ stream = ffmpeg.filter(stream, 'select', 'not(mod(n,{}))'.format(ds_factor))
158
+ # Cropping
159
+ if crop is not None:
160
+ stream = stream.crop(crop[0], crop[1], crop[2], crop[3])
161
+ # Scaling
162
+ # http://trac.ffmpeg.org/wiki/Scaling#Specifyingscalingalgorithm
163
+ if scale not in [None, (0, 0)]:
164
+ stream = ffmpeg.filter(stream, 'scale', target_w, target_h, scale_algorithm)
165
+ # Return
166
+ return stream, target_n, target_w, target_h, ds_factor
167
+
168
+ def _ffmpeg_output_to_numpy(
169
+ stream: ffmpeg.nodes.FilterableStream,
170
+ r: int,
171
+ fps: Union[float, int, None],
172
+ n: int,
173
+ w: int,
174
+ h: int,
175
+ scale: Union[tuple, int, None] = None,
176
+ crf: Union[int, None] = None,
177
+ pix_fmt: str = 'bgr24',
178
+ preserve_aspect_ratio: bool = False,
179
+ scale_algorithm: str = 'bicubic',
180
+ dim_deltas: tuple = (0, 0, 0)
181
+ ) -> np.ndarray:
182
+ """Run the stream and capture the raw video output in a numpy array.
183
+
184
+ Args:
185
+ stream: The ffmpeg stream
186
+ r: Rotation present in the video (after applying stream)
187
+ fps: Framerate attempted to create in stream.
188
+ n: Number of frames attempted to create in stream.
189
+ w: Frame width attempted to create in stream.
190
+ h: Frame height attempted to create in stream.
191
+ scale: Size(s) for spatial scaling. Scalar or (width, height) (optional)
192
+ crf: Constant rate factor for H.264 encoding (higher = more compression)
193
+ If not None, need to run the stream to encode before capturing to np.
194
+ pix_fmt: Pixel format to read into.
195
+ preserve_aspect_ratio: Preserve the aspect ratio if scaling
196
+ scale_algorithm: The algorithm used for scaling.
197
+ Supported: bicubic, bilinear, area, lanczos. Default: bicubic
198
+ dim_deltas: Allowed deviation from target (n_frames, height, width)
199
+ Returns:
200
+ frames: The video frames in shape (n, h, w, c)
201
+ """
202
+ assert isinstance(stream, ffmpeg.nodes.FilterableStream)
203
+ assert isinstance(r, int)
204
+ assert fps is None or isinstance(fps, (float, int))
205
+ assert isinstance(n, int)
206
+ assert isinstance(w, int)
207
+ assert isinstance(h, int)
208
+ assert crf is None or isinstance(crf, int)
209
+ assert isinstance(pix_fmt, str)
210
+ assert isinstance(dim_deltas, tuple) and len(dim_deltas) == 3 and all(isinstance(i, int) for i in dim_deltas)
211
+ if crf is None:
212
+ # Run stream straight to raw video
213
+ stream = stream.output("pipe:", vsync=0, format="rawvideo", pix_fmt=pix_fmt)
214
+ stream = stream.global_args("-loglevel", "panic", "-hide_banner", "-nostdin", "-nostats")
215
+ out, _ = stream.run(capture_stdout=True, capture_stderr=True)
216
+ else:
217
+ # Run stream to encode H264 with crf
218
+ stream = stream.output("pipe:", vsync=0, format='rawvideo', vcodec='libx264', crf=crf)
219
+ out, _ = stream.run(capture_stdout=True, capture_stderr=True)
220
+ # Run stream to decode H264 to raw video
221
+ stream = _ffmpeg_input_from_pipe()
222
+ stream, _, w, h, _ = _ffmpeg_filtering(
223
+ stream, fps=fps, n=n, w=w, h=h, scale=scale,
224
+ preserve_aspect_ratio=preserve_aspect_ratio, scale_algorithm=scale_algorithm)
225
+ stream = stream.output("pipe:", vsync=0, format="rawvideo", pix_fmt=pix_fmt)
226
+ out, _ = stream.run(input=out, capture_stdout=True, capture_stderr=True)
227
+ # Swap h and w if necessary -> not needed if scaled!
228
+ if r != 0:
229
+ if abs(r) == 90:
230
+ logging.warn("Rotation {} present in video fixed; results in W and H swapped.".format(r))
231
+ w, h = h, w
232
+ else:
233
+ logging.warn("Rotation {} present in video; Fixing is not yet supported.".format(r))
234
+ # Parse result
235
+ frames = np.frombuffer(out, np.uint8)
236
+ adj_n, adh_h, adh_w = find_factors_near(
237
+ frames.shape[0]//3, n, h, w, dim_deltas[0], dim_deltas[1], dim_deltas[2])
238
+ assert adj_n * adh_h * adh_w * 3 == frames.shape[0]
239
+ frames = frames.reshape([adj_n, adh_h, adh_w, 3])
240
+ # Return
241
+ return frames
242
+
243
+ def _ffmpeg_output_to_file(
244
+ stream: ffmpeg.nodes.FilterableStream,
245
+ output_dir: str,
246
+ output_file: str,
247
+ from_stdin: Union[bytes, None] = None,
248
+ pix_fmt: str = 'yuv420p',
249
+ crf: int = 12,
250
+ overwrite: bool = False
251
+ ):
252
+ """Run the stream and encode to file as H264.
253
+
254
+ Args:
255
+ stream: The ffmpeg stream
256
+ output_dir: The directory where the video will be written
257
+ ourput_file: The filename as which the video will be written
258
+ from_stdin: Byte buffer to pipe data from (optional)
259
+ pix_fmt: Pixel format to write into
260
+ crf: Constant rate factor for H.264 encoding (higher = more compression)
261
+ overwrite: Overwrite if file exists?
262
+ """
263
+ assert isinstance(stream, ffmpeg.nodes.FilterableStream)
264
+ assert isinstance(output_dir, str)
265
+ assert isinstance(output_file, str)
266
+ assert from_stdin is None or isinstance(from_stdin, bytes)
267
+ assert isinstance(pix_fmt, str)
268
+ assert crf is None or isinstance(crf, int)
269
+ assert isinstance(overwrite, bool)
270
+ output_path = os.path.join(output_dir, output_file)
271
+ stream = ffmpeg.output(stream, output_path, pix_fmt=pix_fmt, crf=crf)
272
+ if overwrite:
273
+ stream = stream.global_args("-vsync", "2", "-y")
274
+ else:
275
+ stream = stream.global_args("-vsync", "2")
276
+ if from_stdin is None:
277
+ stream.run(quiet=True)
278
+ else:
279
+ process = stream.run_async(pipe_stdin=True, quiet=True)
280
+ process.communicate(input=from_stdin)
281
+
282
+ def read_video_from_path(
283
+ path: str,
284
+ target_fps: Union[float, None] = None,
285
+ crop: Union[tuple, None] = None,
286
+ scale: Union[int, tuple, None] = None,
287
+ trim: Union[tuple, None] = None,
288
+ crf: Union[int, None] = None,
289
+ pix_fmt: str = 'bgr24',
290
+ preserve_aspect_ratio: bool = False,
291
+ scale_algorithm: str = 'bicubic',
292
+ order: str = 'scale_crf',
293
+ dim_deltas: tuple = (0, 0, 0)
294
+ ) -> Tuple[np.ndarray, int]:
295
+ """Read a video from path into a numpy array.
296
+ Optionally transformed by downsampling, spatial cropping, spatial scaling
297
+ (applied to result of cropping if specified), temporal trimming, and
298
+ intermediate encoding.
299
+
300
+ Args:
301
+ path: The path from which video will be read.
302
+ target_fps: Try to downsample frames to achieve this framerate.
303
+ crop: Coords and sizes for spatial cropping (x, y, width, height) (optional).
304
+ scale: Size(s) for spatial scaling. Scalar or (width, height) (optional).
305
+ trim: Frame numbers for temporal trimming (start, end) (optional).
306
+ crf: Constant rate factor for H.264 encoding (higher = more compression)
307
+ If specified, do intermediate encoding, otherwise ignore.
308
+ pix_fmt: Pixel format to read into.
309
+ preserve_aspect_ratio: Preserve the aspect ratio if scaling.
310
+ scale_algorithm: The algorithm used for scaling.
311
+ Supported: bicubic, bilinear, area, lanczos. Default: bicubic
312
+ order: scale_crf or crf_scale - specifies order of application
313
+ dim_deltas: Allowed deviation from target (n_frames, height, width)
314
+ Returns:
315
+ frames: The video frames (n, h, w, 3)
316
+ ds_factor: The applied downsampling factor
317
+ """
318
+ assert isinstance(path, str)
319
+ # Check if file exists
320
+ if not os.path.exists(path):
321
+ raise FileNotFoundError("File {} does not exist".format(path))
322
+ # Get metadata of original video
323
+ fps, n, w, h, _, _, r = probe_video(path=path)
324
+ # Input
325
+ stream = _ffmpeg_input_from_path(path=path, fps=fps, trim=trim)
326
+ # Filtering
327
+ scale_0 = scale if order == 'scale_crf' or crf == None else 0
328
+ stream, target_n, target_w, target_h, ds_factor = _ffmpeg_filtering(
329
+ stream=stream, fps=fps, n=n, w=w, h=h, target_fps=target_fps, crop=crop, scale=scale_0,
330
+ trim=trim, preserve_aspect_ratio=preserve_aspect_ratio, scale_algorithm=scale_algorithm)
331
+ # Save whether rotation still present
332
+ if scale not in [None, 0] or crop is not None: r = 0
333
+ # Output
334
+ scale_1 = 0 if order == 'scale_crf' or crf == None else scale
335
+ frames = _ffmpeg_output_to_numpy(
336
+ stream=stream, r=r, fps=target_fps, n=target_n, w=target_w, h=target_h,
337
+ scale=scale_1, crf=crf, pix_fmt=pix_fmt, scale_algorithm=scale_algorithm,
338
+ dim_deltas=dim_deltas)
339
+ # Return
340
+ return frames, ds_factor
341
+
342
+ def write_video_from_path(
343
+ path: str,
344
+ output_dir: str,
345
+ output_file: str,
346
+ target_fps: Union[float, None] = None,
347
+ crop: Union[tuple, None] = None,
348
+ scale: Union[int, tuple, None] = None,
349
+ trim: Union[tuple, None] = None,
350
+ pix_fmt: str = 'yuv420p',
351
+ crf: int = 12,
352
+ preserve_aspect_ratio: bool = False,
353
+ scale_algorithm: str = 'bicubic',
354
+ overwrite: bool = False
355
+ ):
356
+ """Read a video from path and write back to a video file.
357
+ Optionally transformed by downsampling, spatial cropping, spatial scaling
358
+ (applied to result of cropping if specified), and temporal trimming.
359
+
360
+ Args:
361
+ path: The path from which video will be read.
362
+ output_dir: The directory where the video will be written.
363
+ ourput_file: The filename as which the video will be written.
364
+ target_fps: Try to downsample frames to achieve this framerate (optional).
365
+ crop: Coords and sizes for spatial cropping (x, y, width, height) (optional).
366
+ scale: Size(s) for spatial scaling. Scalar or (width, height) (optional).
367
+ trim: Frame numbers for temporal trimming (start, end) (optional).
368
+ crf: Constant rate factor for H.264 encoding (higher = more compression).
369
+ preserve_aspect_ratio: Preserve the aspect ratio if scaling.
370
+ scale_algorithm: The algorithm used for scaling. Default: bicubic
371
+ """
372
+ # Get metadata of original video
373
+ fps, n, w, h, _, _, r = probe_video(path=path)
374
+ # Input
375
+ stream = _ffmpeg_input_from_path(path=path, fps=fps, trim=trim)
376
+ # Filtering
377
+ stream, _, _, _, _ = _ffmpeg_filtering(
378
+ stream=stream, fps=fps, n=n, w=w, h=h, target_fps=target_fps, crop=crop, scale=scale,
379
+ trim=trim, preserve_aspect_ratio=preserve_aspect_ratio, scale_algorithm=scale_algorithm)
380
+ # Output
381
+ _ffmpeg_output_to_file(
382
+ stream, output_dir=output_dir, output_file=output_file, pix_fmt=pix_fmt, crf=crf, overwrite=overwrite)
383
+
384
+ def write_video_from_numpy(
385
+ data: np.ndarray,
386
+ fps: Union[float, int],
387
+ pix_fmt: str,
388
+ output_dir: str,
389
+ output_file: str,
390
+ out_pix_fmt: str = 'yuv420p',
391
+ crf: int = 12,
392
+ overwrite: bool = False
393
+ ):
394
+ """Write data from a numpy array to a video file.
395
+
396
+ Args:
397
+ data: The numpy array with video frames. Shape (n, h, w, 3)
398
+ fps: The frame rate
399
+ pix_fmt: The pixel format of `data`
400
+ output_dir: The directory where the video will be written
401
+ ourput_file: The filename as which the video will be written
402
+ pix_fmt: The pixel format for the output video
403
+ crf: Constant rate factor for H.264 encoding (higher = more compression)
404
+ overwrite: Overwrite if file exists?
405
+ """
406
+ assert isinstance(data, np.ndarray)
407
+ _, h, w, _ = data.shape
408
+ stream = _ffmpeg_input_from_numpy(w=w, h=h, fps=fps, pix_fmt=pix_fmt)
409
+ buffer = data.flatten().tobytes()
410
+ _ffmpeg_output_to_file(
411
+ stream, output_dir=output_dir, output_file=output_file, from_stdin=buffer,
412
+ crf=crf, pix_fmt=out_pix_fmt, overwrite=overwrite)
prpy/ffmpeg/utils.py ADDED
@@ -0,0 +1,81 @@
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 ffmpeg
22
+ import itertools
23
+ import logging
24
+ from typing import Tuple
25
+
26
+ def find_factors_near(
27
+ i: int,
28
+ f1: int,
29
+ f2: int,
30
+ f3: int,
31
+ max_delta_1: int,
32
+ max_delta_2: int,
33
+ max_delta_3: int
34
+ ) -> Tuple[int, int, int]:
35
+ """Search for factors of a number near provided approximate factors.
36
+ Used to determine dim of filtered video which can minimally deviate from targets
37
+
38
+ Args:
39
+ i: The number to be factorized
40
+ f1: The first factor to search nearby
41
+ f2: The second factor to search nearby
42
+ f3: The third factor to search nearby
43
+ max_delta_1: Maximum deviation allowed for f1
44
+ max_delta_2: Maximum deviation allowed for f2
45
+ max_delta_3: Maximum deviation allowed for f3
46
+ Returns:
47
+ f1: The actual first factor
48
+ f2: The actual second factor
49
+ f3: The actual third factor
50
+ """
51
+ assert isinstance(i, int)
52
+ assert isinstance(f1, int)
53
+ assert isinstance(f2, int)
54
+ assert isinstance(f3, int)
55
+ assert isinstance(max_delta_1, int)
56
+ assert isinstance(max_delta_2, int)
57
+ assert isinstance(max_delta_3, int)
58
+ # Iterative deepening
59
+ for delta in range(max(max_delta_1, max_delta_2, max_delta_3)+1):
60
+ delta_1 = min(delta, max_delta_1)
61
+ delta_2 = min(delta, max_delta_2)
62
+ delta_3 = min(delta, max_delta_3)
63
+ ts = [(t1, t2, t3) for t1, t2, t3 in list(itertools.product( \
64
+ range(f1-delta_1, f1+delta_1+1), range(f2-delta_2, f2+delta_2+1), range(f3-delta_3, f3+delta_3+1)))]
65
+ for t1, t2, t3 in ts:
66
+ if t1 * t2 * t3 == i:
67
+ return t1, t2, t3
68
+ logging.error("Total={}; Failed to find factors near f1={} f2={} f3={} at delta=({}, {}, {})".format(i, f1, f2, f3, max_delta_1, max_delta_2, max_delta_3))
69
+ raise RuntimeError("Could not find factors near the provided values")
70
+
71
+ def create_test_video_stream(t: int) -> ffmpeg.nodes.FilterableStream:
72
+ """Create an ffmpeg video stream for testing.
73
+ Like `ffmpeg -f lavfi -i testsrc -t 30 -pix_fmt yuv420p testsrc.mp4`
74
+
75
+ Args:
76
+ t: The test stream time in seconds
77
+ Returns:
78
+ stream: The test stream
79
+ """
80
+ stream = ffmpeg.input('testsrc', f='lavfi', t=t)
81
+ return stream
prpy/numpy/__init__.py ADDED
@@ -0,0 +1,19 @@
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.