Skip to content

Commit bfd5173

Browse files
committed
Move iter_motion to analyze.py
1 parent 3617b4a commit bfd5173

File tree

2 files changed

+67
-101
lines changed

2 files changed

+67
-101
lines changed

auto_editor/analyze.py

Lines changed: 66 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
from fractions import Fraction
77
from typing import TYPE_CHECKING
88

9+
import av
910
import numpy as np
11+
from av.subtitles.subtitle import AssSubtitle
1012

1113
from auto_editor import version
1214
from auto_editor.utils.subtitle_tools import convert_ass_to_text
1315
from auto_editor.wavfile import read
1416

1517
if TYPE_CHECKING:
18+
from collections.abc import Iterator
1619
from fractions import Fraction
1720
from typing import Any
1821

@@ -86,6 +89,53 @@ def obj_tag(tag: str, tb: Fraction, obj: dict[str, Any]) -> str:
8689
return key
8790

8891

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+
89139
@dataclass(slots=True)
90140
class Levels:
91141
ensure: Ensure
@@ -114,8 +164,6 @@ def media_length(self) -> int:
114164
return ticks
115165

116166
# If there's no audio, get length in video metadata.
117-
import av
118-
119167
with av.open(f"{self.src.path}") as cn:
120168
if len(cn.streams.video) < 1:
121169
self.log.error("Could not get media duration")
@@ -232,9 +280,6 @@ def subtitle(
232280
except re.error as e:
233281
self.log.error(e)
234282

235-
import av
236-
from av.subtitles.subtitle import AssSubtitle
237-
238283
try:
239284
container = av.open(self.src.path, "r")
240285
subtitle_stream = container.streams.subtitles[stream]
@@ -293,66 +338,35 @@ def subtitle(
293338
return result
294339

295340
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float64]:
296-
import av
297-
298341
if stream >= len(self.src.videos):
299342
raise LevelError(f"motion: video stream '{stream}' does not exist.")
300343

301344
mobj = {"stream": stream, "width": width, "blur": blur}
302345
if (arr := self.read_cache("motion", mobj)) is not None:
303346
return arr
304347

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+
)
309355

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")
312358

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)
316360
index = 0
317361

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):
341363
if index > len(threshold_list) - 1:
342364
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))
351366
)
352-
threshold_list[index] = np.count_nonzero(diff) / total_pixels
367+
threshold_list[index] = value
368+
bar.tick(index)
369+
index += 1
353370

354-
prev_frame = current_frame
355-
356-
self.bar.end()
357-
container.close()
371+
bar.end()
358372
return self.cache("motion", mobj, threshold_list[:index])

auto_editor/subcommands/levels.py

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import numpy as np
1111
from av.audio.fifo import AudioFifo
1212

13-
from auto_editor.analyze import LevelError, Levels
13+
from auto_editor.analyze import LevelError, Levels, iter_motion
1414
from auto_editor.ffwrapper import FFmpeg, initFileInfo
1515
from auto_editor.lang.palet import env
1616
from auto_editor.lib.contracts import is_bool, is_nat, is_nat1, is_str, is_void, orc
@@ -130,53 +130,6 @@ def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[float]:
130130
container.close()
131131

132132

133-
def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[float]:
134-
container = av.open(src.path, "r")
135-
136-
video = container.streams.video[stream]
137-
video.thread_type = "AUTO"
138-
139-
prev_frame = None
140-
current_frame = None
141-
total_pixels = src.videos[0].width * src.videos[0].height
142-
index = 0
143-
prev_index = 0
144-
145-
graph = av.filter.Graph()
146-
graph.link_nodes(
147-
graph.add_buffer(template=video),
148-
graph.add("scale", f"{width}:-1"),
149-
graph.add("format", "gray"),
150-
graph.add("gblur", f"sigma={blur}"),
151-
graph.add("buffersink"),
152-
).configure()
153-
154-
for unframe in container.decode(video):
155-
if unframe.pts is None:
156-
continue
157-
158-
graph.push(unframe)
159-
frame = graph.pull()
160-
assert frame.time is not None
161-
index = round(frame.time * tb)
162-
163-
current_frame = frame.to_ndarray()
164-
if prev_frame is None:
165-
value = 0.0
166-
else:
167-
# Use `int16` to avoid underflow with `uint8` datatype
168-
diff = np.abs(prev_frame.astype(np.int16) - current_frame.astype(np.int16))
169-
value = np.count_nonzero(diff) / total_pixels
170-
171-
for _ in range(index - prev_index):
172-
yield value
173-
174-
prev_frame = current_frame
175-
prev_index = index
176-
177-
container.close()
178-
179-
180133
def main(sys_args: list[str] = sys.argv[1:]) -> None:
181134
parser = levels_options(ArgumentParser("levels"))
182135
args = parser.parse_args(LevelArgs, sys_args)
@@ -232,7 +185,6 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
232185
levels = Levels(ensure, src, tb, bar, temp, log)
233186
try:
234187
if method == "audio":
235-
# print_arr(levels.audio(**obj))
236188
print_arr_gen(iter_audio(src, tb, **obj))
237189
elif method == "motion":
238190
print_arr_gen(iter_motion(src, tb, **obj))

0 commit comments

Comments
 (0)