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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec7330664f5b4f5febb390e88e958697daab1fa6968f5952feb25be8504cad21
|
|
| MD5 |
1f9eefac90df1de83b6ed25c8917c90e
|
|
| BLAKE2b-256 |
3f481438fb430a51dec4e1ede436b837612d7826cc56347a649c01315c75ccf8
|
Provenance
The following attestation bundles were made for frame_extractor_ffmpeg-2.0.1.tar.gz:
Publisher:
publish.yml on jgonmor16/frame-extractor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
frame_extractor_ffmpeg-2.0.1.tar.gz -
Subject digest:
ec7330664f5b4f5febb390e88e958697daab1fa6968f5952feb25be8504cad21 - Sigstore transparency entry: 2529828088
- Sigstore integration time:
-
Permalink:
jgonmor16/frame-extractor@2d5791714919a72b234d02cf654c79687f3f00dd -
Branch / Tag:
refs/tags/v2.0.1 - Owner: https://github.com/jgonmor16
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2d5791714919a72b234d02cf654c79687f3f00dd -
Trigger Event:
release
-
Statement type:
File details
Details for the file frame_extractor_ffmpeg-2.0.1-py3-none-any.whl.
File metadata
- Download URL: frame_extractor_ffmpeg-2.0.1-py3-none-any.whl
- Upload date:
- Size: 21.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
83a1c5df0d9e0297022c4550fcd1190b22bb0a9a21100288a48226eac03bcec4
|
|
| MD5 |
f4341b4b82eb810c2a846ebc0eb24833
|
|
| BLAKE2b-256 |
7845f6dbef9e6590f292f93b9eb1893601dc88c1428d20fb2dbafb01fd8ae810
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
frame_extractor_ffmpeg-2.0.1-py3-none-any.whl -
Subject digest:
83a1c5df0d9e0297022c4550fcd1190b22bb0a9a21100288a48226eac03bcec4 - Sigstore transparency entry: 2529830198
- Sigstore integration time:
-
Permalink:
jgonmor16/frame-extractor@2d5791714919a72b234d02cf654c79687f3f00dd -
Branch / Tag:
refs/tags/v2.0.1 - Owner: https://github.com/jgonmor16
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2d5791714919a72b234d02cf654c79687f3f00dd -
Trigger Event:
release
-
Statement type: