Skip to main content

frame-extractor

tests PyPI

Extract frames from a video as PNG or JPEG images, from the command line or from Python. Built on ffmpeg.

Why not just use ffmpeg?

For a one-off extraction by someone who knows ffmpeg, this adds very little. The whole job is one command:

ffmpeg -ss 10 -i in.mp4 -t 5 -vsync 0 frames/f_%06d.png

What it does add is a guard against ffmpeg's silent wrong answers. Each of these exits 0 and produces output that looks reasonable:

What you ask for ffmpeg frame-extractor
-ss 99 on a 2-second video writes nothing, exits 0 rejected, exit 1
-q:v 100 (valid range is 2–31) clamps to 31 silently rejected, exit 1
re-extracting a shorter range leaves the previous run's surplus frames alongside the new ones refused unless --overwrite, which clears them first
scale=-1:-1 resizes nothing rejected, exit 1
fps=20 on a 10fps source 40 files, every second one a duplicate rejected, exit 1

That third row is the one that bites: frames from two runs end up in one directory with nothing marking which is which, and the count you get back is wrong rather than merely untidy.

The other half is the Python API. If you need frames from inside a program, the alternative is writing the subprocess wrapper yourself — and then rediscovering the five rows above one at a time.

Where it doesn't help: frames as arrays for ML or CV work — use PyAV or TorchCodec rather than writing PNGs only to read them back. Anything beyond extraction — trimming, concatenating, re-encoding — is ffmpeg's job.

Install

pip install frame-extractor-ffmpeg

Requires Python 3.10+ and ffmpeg and ffprobe on your PATH. Both ship together, so one install covers them:

sudo apt update && sudo apt install ffmpeg  # Debian / Ubuntu / WSL
brew install ffmpeg                         # macOS

On WSL, a Windows-side ffmpeg installation won't satisfy this — the Linux package is what ends up on your PATH.

The distribution is named frame-extractor-ffmpeg because frame-extractor was already taken on PyPI by an unrelated package. It installs the frame-extractor command and the frame_extractor module. There are no third-party Python dependencies.

Command-line usage

frame-extractor VIDEO OUTPUT_DIR [--start SECONDS] [--end SECONDS]
                [--format {png,jpg}] [--jpeg-quality N]
                [--fps N | --keyframes | --scenes THRESHOLD]
                [--scale W:H] [--timestamps] [--manifest CSV]
                [--no-progress] [--overwrite]
Argument Required Default Meaning
VIDEO yes Path to the input video file
OUTPUT_DIR yes Directory for the frames; created if missing
--start no 0.0 Start of the range in seconds, inclusive
--end no end of video End of the range in seconds, exclusive
--format no png Output image format: png or jpg
--jpeg-quality no 2 JPEG quality, 2 (best) to 31 (worst); ignored for PNG
--fps no every frame Frames to extract per second of video
--keyframes no off Extract only the video's key frames
--scenes no off Extract only frames where the picture changes
--scale no source size Output size as WIDTH:HEIGHT
--no-progress no off Suppress the progress indicator
--timestamps no off Record where each frame sits in the source
--manifest no Write path, index, and timestamp as CSV
--overwrite no off Replace frames from an earlier extraction

Examples

# Every frame of the whole video
frame-extractor input.mp4 frames/

# Five seconds, starting at ten
frame-extractor input.mp4 frames/ --start 10 --end 15

# From the 30-second mark to the end
frame-extractor input.mp4 frames/ --start 30

# One frame per second instead of all of them
frame-extractor input.mp4 frames/ --fps 1

# One frame every four seconds, for a rough overview
frame-extractor input.mp4 frames/ --fps 0.25

# Only the key frames: fastest way to see what is in a long file
frame-extractor input.mp4 frames/ --keyframes

# Only where the picture changes, to find the cuts
frame-extractor input.mp4 frames/ --scenes 0.4

# Resize to a fixed size, for a model expecting one
frame-extractor input.mp4 frames/ --scale 224:224

# Fixed width, height following the aspect ratio
frame-extractor input.mp4 frames/ --scale 640:auto

# JPEG instead of PNG, trading fidelity for disk space
frame-extractor input.mp4 frames/ --format jpg --jpeg-quality 10

# Record when each frame happened, as a CSV alongside the images
frame-extractor input.mp4 frames/ --fps 1 --manifest frames.csv

# Re-extract a different range into a directory already holding frames
frame-extractor input.mp4 frames/ --start 5 --end 8 --overwrite

The range is half-open — a frame landing exactly on --end is excluded, so --start 0 --end 1 and --start 1 --end 2 produce no overlap.

Library usage

from pathlib import Path
from frame_extractor import extract_frames, FrameExtractorError

try:
    frames = extract_frames(
        Path("input.mp4"),
        Path("frames"),
        start_time=10.0,
        end_time=15.0,
        fps=1.0,
        timestamps=True,
    )
except FrameExtractorError as exc:
    print(f"extraction failed: {exc}")
else:
    for frame in frames:
        print(f"{frame.index}  {frame.timestamp}s  {frame.path.name}")

Upgrading from 1.x: extract_frames returns list[Frame] rather than list[Path]. Add .path where you were using an item as a path; filenames and every argument are unchanged.

The source, destination, and time range are positional; every option after those is keyword-only, so new options can be added without their position becoming part of the API.

Pass on_progress to follow a long extraction — the library never prints, so what to do with each update is yours:

