Skip to content

Commit 51629fa

Browse files
committed
Get motion with pyav filters, remove pixeldiff
1 parent d56d55b commit 51629fa

File tree

5 files changed

+23
-113
lines changed

5 files changed

+23
-113
lines changed

Diff for: auto_editor/analyze.py

+18-102
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,6 @@
5858
pAttr("blur", 9, is_nat),
5959
pAttr("width", 400, is_nat1),
6060
)
61-
pixeldiff_builder = pAttrs(
62-
"pixeldiff",
63-
pAttr("threshold", 1, is_nat),
64-
pAttr("stream", 0, is_nat),
65-
)
6661
subtitle_builder = pAttrs(
6762
"subtitle",
6863
pAttr("pattern", Required, is_str),
@@ -74,7 +69,6 @@
7469
builder_map = {
7570
"audio": audio_builder,
7671
"motion": motion_builder,
77-
"pixeldiff": pixeldiff_builder,
7872
"subtitle": subtitle_builder,
7973
}
8074

@@ -353,7 +347,6 @@ def cleanhtml(raw_html: str) -> str:
353347

354348
def motion(self, s: int, blur: int, width: int) -> NDArray[np.float64]:
355349
import av
356-
from PIL import ImageChops, ImageFilter
357350

358351
av.logging.set_level(av.logging.PANIC)
359352

@@ -370,28 +363,20 @@ def motion(self, s: int, blur: int, width: int) -> NDArray[np.float64]:
370363
stream = container.streams.video[s]
371364
stream.thread_type = "AUTO"
372365

373-
if (
374-
stream.duration is None
375-
or stream.time_base is None
376-
or stream.average_rate is None
377-
):
378-
inaccurate_dur = 1
379-
else:
380-
inaccurate_dur = int(
381-
stream.duration * stream.time_base * stream.average_rate
382-
)
383-
366+
inaccurate_dur = 1 if stream.duration is None else stream.duration
384367
self.bar.start(inaccurate_dur, "Analyzing motion")
385368

386-
prev_image = None
387-
image = None
369+
prev_frame = None
370+
current_frame = None
388371
total_pixels = self.src.videos[0].width * self.src.videos[0].height
389372
index = 0
390373

391374
graph = av.filter.Graph()
392375
link_nodes(
393376
graph.add_buffer(template=stream),
394377
graph.add("scale", f"{width}:-1"),
378+
graph.add("format", "gray"),
379+
graph.add("gblur", f"sigma={blur}"),
395380
graph.add("buffersink"),
396381
)
397382
graph.configure()
@@ -402,98 +387,31 @@ def motion(self, s: int, blur: int, width: int) -> NDArray[np.float64]:
402387
graph.push(unframe)
403388
frame = graph.pull()
404389

405-
prev_image = image
406-
407-
assert isinstance(frame.time, float)
390+
# Showing progress ...
391+
assert frame.time is not None
408392
index = int(frame.time * self.tb)
409-
self.bar.tick(index)
393+
if frame.pts is not None:
394+
self.bar.tick(frame.pts)
395+
396+
current_frame = frame.to_ndarray()
410397

411398
if index > len(threshold_list) - 1:
412399
threshold_list = np.concatenate(
413400
(threshold_list, np.zeros((len(threshold_list)), dtype=np.float64)),
414401
axis=0,
415402
)
416403

417-
image = frame.to_image().convert("L")
418-
419-
if blur > 0:
420-
image = image.filter(ImageFilter.GaussianBlur(radius=blur))
421-
422-
if prev_image is not None:
423-
count = np.count_nonzero(ImageChops.difference(prev_image, image))
424-
425-
threshold_list[index] = count / total_pixels
426-
427-
self.bar.end()
428-
result = threshold_list[:index]
429-
del threshold_list
430-
431-
return self.cache("motion", mobj, result)
432-
433-
def pixeldiff(self, s: int) -> NDArray[np.uint64]:
434-
import av
435-
from PIL import ImageChops
436-
437-
av.logging.set_level(av.logging.PANIC)
438-
439-
pobj = {"stream": s}
440-
441-
if s >= len(self.src.videos):
442-
raise LevelError(f"pixeldiff: video stream '{s}' does not exist.")
443-
444-
if (arr := self.read_cache("pixeldiff", pobj)) is not None:
445-
return arr
446-
447-
container = av.open(f"{self.src.path}", "r")
448-
449-
stream = container.streams.video[s]
450-
stream.thread_type = "AUTO"
451-
452-
if (
453-
stream.duration is None
454-
or stream.time_base is None
455-
or stream.average_rate is None
456-
):
457-
inaccurate_dur = 1
458-
else:
459-
inaccurate_dur = int(
460-
stream.duration * stream.time_base * stream.average_rate
461-
)
462-
463-
self.bar.start(inaccurate_dur, "Analyzing pixel diffs")
464-
465-
prev_image = None
466-
image = None
467-
index = 0
468-
469-
threshold_list = np.zeros((1024), dtype=np.uint64)
470-
471-
for frame in container.decode(stream):
472-
prev_image = image
473-
474-
assert frame.time is not None
475-
index = int(frame.time * self.tb)
476-
self.bar.tick(index)
477-
478-
if index > len(threshold_list) - 1:
479-
threshold_list = np.concatenate(
480-
(threshold_list, np.zeros((len(threshold_list)), dtype=np.uint64)),
481-
axis=0,
404+
if prev_frame is not None:
405+
# Use `int16` to avoid underflow with `uint8` datatype
406+
diff = np.abs(
407+
prev_frame.astype(np.int16) - current_frame.astype(np.int16)
482408
)
409+
threshold_list[index] = np.count_nonzero(diff) / total_pixels
483410

484-
assert isinstance(frame, av.VideoFrame)
485-
image = frame.to_image()
486-
487-
if prev_image is not None:
488-
threshold_list[index] = np.count_nonzero(
489-
ImageChops.difference(prev_image, image)
490-
)
411+
prev_frame = current_frame
491412

492413
self.bar.end()
493-
result = threshold_list[:index]
494-
del threshold_list
495-
496-
return self.cache("pixeldiff", pobj, result)
414+
return self.cache("motion", mobj, threshold_list[:index])
497415

498416

499417
def edit_method(val: str, filesetup: FileSetup, env: Env) -> NDArray[np.bool_]:
@@ -558,8 +476,6 @@ def edit_method(val: str, filesetup: FileSetup, env: Env) -> NDArray[np.bool_]:
558476
levels.motion(obj["stream"], obj["blur"], obj["width"]),
559477
obj["threshold"],
560478
)
561-
if method == "pixeldiff":
562-
return to_threshold(levels.pixeldiff(obj["stream"]), obj["threshold"])
563479

564480
if method == "subtitle":
565481
return levels.subtitle(

Diff for: auto_editor/help.py

+4-7
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,16 @@
4141
- blur nat? : 9
4242
- width nat1? : 400
4343
44-
- none ; Do not modify the media in anyway; mark all sections as "loud" (1).
45-
- all/e ; Cut out everything out; mark all sections as "silent" (0).
46-
47-
- pixeldiff ; Detect when a certain amount of pixels have changed between frames.
48-
- threshold nat? : 1
49-
- stream nat? : 0
50-
5144
- subtitle ; Detect when subtitle matches pattern as a RegEx string.
5245
- pattern string?
5346
- stream nat? : 0
5447
- ignore-case bool? : #f
5548
- max-count (or/c nat? void?) : (void)
5649
50+
- none ; Do not modify the media in anyway; mark all sections as "loud" (1).
51+
- all/e ; Cut out everything out; mark all sections as "silent" (0).
52+
53+
5754
Command-line Examples:
5855
--edit audio
5956
--edit audio:threshold=4%

Diff for: auto_editor/lang/palet.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class ClosingError(MyError):
5151
LPAREN, RPAREN, LBRAC, RBRAC, LCUR, RCUR, EOF = "(", ")", "[", "]", "{", "}", "EOF"
5252
VAL, QUOTE, SEC, DB, DOT, VLIT = "VAL", "QUOTE", "SEC", "DB", "DOT", "VLIT"
5353
SEC_UNITS = ("s", "sec", "secs", "second", "seconds")
54-
METHODS = ("audio:", "motion:", "pixeldiff:", "subtitle:")
54+
METHODS = ("audio:", "motion:", "subtitle:")
5555
brac_pairs = {LPAREN: RPAREN, LBRAC: RBRAC, LCUR: RCUR}
5656

5757
str_escape = {

Diff for: auto_editor/subcommands/levels.py

-2
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,6 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
114114
print_arr(levels.audio(obj["stream"]))
115115
elif method == "motion":
116116
print_arr(levels.motion(obj["stream"], obj["blur"], obj["width"]))
117-
elif method == "pixeldiff":
118-
print_arr(levels.pixeldiff(obj["stream"]))
119117
elif method == "subtitle":
120118
print_arr(
121119
levels.subtitle(

Diff for: pyproject.toml

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ authors = [{ name = "WyattBlue", email = "wyattblue@auto-editor.com" }]
1010
requires-python = ">=3.10"
1111
dependencies = [
1212
"numpy>=1.22.0",
13-
"pillow==10.2.0",
1413
"pyav==12.0.2",
1514
"ae-ffmpeg==1.1.*",
1615
]

0 commit comments

Comments
 (0)