|
6 | 6 | from fractions import Fraction
|
7 | 7 | from typing import TYPE_CHECKING
|
8 | 8 |
|
| 9 | +import av |
9 | 10 | import numpy as np
|
| 11 | +from av.subtitles.subtitle import AssSubtitle |
10 | 12 |
|
11 | 13 | from auto_editor import version
|
12 | 14 | from auto_editor.utils.subtitle_tools import convert_ass_to_text
|
13 | 15 | from auto_editor.wavfile import read
|
14 | 16 |
|
15 | 17 | if TYPE_CHECKING:
|
| 18 | + from collections.abc import Iterator |
16 | 19 | from fractions import Fraction
|
17 | 20 | from typing import Any
|
18 | 21 |
|
@@ -86,6 +89,53 @@ def obj_tag(tag: str, tb: Fraction, obj: dict[str, Any]) -> str:
|
86 | 89 | return key
|
87 | 90 |
|
88 | 91 |
|
| 92 | +def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[float]: |
| 93 | + container = av.open(src.path, "r") |
| 94 | + |
| 95 | + video = container.streams.video[stream] |
| 96 | + video.thread_type = "AUTO" |
| 97 | + |
| 98 | + prev_frame = None |
| 99 | + current_frame = None |
| 100 | + total_pixels = src.videos[0].width * src.videos[0].height |
| 101 | + index = 0 |
| 102 | + prev_index = -1 |
| 103 | + |
| 104 | + graph = av.filter.Graph() |
| 105 | + graph.link_nodes( |
| 106 | + graph.add_buffer(template=video), |
| 107 | + graph.add("scale", f"{width}:-1"), |
| 108 | + graph.add("format", "gray"), |
| 109 | + graph.add("gblur", f"sigma={blur}"), |
| 110 | + graph.add("buffersink"), |
| 111 | + ).configure() |
| 112 | + |
| 113 | + for unframe in container.decode(video): |
| 114 | + if unframe.pts is None: |
| 115 | + continue |
| 116 | + |
| 117 | + graph.push(unframe) |
| 118 | + frame = graph.pull() |
| 119 | + assert frame.time is not None |
| 120 | + index = round(frame.time * tb) |
| 121 | + |
| 122 | + current_frame = frame.to_ndarray() |
| 123 | + if prev_frame is None: |
| 124 | + value = 0.0 |
| 125 | + else: |
| 126 | + # Use `int16` to avoid underflow with `uint8` datatype |
| 127 | + diff = np.abs(prev_frame.astype(np.int16) - current_frame.astype(np.int16)) |
| 128 | + value = np.count_nonzero(diff) / total_pixels |
| 129 | + |
| 130 | + for _ in range(index - prev_index): |
| 131 | + yield value |
| 132 | + |
| 133 | + prev_frame = current_frame |
| 134 | + prev_index = index |
| 135 | + |
| 136 | + container.close() |
| 137 | + |
| 138 | + |
89 | 139 | @dataclass(slots=True)
|
90 | 140 | class Levels:
|
91 | 141 | ensure: Ensure
|
@@ -114,8 +164,6 @@ def media_length(self) -> int:
|
114 | 164 | return ticks
|
115 | 165 |
|
116 | 166 | # If there's no audio, get length in video metadata.
|
117 |
| - import av |
118 |
| - |
119 | 167 | with av.open(f"{self.src.path}") as cn:
|
120 | 168 | if len(cn.streams.video) < 1:
|
121 | 169 | self.log.error("Could not get media duration")
|
@@ -232,9 +280,6 @@ def subtitle(
|
232 | 280 | except re.error as e:
|
233 | 281 | self.log.error(e)
|
234 | 282 |
|
235 |
| - import av |
236 |
| - from av.subtitles.subtitle import AssSubtitle |
237 |
| - |
238 | 283 | try:
|
239 | 284 | container = av.open(self.src.path, "r")
|
240 | 285 | subtitle_stream = container.streams.subtitles[stream]
|
@@ -293,66 +338,35 @@ def subtitle(
|
293 | 338 | return result
|
294 | 339 |
|
295 | 340 | def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float64]:
|
296 |
| - import av |
297 |
| - |
298 | 341 | if stream >= len(self.src.videos):
|
299 | 342 | raise LevelError(f"motion: video stream '{stream}' does not exist.")
|
300 | 343 |
|
301 | 344 | mobj = {"stream": stream, "width": width, "blur": blur}
|
302 | 345 | if (arr := self.read_cache("motion", mobj)) is not None:
|
303 | 346 | return arr
|
304 | 347 |
|
305 |
| - container = av.open(f"{self.src.path}", "r") |
306 |
| - |
307 |
| - video = container.streams.video[stream] |
308 |
| - video.thread_type = "AUTO" |
| 348 | + with av.open(self.src.path, "r") as container: |
| 349 | + video = container.streams.video[stream] |
| 350 | + inaccurate_dur = ( |
| 351 | + 1024 |
| 352 | + if video.duration is None or video.time_base is None |
| 353 | + else int(video.duration * video.time_base * self.tb) |
| 354 | + ) |
309 | 355 |
|
310 |
| - inaccurate_dur = 1 if video.duration is None else video.duration |
311 |
| - self.bar.start(inaccurate_dur, "Analyzing motion") |
| 356 | + bar = self.bar |
| 357 | + bar.start(inaccurate_dur, "Analyzing motion") |
312 | 358 |
|
313 |
| - prev_frame = None |
314 |
| - current_frame = None |
315 |
| - total_pixels = self.src.videos[0].width * self.src.videos[0].height |
| 359 | + threshold_list = np.zeros((inaccurate_dur), dtype=np.float64) |
316 | 360 | index = 0
|
317 | 361 |
|
318 |
| - graph = av.filter.Graph() |
319 |
| - graph.link_nodes( |
320 |
| - graph.add_buffer(template=video), |
321 |
| - graph.add("scale", f"{width}:-1"), |
322 |
| - graph.add("format", "gray"), |
323 |
| - graph.add("gblur", f"sigma={blur}"), |
324 |
| - graph.add("buffersink"), |
325 |
| - ).configure() |
326 |
| - |
327 |
| - threshold_list = np.zeros((1024), dtype=np.float64) |
328 |
| - |
329 |
| - for unframe in container.decode(video): |
330 |
| - graph.push(unframe) |
331 |
| - frame = graph.pull() |
332 |
| - |
333 |
| - # Showing progress ... |
334 |
| - assert frame.time is not None |
335 |
| - index = int(frame.time * self.tb) |
336 |
| - if frame.pts is not None: |
337 |
| - self.bar.tick(frame.pts) |
338 |
| - |
339 |
| - current_frame = frame.to_ndarray() |
340 |
| - |
| 362 | + for value in iter_motion(self.src, self.tb, stream, blur, width): |
341 | 363 | if index > len(threshold_list) - 1:
|
342 | 364 | threshold_list = np.concatenate(
|
343 |
| - (threshold_list, np.zeros((len(threshold_list)), dtype=np.float64)), |
344 |
| - axis=0, |
345 |
| - ) |
346 |
| - |
347 |
| - if prev_frame is not None: |
348 |
| - # Use `int16` to avoid underflow with `uint8` datatype |
349 |
| - diff = np.abs( |
350 |
| - prev_frame.astype(np.int16) - current_frame.astype(np.int16) |
| 365 | + (threshold_list, np.zeros((len(threshold_list)), dtype=np.float64)) |
351 | 366 | )
|
352 |
| - threshold_list[index] = np.count_nonzero(diff) / total_pixels |
| 367 | + threshold_list[index] = value |
| 368 | + bar.tick(index) |
| 369 | + index += 1 |
353 | 370 |
|
354 |
| - prev_frame = current_frame |
355 |
| - |
356 |
| - self.bar.end() |
357 |
| - container.close() |
| 371 | + bar.end() |
358 | 372 | return self.cache("motion", mobj, threshold_list[:index])
|
0 commit comments