mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-29 20:49:42 +00:00
fix video encoding/opencv
This commit is contained in:
@@ -425,6 +425,56 @@ def _encode_h264(temp_path: Path, output_path: Path) -> None:
|
|||||||
temp_path.unlink(missing_ok=True)
|
temp_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_decode_proxy(
|
||||||
|
source_path: Path,
|
||||||
|
proxy_path: Path,
|
||||||
|
*,
|
||||||
|
start_timestamp: float,
|
||||||
|
end_timestamp: float,
|
||||||
|
fps: float,
|
||||||
|
expected_frames: int,
|
||||||
|
) -> Path:
|
||||||
|
"""Decode an episode segment with system FFmpeg into an OpenCV-safe proxy."""
|
||||||
|
ffmpeg = shutil.which("ffmpeg")
|
||||||
|
if ffmpeg is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"System ffmpeg is required to decode this dataset's video codec. "
|
||||||
|
"Install ffmpeg and rerun the command."
|
||||||
|
)
|
||||||
|
duration = end_timestamp - start_timestamp
|
||||||
|
result = subprocess.run( # nosec B603
|
||||||
|
[
|
||||||
|
ffmpeg,
|
||||||
|
"-y",
|
||||||
|
"-ss",
|
||||||
|
f"{start_timestamp:.9f}",
|
||||||
|
"-i",
|
||||||
|
str(source_path),
|
||||||
|
"-t",
|
||||||
|
f"{duration:.9f}",
|
||||||
|
"-an",
|
||||||
|
"-vf",
|
||||||
|
f"fps={fps:.9f}",
|
||||||
|
"-frames:v",
|
||||||
|
str(expected_frames),
|
||||||
|
"-c:v",
|
||||||
|
"libx264",
|
||||||
|
"-preset",
|
||||||
|
"ultrafast",
|
||||||
|
"-crf",
|
||||||
|
"12",
|
||||||
|
"-pix_fmt",
|
||||||
|
"yuv420p",
|
||||||
|
str(proxy_path),
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0 or not proxy_path.is_file():
|
||||||
|
raise RuntimeError(f"FFmpeg episode decode failed:\n{result.stderr[-1500:]}")
|
||||||
|
return proxy_path
|
||||||
|
|
||||||
|
|
||||||
def create_advantage_video(
|
def create_advantage_video(
|
||||||
*,
|
*,
|
||||||
repo_id: str,
|
repo_id: str,
|
||||||
@@ -473,19 +523,29 @@ def create_advantage_video(
|
|||||||
tasks = episode_metadata.get("tasks") or []
|
tasks = episode_metadata.get("tasks") or []
|
||||||
task = str(tasks[0]) if len(tasks) else ""
|
task = str(tasks[0]) if len(tasks) else ""
|
||||||
|
|
||||||
capture = cv2.VideoCapture(str(video_path))
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
capture.set(cv2.CAP_PROP_POS_MSEC, start_timestamp * 1000)
|
camera_name = camera_key.replace(".", "_").replace("/", "_")
|
||||||
|
output_path = output_dir / f"episode_{episode_index:06d}_{camera_name}_advantage.mp4"
|
||||||
|
decode_proxy_path = output_path.with_name(output_path.stem + "_decode_proxy.mp4")
|
||||||
|
_create_decode_proxy(
|
||||||
|
video_path,
|
||||||
|
decode_proxy_path,
|
||||||
|
start_timestamp=start_timestamp,
|
||||||
|
end_timestamp=end_timestamp,
|
||||||
|
fps=fps,
|
||||||
|
expected_frames=len(episode),
|
||||||
|
)
|
||||||
|
|
||||||
|
capture = cv2.VideoCapture(str(decode_proxy_path))
|
||||||
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
|
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||||
frame_height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
frame_height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||||
if width <= 0 or frame_height <= 0:
|
if width <= 0 or frame_height <= 0:
|
||||||
capture.release()
|
capture.release()
|
||||||
|
decode_proxy_path.unlink(missing_ok=True)
|
||||||
raise RuntimeError(f"Could not read video dimensions from {video_path}")
|
raise RuntimeError(f"Could not read video dimensions from {video_path}")
|
||||||
dashboard_height = max(132, round(frame_height * 0.27))
|
dashboard_height = max(132, round(frame_height * 0.27))
|
||||||
output_height = frame_height + dashboard_height
|
output_height = frame_height + dashboard_height
|
||||||
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
camera_name = camera_key.replace(".", "_").replace("/", "_")
|
|
||||||
output_path = output_dir / f"episode_{episode_index:06d}_{camera_name}_advantage.mp4"
|
|
||||||
temp_path = output_path.with_name(output_path.stem + "_temp.mp4")
|
temp_path = output_path.with_name(output_path.stem + "_temp.mp4")
|
||||||
writer = cv2.VideoWriter(
|
writer = cv2.VideoWriter(
|
||||||
str(temp_path),
|
str(temp_path),
|
||||||
@@ -495,6 +555,7 @@ def create_advantage_video(
|
|||||||
)
|
)
|
||||||
if not writer.isOpened():
|
if not writer.isOpened():
|
||||||
capture.release()
|
capture.release()
|
||||||
|
decode_proxy_path.unlink(missing_ok=True)
|
||||||
raise RuntimeError(f"Could not open video writer for {temp_path}")
|
raise RuntimeError(f"Could not open video writer for {temp_path}")
|
||||||
|
|
||||||
expected_frames = len(episode)
|
expected_frames = len(episode)
|
||||||
@@ -534,6 +595,7 @@ def create_advantage_video(
|
|||||||
finally:
|
finally:
|
||||||
writer.release()
|
writer.release()
|
||||||
capture.release()
|
capture.release()
|
||||||
|
decode_proxy_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
if written != expected_frames:
|
if written != expected_frames:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
pytest.importorskip("cv2")
|
|
||||||
|
|
||||||
from lerobot.scripts.lerobot_create_advantage_video import (
|
from lerobot.scripts.lerobot_create_advantage_video import (
|
||||||
_contiguous_segments,
|
_contiguous_segments,
|
||||||
|
_create_decode_proxy,
|
||||||
_draw_dashboard,
|
_draw_dashboard,
|
||||||
_draw_status_badge,
|
_draw_status_badge,
|
||||||
)
|
)
|
||||||
@@ -55,3 +58,43 @@ def test_status_badge_modifies_frame():
|
|||||||
_draw_status_badge(frame, "positive", intervention=False, episode_index=12)
|
_draw_status_badge(frame, "positive", intervention=False, episode_index=12)
|
||||||
|
|
||||||
assert frame.sum() > 0
|
assert frame.sum() > 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg is required")
|
||||||
|
def test_ffmpeg_decode_proxy_handles_av1(tmp_path):
|
||||||
|
source = tmp_path / "source.mkv"
|
||||||
|
proxy = tmp_path / "proxy.mp4"
|
||||||
|
encode = subprocess.run(
|
||||||
|
[
|
||||||
|
"ffmpeg",
|
||||||
|
"-y",
|
||||||
|
"-f",
|
||||||
|
"lavfi",
|
||||||
|
"-i",
|
||||||
|
"testsrc=size=64x64:rate=10:duration=1",
|
||||||
|
"-c:v",
|
||||||
|
"libaom-av1",
|
||||||
|
"-cpu-used",
|
||||||
|
"8",
|
||||||
|
"-crf",
|
||||||
|
"40",
|
||||||
|
str(source),
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
if encode.returncode != 0:
|
||||||
|
pytest.skip("ffmpeg does not provide the libaom-av1 encoder")
|
||||||
|
|
||||||
|
_create_decode_proxy(
|
||||||
|
source,
|
||||||
|
proxy,
|
||||||
|
start_timestamp=0,
|
||||||
|
end_timestamp=1,
|
||||||
|
fps=10,
|
||||||
|
expected_frames=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
capture = cv2.VideoCapture(str(proxy))
|
||||||
|
frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
capture.release()
|
||||||
|
assert frame_count == 10
|
||||||
|
|||||||
Reference in New Issue
Block a user