10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149 | def videograms_ffmpeg(
self,
target_name_x: str | None = None,
target_name_y: str | None = None,
overwrite: bool = True,
mode: str = "average",
line_x: int | None = None,
line_y: int | None = None,
) -> "MgList":
"""
Renders horizontal and vertical videograms of the source video using ffmpeg.
By default, videoframes are averaged by axes. Alternatively, ``mode='slit'``
samples a single column and row per frame (photo-finish style) and stacks
those over time.
Args:
target_name_x (str, optional): Target output name for the vertical videogram (the x-axis collapse). Defaults to None (which assumes that the input filename with the suffix "_vgv" should be used).
target_name_y (str, optional): Target output name for the horizontal videogram (the y-axis collapse). Defaults to None (which assumes that the input filename with the suffix "_vgh" should be used).
overwrite (bool, optional): Whether to allow overwriting existing files or to automatically increment target filenames to avoid overwriting. Defaults to True.
mode (str, optional): Either ``'average'`` (default) or ``'slit'``.
``'slit'`` uses one column/row line per frame instead of averaging.
line_x (int, optional): Source x-position (column index) used in slit
mode for the vertical videogram. Defaults to the center column.
line_y (int, optional): Source y-position (row index) used in slit mode
for the horizontal videogram. Defaults to the center row.
Returns:
MgList: An MgList with the MgImage objects referring to the vertical and horizontal videograms respectively.
"""
width, height = get_widthheight(self.filename)
framecount = get_framecount(self.filename)
mode = mode.lower()
if mode not in ["average", "slit"]:
raise ValueError("mode must be 'average' or 'slit'.")
def _resolve_line(line: int | None, max_size: int, name: str, default: int) -> int:
if line is None:
return default
if isinstance(line, bool) or not isinstance(line, int):
raise ValueError(f"{name} must be an integer in the range [0, {max_size - 1}].")
if line < 0 or line >= max_size:
raise ValueError(f"{name} must be an integer in the range [0, {max_size - 1}].")
return line
slit_x = width // 2
slit_y = height // 2
if mode == "slit":
slit_x = _resolve_line(line_x, width, "line_x", width // 2)
slit_y = _resolve_line(line_y, height, "line_y", height // 2)
def calc_skipfactor(width, height, framecount):
"""
Helper function to calculate the necessary frame-skipping to avoid integer overflow. This makes sure that we can succesfully create videograms even on many-hours-long videos as well.
Args:
width (int): The width of the video.
height (int): The height of the video.
framecount (int): The number of frames in the video.
Returns:
list(int, int): The necessary dilation factors to apply on the video for the horizontal and vertical videograms, respectively.
"""
intmax = 2147483647
skipfactor_x = int(
math.ceil(framecount*8 / (intmax / (height+128) - 1024)))
skipfactor_y = int(
math.ceil(framecount / (intmax / ((width*8)+1024) - 128)))
return skipfactor_x, skipfactor_y
testx, testy = calc_skipfactor(width, height, framecount)
if mode == "average":
suffix_x, suffix_y = "_vgv.png", "_vgh.png"
else:
suffix_x, suffix_y = "_vgv_slit.png", "_vgh_slit.png"
def _filters(framecount: int) -> tuple[str, str]:
if mode == "average":
return (
f"scale=1:{height}:sws_flags=area,normalize,tile={framecount}x1",
f"scale={width}:1:sws_flags=area,normalize,tile=1x{framecount}",
)
return (
f"format=rgb24,crop=1:{height}:{slit_x}:0,normalize,tile={framecount}x1",
f"format=rgb24,crop={width}:1:0:{slit_y},normalize,tile=1x{framecount}",
)
if testx > 1 or testy > 1:
necessary_skipfactor = max([testx, testy])
print(f'{os.path.basename(self.filename)} is too large to process. Applying minimal skipping necessary...')
shortened_file = skip_frames_ffmpeg(self.filename, skip=necessary_skipfactor-1)
skip_of = os.path.splitext(shortened_file)[0]
framecount = get_framecount(shortened_file)
length = get_length(shortened_file)
vf_y, vf_x = _filters(framecount)
target_name_x = resolve_filename(skip_of, suffix_x, target_name_x, overwrite)
target_name_y = resolve_filename(skip_of, suffix_y, target_name_y, overwrite)
cmd = ['ffmpeg', '-y', '-i', shortened_file, '-vf',
vf_y, '-aspect', f'{framecount}:{height}', '-frames', '1', target_name_y]
ffmpeg_cmd(cmd, length, stream=False, pb_prefix="Rendering horizontal videogram:")
cmd = ['ffmpeg', '-y', '-i', shortened_file, '-vf',
vf_x, '-aspect', f'{width}:{framecount}', '-frames', '1', target_name_x]
ffmpeg_cmd(cmd, length, stream=False, pb_prefix="Rendering vertical videogram:")
# save results as MgImages at self.video_gram_x and self.video_gram_y for parent MgObject
self.videogram_x_image = MgImage(target_name_x)
self.videogram_y_image = MgImage(target_name_y)
# return MgList([MgImage(target_name_x), MgImage(target_name_y)])
return MgList(self.videogram_x_image, self.videogram_y_image)
else:
length = get_length(self.filename)
vf_y, vf_x = _filters(framecount)
target_name_x = resolve_filename(self.of, suffix_x, target_name_x, overwrite)
target_name_y = resolve_filename(self.of, suffix_y, target_name_y, overwrite)
cmd = ['ffmpeg', '-y', '-i', self.filename, '-frames', '1', '-vf',
vf_y, '-aspect', f'{framecount}:{height}', target_name_y]
ffmpeg_cmd(cmd, length, stream=False, pb_prefix="Rendering horizontal videogram:")
cmd = ['ffmpeg', '-y', '-i', self.filename, '-frames', '1', '-vf',
vf_x, '-aspect', f'{width}:{framecount}', target_name_x]
ffmpeg_cmd(cmd, length, stream=False, pb_prefix="Rendering vertical videogram:")
# save results as MgImages at self.videogram_x_image and self.videogram_y_image for parent MgObject
self.videogram_x_image = MgImage(target_name_x)
self.videogram_y_image = MgImage(target_name_y)
# return MgList([MgImage(target_name_x), MgImage(target_name_y)])
return MgList(self.videogram_x_image, self.videogram_y_image)
|