Project 02 · Python
Frame Extractor
Extract frames from a video as PNG or JPEG, from the command line or from Python. Built on ffmpeg, and written because ffmpeg accepts several requests it cannot satisfy and reports success anyway.
Problem
Exit zero, wrong answer
For a one-off extraction by someone who knows ffmpeg, a wrapper adds very little. The whole job is one command. What it does add is a guard against the cases where ffmpeg exits zero and produces output that looks reasonable.
| What you ask for | ffmpeg | frame-extractor |
|---|---|---|
--start 99 on a 2-second video |
writes nothing, exits 0 | rejected, exit 1 |
--jpeg-quality 100, valid range 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
|
--scale -1:-1 |
resizes nothing | rejected, exit 1 |
--fps 20 on a 10fps source |
40 files, every second one a duplicate | rejected, exit 1 |
The 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.
Approach
Refuse before running, not after
Every one of those cases is knowable before ffmpeg starts. The
source is probed first, the request is checked against what the
file can actually provide, and anything impossible is rejected
with a single error: line on stderr and a non-zero
exit. No tracebacks.
# ffmpeg: start past the end of a 2-second clip
$ ffmpeg -ss 99 -i in.mp4 -vsync 0 frames/f_%06d.png
$ echo $?
0 # and frames/ is empty
$ frame-extractor in.mp4 frames/ --start 99
error: --start (99.0s) is at or past the end of the video (2.000s)
The same checks back the Python API, which raises
FrameExtractorError subclasses rather than
printing — the library never writes to stdout, so what to
do with a failure stays with the caller.
Evidence
What is actually verified
- Tested
- Python 3.10 through 3.14
- CI runs the full matrix on every pull request. The suite generates its own clip with ffmpeg's testsrc, so no fixture video is committed.
- Documented
- Design notes in the repository
- DESIGN.md explains why the ffmpeg invocation looks the way it does, for whoever changes a flag next.
Scope
Where it does not help
Frames as arrays for machine learning or computer vision work are better served by PyAV or TorchCodec, rather than writing PNGs only to read them straight back. Anything past extraction — trimming, concatenating, re-encoding — is ffmpeg's job and stays there.
Two of the frame-selection modes 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; 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.