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 +19 -0
- prpy/constants.py +24 -0
- prpy/ffmpeg/__init__.py +19 -0
- prpy/ffmpeg/probe.py +86 -0
- prpy/ffmpeg/readwrite.py +412 -0
- prpy/ffmpeg/utils.py +81 -0
- prpy/numpy/__init__.py +19 -0
- prpy/numpy/face.py +213 -0
- prpy/numpy/image.py +141 -0
- prpy/numpy/metric.py +179 -0
- prpy/numpy/signal.py +649 -0
- prpy/numpy/stride_tricks.py +182 -0
- prpy/tensorflow/__init__.py +19 -0
- prpy/tensorflow/image.py +203 -0
- prpy/tensorflow/loss.py +104 -0
- prpy/tensorflow/lr_schedule.py +74 -0
- prpy/tensorflow/model_saver.py +203 -0
- prpy/tensorflow/nan.py +226 -0
- prpy/tensorflow/optimizer.py +233 -0
- prpy/tensorflow/signal.py +103 -0
- prpy/torch/__init__.py +19 -0
- prpy/torch/model_saver.py +208 -0
- prpy-0.2.2.dist-info/LICENSE +19 -0
- prpy-0.2.2.dist-info/METADATA +75 -0
- prpy-0.2.2.dist-info/RECORD +27 -0
- prpy-0.2.2.dist-info/WHEEL +5 -0
- prpy-0.2.2.dist-info/top_level.txt +1 -0
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
|
prpy/ffmpeg/__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/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
|
+
|
prpy/ffmpeg/readwrite.py
ADDED
|
@@ -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.
|