Buckets:
stzhao/rice-range / tmp /video-velocity-model /data_processing /VidaForge /vidaforge /media /motion.py
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| import re | |
| import subprocess | |
| import tempfile | |
| import numpy as np | |
| class FFmpegMotionResult: | |
| vmafmotion_mean: float | |
| vmafmotion_frame_count: int | |
| scdet_score_max: float | |
| scdet_cut_ratio: float | |
| scdet_frame_count: int | |
| _VMAFMOTION_PATTERN = re.compile(r"\bmotion:(?P<value>[-+0-9.eE]+)\b") | |
| _SCDET_SCORE_PATTERN = re.compile(r"^lavfi\.scd\.score=(?P<value>[-+0-9.eE]+)$") | |
| def _read_vmafmotion_log(log_path: Path) -> list[float]: | |
| values: list[float] = [] | |
| for line in log_path.read_text().splitlines(): | |
| match = _VMAFMOTION_PATTERN.search(line) | |
| if match is not None: | |
| values.append(float(match.group("value"))) | |
| if not values: | |
| raise RuntimeError("vmafmotion did not produce frame scores.") | |
| return values | |
| def _read_scdet_log(log_path: Path) -> list[float]: | |
| values: list[float] = [] | |
| for line in log_path.read_text().splitlines(): | |
| match = _SCDET_SCORE_PATTERN.match(line.strip()) | |
| if match is not None: | |
| values.append(float(match.group("value"))) | |
| if not values: | |
| raise RuntimeError("scdet did not produce frame scores.") | |
| return values | |
| def run_ffmpeg_motion( | |
| video_path: str | Path, | |
| *, | |
| scdet_threshold: float = 10.0, | |
| scdet_sample_fps: float = 8.0, | |
| scdet_sample_width: int = 320, | |
| ffmpeg_bin: str = "ffmpeg", | |
| ) -> FFmpegMotionResult: | |
| if not 0 <= scdet_threshold <= 100: | |
| raise ValueError("scdet_threshold must be in [0, 100].") | |
| if scdet_sample_fps <= 0: | |
| raise ValueError("scdet_sample_fps must be > 0.") | |
| if scdet_sample_width <= 0: | |
| raise ValueError("scdet_sample_width must be > 0.") | |
| with tempfile.TemporaryDirectory(prefix="motion_") as tmp_dir: | |
| tmp_path = Path(tmp_dir) | |
| vmafmotion_log_path = tmp_path / "vmafmotion.log" | |
| scdet_log_path = tmp_path / "scdet.log" | |
| filter_graph = ( | |
| "[0:v]split=2[vmaf_in][scd_in];" | |
| f"[vmaf_in]vmafmotion=stats_file={vmafmotion_log_path}[vmaf_out];" | |
| f"[scd_in]scale={scdet_sample_width}:-2," | |
| f"fps={scdet_sample_fps:g}," | |
| f"scdet=threshold={scdet_threshold:g}," | |
| f"metadata=mode=print:file={scdet_log_path}[scd_out]" | |
| ) | |
| video = Path(video_path).expanduser().resolve() | |
| completed = subprocess.run( | |
| [ | |
| ffmpeg_bin, | |
| "-hide_banner", | |
| "-v", | |
| "error", | |
| "-i", | |
| str(video), | |
| "-filter_complex", | |
| filter_graph, | |
| "-map", | |
| "[vmaf_out]", | |
| "-map", | |
| "[scd_out]", | |
| "-an", | |
| "-f", | |
| "null", | |
| "-", | |
| ], | |
| text=True, | |
| capture_output=True, | |
| check=False, | |
| ) | |
| if completed.returncode != 0: | |
| message = completed.stderr.strip() or "ffmpeg failed" | |
| raise RuntimeError(f"ffmpeg motion error for {video}: {message}") | |
| vmafmotion_values = _read_vmafmotion_log(vmafmotion_log_path) | |
| scdet_scores = _read_scdet_log(scdet_log_path) | |
| vmafmotion_array = np.asarray(vmafmotion_values, dtype=np.float64) | |
| scdet_array = np.asarray(scdet_scores, dtype=np.float64) | |
| return FFmpegMotionResult( | |
| vmafmotion_mean=round(float(vmafmotion_array.mean()), 6), | |
| vmafmotion_frame_count=len(vmafmotion_values), | |
| scdet_score_max=round(float(scdet_array.max()), 6), | |
| scdet_cut_ratio=round( | |
| float(np.mean(scdet_array >= float(scdet_threshold))), | |
| 6, | |
| ), | |
| scdet_frame_count=len(scdet_scores), | |
| ) | |
Xet Storage Details
- Size:
- 3.82 kB
- Xet hash:
- 221976c52c0dae3d2b2b7131847e242203af5efa9eebe77da7e3ea91244c35fd
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.