initial
This commit is contained in:
105
tools/video_to_rgb_frames.py
Normal file
105
tools/video_to_rgb_frames.py
Normal file
@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
|
||||
|
||||
DEFAULT_VIDEO_DIR = Path("d405_recordings")
|
||||
DEFAULT_OUTPUT_DIR = Path("d405_rgb_frames")
|
||||
DEFAULT_TARGET_FPS = 8.0
|
||||
|
||||
|
||||
def find_latest_video(video_dir: Path) -> Path:
|
||||
candidates = sorted(video_dir.glob("*.mp4"), key=lambda path: path.stat().st_mtime, reverse=True)
|
||||
if not candidates:
|
||||
raise FileNotFoundError(f"No .mp4 videos found in {video_dir.resolve()}")
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def extract_frames(video_path: Path, output_dir: Path, target_fps: float) -> None:
|
||||
cap = cv2.VideoCapture(str(video_path))
|
||||
if not cap.isOpened():
|
||||
raise RuntimeError(f"Failed to open video: {video_path}")
|
||||
|
||||
source_fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
if source_fps <= 0:
|
||||
source_fps = 30.0
|
||||
|
||||
frame_interval = max(source_fps / target_fps, 1.0)
|
||||
next_frame_to_save = 0.0
|
||||
frame_index = 0
|
||||
saved_count = 0
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print("-" * 40)
|
||||
print(f"Input video : {video_path.resolve()}")
|
||||
print(f"Output dir : {output_dir.resolve()}")
|
||||
print(f"Source FPS : {source_fps:.2f}")
|
||||
print(f"Target FPS : {target_fps:.2f}")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
while True:
|
||||
success, frame = cap.read()
|
||||
if not success:
|
||||
break
|
||||
|
||||
if frame_index + 1e-6 >= next_frame_to_save:
|
||||
saved_count += 1
|
||||
image_path = output_dir / f"{saved_count:06d}.jpg"
|
||||
cv2.imwrite(str(image_path), frame)
|
||||
next_frame_to_save += frame_interval
|
||||
|
||||
frame_index += 1
|
||||
finally:
|
||||
cap.release()
|
||||
|
||||
print(f"Saved {saved_count} RGB frames.")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract RGB frames from a recorded D405 video at 8 FPS.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--video",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Path to the input video. If omitted, the latest video in d405_recordings is used.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Directory to save extracted RGB images.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-fps",
|
||||
type=float,
|
||||
default=DEFAULT_TARGET_FPS,
|
||||
help="Frame extraction rate. Default is 8 FPS.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
video_path = args.video if args.video is not None else find_latest_video(DEFAULT_VIDEO_DIR)
|
||||
if not video_path.exists():
|
||||
raise FileNotFoundError(f"Video file does not exist: {video_path.resolve()}")
|
||||
|
||||
output_dir = args.output_dir
|
||||
if output_dir is None:
|
||||
output_dir = DEFAULT_OUTPUT_DIR / video_path.stem
|
||||
|
||||
if args.target_fps <= 0:
|
||||
raise ValueError("--target-fps must be greater than 0.")
|
||||
|
||||
extract_frames(video_path, output_dir, args.target_fps)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user