extract_frames(video, out, on_progress=lambda p: print(f"{p.fraction:.0%}"))

Public API

Importable from frame_extractor directly. Anything not listed is an implementation detail and may move between versions.

Name
extract_frames The extraction function
Frame One extracted frame: path, index, timestamp
Progress What an on_progress callback receives
FrameExtractorError Base class — catch this to handle any expected failure
VideoFileError Input missing, or unreadable as media
InvalidTimeRangeError Range negative, inverted, or starting past the end
InvalidOutputOptionError Unsupported format, or quality out of range
OutputDirectoryError Output directory holds frames and overwrite is False
FFmpegNotFoundError ffmpeg or ffprobe missing from PATH
FFmpegExecutionError ffmpeg exited non-zero; carries returncode and stderr
SUPPORTED_FORMATS ("png", "jpg")
MIN_JPEG_QUALITY / MAX_JPEG_QUALITY 2 and 31

Choosing frames

Frames are written as frame_000001.png, numbered from 1 in playback order, restarting each run. PNG is the default because it's lossless; use --format jpg when disk space matters more than fidelity.

How many. --fps N is the predictable option: you know in advance what you get. --keyframes is far faster — the encoder stores those as complete pictures and everything between them as differences, so the rest are never decoded — but their spacing is the encoder's choice, not yours. --scenes keeps frames that differ from the one before by more than a threshold.

Only one of the three can be used at a time. Both --keyframes and --scenes can return far fewer frames than expected, and that is correct rather than broken: scene detection is a heuristic on pixel differences, not a cut list, and in a test clip of four solid colours it found two of the three changes. The command line says so when a run writes nothing.

Where they sit in the source. --timestamps records each frame's position, and --manifest frames.csv writes the mapping out:

path,index,timestamp
frames/frame_000001.png,0,5.0
frames/frame_000002.png,1,6.0

It is off by default because it costs a pass over the range. With --keyframes or --scenes the spacing is uneven, so this is the only way to know when a frame happened.

Re-running. Extracting into a directory that already holds frame_* files of the same format is an error. --overwrite deletes them first rather than writing over them, because ffmpeg renumbers from 1 each run and a shorter second extraction would otherwise leave the first's tail behind. Only this run's own pattern is touched.

Errors

Failures print a single error: line to stderr and exit non-zero — no tracebacks.

Exit code Meaning
0 Frames extracted successfully
1 The request or the environment was rejected (see below)
2 Bad command-line usage, reported by argparse

What can go wrong, and what it looks like:

error: Video file not found: input.mp4
error: --end (1.0) must be greater than --start (5.0)
error: --start (99.0s) is at or past the end of the video (2.000s) ...
error: --jpeg-quality must be between 2 (best) and 31 (worst), got 100
error: 'frames' already holds 20 file(s) matching 'frame_*.png'. Pass ...
error: ffprobe could not read 'broken.mp4': ...
error: ffmpeg and ffprobe were not found on PATH. Install ffmpeg with ...
error: --fps (20.0) is above the video's own rate of 10.000 ...
error: --scale must be WIDTH:HEIGHT, got 'abc'. Use 'auto' ...
error: --scale 'auto:auto' derives both dimensions from each other ...

When ffmpeg or ffprobe fails, its own diagnosis follows the summary line. All of these are FrameExtractorError subclasses.

Development

git clone https://github.com/jgonmor16/frame-extractor.git
cd frame-extractor
make install
make check       # lint, formatting, types, tests — everything CI runs

Run make on its own to list the other targets. The test suite generates its own sample clip with ffmpeg's testsrc, so there is no fixture video in the repository; tests needing real decoding skip if ffmpeg is missing. CI runs against Python 3.10 through 3.14 on every pull request.

docs/DESIGN.md explains why the ffmpeg invocation looks the way it does — worth reading before changing any of the flags.

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

frame_extractor_ffmpeg-2.0.1.tar.gz (33.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

frame_extractor_ffmpeg-2.0.1-py3-none-any.whl (21.7 kB view details)

Uploaded Python 3

File details

Details for the file frame_extractor_ffmpeg-2.0.1.tar.gz.

File metadata

  • Download URL: frame_extractor_ffmpeg-2.0.1.tar.gz
  • Upload date:
  • Size: 33.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for frame_extractor_ffmpeg-2.0.1.tar.gz
Algorithm Hash digest
SHA256 ec7330664f5b4f5febb390e88e958697daab1fa6968f5952feb25be8504cad21
MD5 1f9eefac90df1de83b6ed25c8917c90e
BLAKE2b-256 3f481438fb430a51dec4e1ede436b837612d7826cc56347a649c01315c75ccf8

See more details on using hashes here.

Provenance

The following attestation bundles were made for frame_extractor_ffmpeg-2.0.1.tar.gz:

Publisher: publish.yml on jgonmor16/frame-extractor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file frame_extractor_ffmpeg-2.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for frame_extractor_ffmpeg-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 83a1c5df0d9e0297022c4550fcd1190b22bb0a9a21100288a48226eac03bcec4
MD5 f4341b4b82eb810c2a846ebc0eb24833
BLAKE2b-256 7845f6dbef9e6590f292f93b9eb1893601dc88c1428d20fb2dbafb01fd8ae810

See more details on using hashes here.

Provenance

The following attestation bundles were made for frame_extractor_ffmpeg-2.0.1-py3-none-any.whl:

Publisher: publish.yml on jgonmor16/frame-extractor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page