Menú

Project:

#!/usr/bin/env python3
"""
extract_frames.py

Extract frames from a video file using OpenCV.

Usage examples:
python extract_frames.py -i sample.mp4 -o frames_dir --every-frame 10
python extract_frames.py -i sample.mp4 -o frames_dir --every-sec 1 --display
"""

import cv2
import os
import argparse
from pathlib import Path
from datetime import timedelta

def parse_args():
    p = argparse.ArgumentParser(description="Extract frames from a video file using OpenCV.")
    p.add_argument("-i", "--input", required=True, help="Path to input video file.")
    p.add_argument("-o", "--output", default="frames", help="Output directory to save extracted frames.")
    group = p.add_mutually_exclusive_group()
    group.add_argument("--every-frame", type=int, default=1,
                       help="Save every Nth frame (default=1 -> save every frame).")
    group.add_argument("--every-sec", type=float,
                       help="Save one frame every N seconds (overrides --every-frame).")
    p.add_argument("--display", action="store_true", help="Show frames in a window while extracting.")
    p.add_argument("--max-frames", type=int, default=0, help="Stop after saving this many frames (0 = no limit).")
    p.add_argument("--prefix", default="frame", help="Filename prefix for saved frames.")
    return p.parse_args()

def ensure_output_dir(path: Path):
    path.mkdir(parents=True, exist_ok=True)

def seconds_to_hhmmss(sec: float) -> str:
    td = timedelta(seconds=round(sec))
    return str(td)

def main():
    args = parse_args()
    input_path = Path(args.input)
    output_dir = Path(args.output)
    ensure_output_dir(output_dir)

    # Open the video capture
    cap = cv2.VideoCapture(str(input_path))
    if not cap.isOpened():
        print(f"Error: Could not open video file: {input_path}")
        return

    # Get video properties
    fps = cap.get(cv2.CAP_PROP_FPS) or 0
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
    duration_sec = total_frames / fps if fps > 0 else 0

    print(f"Input: {input_path}")
    print(f"Resolution: {int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))}x{int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))}")
    print(f"FPS: {fps:.2f}")
    print(f"Total frames (approx): {total_frames}")
    print(f"Duration (sec): {duration_sec:.2f}")
    print(f"Saving to directory: {output_dir.resolve()}")

    # Determine the saving interval
    if args.every_sec is not None:
        # Convert seconds to frames interval (may be fractional; round to nearest frame)
        if fps <= 0:
            print("Warning: FPS unknown, falling back to every-frame behavior.")
            save_every_n_frames = max(1, args.every_frame)
        else:
            save_every_n_frames = max(1, int(round(args.every_sec * fps)))
        print(f"Saving one frame every {args.every_sec} sec -> every {save_every_n_frames} frames (approx).")
    else:
        save_every_n_frames = max(1, args.every_frame)
        print(f"Saving every {save_every_n_frames} frames.")

    saved_count = 0
    read_count = 0
    frame_idx = 0

    # Loop over frames
    while True:
        ret, frame = cap.read()
        if not ret:
            # End of video or read error
            break
        read_count += 1
        frame_idx += 1

        # Decide whether to save this frame
        if (frame_idx - 1) % save_every_n_frames == 0:
            # Compute timestamp (in seconds) of current frame
            timestamp_sec = frame_idx / fps if fps > 0 else 0.0
            timestamp_text = seconds_to_hhmmss(timestamp_sec)

            # Construct filename with zero-padded frame index and timestamp
            filename = f"{args.prefix}_{frame_idx:06d}_{timestamp_text.replace(':','-')}.png"
            output_file = output_dir / filename

            # Save the image (PNG keeps good quality)
            cv2.imwrite(str(output_file), frame)
            saved_count += 1

            # Print progress
            print(f"[{saved_count}] Saved frame {frame_idx} @ {timestamp_sec:.2f}s -> {output_file.name}")

            # Stop if reached max frames limit
            if args.max_frames and saved_count >= args.max_frames:
                print("Reached max_frames limit. Stopping extraction.")
                break

        # Optionally display the frame (scaled to fit window)
        if args.display:
            disp = frame.copy()
            h, w = disp.shape[:2]
            max_w, max_h = 960, 540
            scale = min(1.0, max_w / w, max_h / h)
            if scale < 1.0:
                disp = cv2.resize(disp, (int(w*scale), int(h*scale)), interpolation=cv2.INTER_LINEAR)
            cv2.imshow("Frame Extractor - Press 'q' to quit display", disp)
            # waitKey small delay to allow window to refresh; 1ms is okay for fast extraction
            key = cv2.waitKey(1) & 0xFF
            if key == ord('q'):
                print("User requested display quit. Continuing extraction without display.")
                args.display = False
                cv2.destroyAllWindows()

    # Release resources
    cap.release()
    cv2.destroyAllWindows()

    print("Extraction complete.")
    print(f"Total frames read: {read_count}")
    print(f"Total frames saved: {saved_count}")
    print(f"Saved images are in: {output_dir.resolve()}")

if __name__ == "__main__":
    main()