betty0's picture
Deploy fall-detection-pose Gradio demo
11fab85 verified
Raw
History Blame Contribute Delete
4.19 kB
"""影片讀寫封裝(依賴 opencv/imageio-ffmpeg;extras: infer)。
輸出約定:所有「給人看」的影片一律 H.264(yuv420p + faststart)。
OpenCV 的 mp4v(MPEG-4 Part 2)在瀏覽器/Gradio 會是黑畫面,且 pip 版
opencv-python 因授權不含 H.264 encoder——因此先寫 mp4v 暫存檔,
關檔時用 ffmpeg(由 imageio-ffmpeg 保證存在)重編碼。
"""
from __future__ import annotations
import subprocess
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator
import cv2
import numpy as np
@dataclass
class VideoInfo:
fps: float
width: int
height: int
n_frames: int
def probe(path: str | Path) -> VideoInfo:
"""取得影片基本資訊;開不了檔直接拋錯。"""
cap = cv2.VideoCapture(str(path))
if not cap.isOpened():
raise FileNotFoundError(f"無法開啟影片:{path}")
info = VideoInfo(
fps=float(cap.get(cv2.CAP_PROP_FPS)) or 30.0,
width=int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
height=int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
n_frames=int(cap.get(cv2.CAP_PROP_FRAME_COUNT)),
)
cap.release()
return info
def iter_frames(path: str | Path) -> Iterator[tuple[int, np.ndarray]]:
"""逐幀迭代 (frame_idx, BGR frame)。"""
cap = cv2.VideoCapture(str(path))
if not cap.isOpened():
raise FileNotFoundError(f"無法開啟影片:{path}")
idx = 0
try:
while True:
ok, frame = cap.read()
if not ok:
break
yield idx, frame
idx += 1
finally:
cap.release()
def get_ffmpeg_exe() -> str:
import imageio_ffmpeg
return imageio_ffmpeg.get_ffmpeg_exe()
def reencode_h264(src: str | Path, dst: str | Path) -> None:
"""重編碼為瀏覽器可播的 H.264 mp4(yuv420p + faststart)。"""
dst = Path(dst)
dst.parent.mkdir(parents=True, exist_ok=True)
cmd = [
get_ffmpeg_exe(),
"-y",
"-loglevel", "error",
"-i", str(src),
"-c:v", "libx264",
"-pix_fmt", "yuv420p", # yuv444 在 Chrome/Safari 播不了
"-movflags", "+faststart", # moov atom 前置,邊下載邊播
str(dst),
]
subprocess.run(cmd, check=True)
class H264VideoWriter:
"""先寫 mp4v 暫存,close() 時 ffmpeg 重編碼成 H.264 輸出。"""
def __init__(self, out_path: str | Path, fps: float, width: int, height: int):
self.out_path = Path(out_path)
self._tmp = Path(tempfile.mkstemp(suffix=".mp4")[1])
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
self._writer = cv2.VideoWriter(str(self._tmp), fourcc, fps, (width, height))
if not self._writer.isOpened():
raise RuntimeError(f"VideoWriter 開啟失敗(fps={fps}, size={width}x{height})")
def write(self, frame_bgr: np.ndarray) -> None:
self._writer.write(frame_bgr)
def close(self) -> None:
self._writer.release()
try:
reencode_h264(self._tmp, self.out_path)
finally:
self._tmp.unlink(missing_ok=True)
def __enter__(self) -> "H264VideoWriter":
return self
def __exit__(self, *exc) -> None:
self.close()
def write_video_mp4v(frames: Iterator[np.ndarray], out_path: str | Path, fps: float) -> int:
"""把幀序列寫成 mp4v 影片(僅供內部再處理,如 URFD PNG 序列重組;
非瀏覽器播放用途,不做 H.264 重編碼以省時)。回傳寫入幀數。"""
out_path = Path(out_path)
out_path.parent.mkdir(parents=True, exist_ok=True)
writer = None
n = 0
for frame in frames:
if writer is None:
h, w = frame.shape[:2]
writer = cv2.VideoWriter(
str(out_path), cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h)
)
if not writer.isOpened():
raise RuntimeError(f"VideoWriter 開啟失敗:{out_path}")
writer.write(frame)
n += 1
if writer is not None:
writer.release()
return n