frame-extractor
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_framesreturnslist[Frame]rather thanlist[Path]. Add.pathwhere 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.
Metadata
Release files for frame-extractor-ffmpeg 2.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| frame_extractor_ffmpeg-2.0.1.tar.gz | 33.7 kB | With provenance | Details for frame_extractor_ffmpeg-2.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| frame_extractor_ffmpeg-2.0.1-py3-none-any.whl | Python 3 | none | any | Details for frame_extractor_ffmpeg-2.0.1-py3-none-any.whl |
Total release size: 55.3 kB
Release files / frame_extractor_ffmpeg-2.0.1.tar.gz
| Download URL | frame_extractor_ffmpeg-2.0.1.tar.gz |
|---|---|
| Size | 33.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ec7330664f5b4f5febb390e88e958697daab1fa6968f5952feb25be8504cad21
|
|
BLAKE2b-256 checksum How to use checksums |
3f481438fb430a51dec4e1ede436b837612d7826cc56347a649c01315c75ccf8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 20, 2026.
Transparency logRelease files / frame_extractor_ffmpeg-2.0.1-py3-none-any.whl
| Download URL | frame_extractor_ffmpeg-2.0.1-py3-none-any.whl |
|---|---|
| Size | 21.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
83a1c5df0d9e0297022c4550fcd1190b22bb0a9a21100288a48226eac03bcec4
|
|
BLAKE2b-256 checksum How to use checksums |
7845f6dbef9e6590f292f93b9eb1893601dc88c1428d20fb2dbafb01fd8ae810
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 20, 2026.
Transparency logSecurity for frame-extractor-ffmpeg 2.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for frame_extractor_ffmpeg-2.0.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 20, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.