| name | ffmpeg-frames-images |
| description | Extract frames, make thumbnails, build sprite/contact sheets, import/export image sequences, and create high-quality GIFs (palettegen+paletteuse) with ffmpeg. Use when the user asks to grab a thumbnail, extract frames, take a screenshot at a timestamp, create a contact sheet or sprite sheet, convert video to images/PNGs/JPGs, convert an image sequence to video, or make a GIF from a video.
|
| argument-hint | [action] [input] |
Ffmpeg Frames Images
Context: $ARGUMENTS
Frame extraction and image I/O with ffmpeg: single stills, periodic frame dumps, contact sheets, sprite sheets for scrubbers, high-quality GIFs, and building video back from an image sequence.
Quick start
- Single screenshot at a timestamp → Step 1 (screenshot) + Step 2
- Dump N frames per second to PNG/JPG → Step 1 (sequence) + Step 2
- Contact sheet / thumbnail grid → Step 1 (sheet) + Step 2
- Sprite sheet for HLS scrubber → Step 1 (sprite) + Step 2
- High-quality GIF from video → Step 1 (gif) + Step 2 (two-pass)
- Build video from PNG/JPG sequence → Step 1 (images-to-video) + Step 2
When to use
- Grab a poster/thumbnail frame at a specific time.
- Produce preview grids (contact sheet) for a long video.
- Generate scrubber sprites for a web player (HLS/DASH image tracks).
- Export frames for ML, computer vision, or VFX pipelines.
- Make a looping GIF for docs/social without visible banding.
- Re-encode a rendered image sequence (e.g. Blender/Nuke output) into MP4.
Step 1 — Pick your output
Choose exactly one shape; each has its own command pattern.
screenshot — one still image at a timestamp.
sequence — many images at regular intervals (fps=1 = 1/sec, fps=1/10 = every 10s).
sheet — one image that is a grid (tile=MxN) of stamps across the video.
sprite — same as sheet but sized for a web player's scrubber.
gif — animated GIF (two-command palette workflow).
images-to-video — reverse direction: image sequence → MP4.
thumbnail-smart — let ffmpeg pick a "representative" frame.
keyframes-only — export only frames marked as keyframes.
Step 2 — Build the command
2a. Single screenshot at 1:30
ffmpeg -ss 00:01:30 -i in.mp4 -vframes 1 -q:v 2 out.jpg
ffmpeg -i in.mp4 -ss 00:01:30 -vframes 1 -q:v 2 out.jpg
ffmpeg -ss 90 -i in.mp4 -vframes 1 -c:v png out.png
2b. One frame per second (or every N seconds)
ffmpeg -i in.mp4 -vf fps=1 frame_%04d.jpg
ffmpeg -i in.mp4 -vf fps=1/10 frame_%04d.jpg
ffmpeg -i in.mp4 -vf fps=1 -q:v 2 frame_%04d.jpg
2c. Contact sheet 4x4 at 5-second intervals
ffmpeg -i in.mp4 \
-vf "fps=1/5,scale=320:-1,tile=4x4" \
-frames:v 1 sheet.jpg
2d. Sprite sheet for HLS scrubber (10x10 grid, 10s interval, 160px wide)
ffmpeg -i in.mp4 \
-vf "fps=1/10,scale=160:-1,tile=10x10" \
-frames:v 1 sprites.jpg
Each sprite cell is 160 x (160/aspect). For a 16:9 video that's 160x90. With 100 cells and a 10s interval you cover 1000 seconds of runtime per sheet. See references/recipes.md for HLS EXT-X-IMAGE-STREAM-INF math.
2e. Smart thumbnail (auto-pick representative frame)
ffmpeg -i in.mp4 -vf "thumbnail=100,scale=640:-1" -frames:v 1 thumb.jpg
thumbnail=100 scans 100 consecutive frames and picks the most "typical" one (lowest histogram distance to the group average). Good for avoiding black/blurred frames.
2f. Keyframes only
ffmpeg -i in.mp4 -vf "select='eq(pict_type,I)'" -vsync vfr key_%04d.jpg
2g. High-quality GIF (TWO commands — palettegen, then paletteuse)
ffmpeg -i in.mp4 \
-vf "fps=15,scale=480:-1:flags=lanczos,palettegen=max_colors=256" \
-y palette.png
ffmpeg -i in.mp4 -i palette.png \
-lavfi "fps=15,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=5" \
-y out.gif
There is no reliable one-shot GIF command that gives this quality. The two-pass palettegen → paletteuse flow is the standard.
2h. Video from image sequence
ffmpeg -framerate 30 -i img_%04d.png \
-c:v libx264 -pix_fmt yuv420p out.mp4
ffmpeg -framerate 30 -pattern_type glob -i 'img_*.png' \
-c:v libx264 -pix_fmt yuv420p out.mp4
ffmpeg -framerate 24 -start_number 100 -i img_%04d.png out.mp4
Step 3 — Verify frame count
Before shipping, confirm you got what you expected.
ls frame_*.jpg | wc -l
ffprobe -v error -select_streams v:0 -show_entries stream=width,height \
-of csv=p=0 sheet.jpg
Available scripts
scripts/frames.py — wrapper around all of the above. Subcommands:
screenshot, every, sheet, sprite, gif, images-to-video.
Supports --dry-run and --verbose. Stdlib-only Python; no prompts.
Workflow
uv run ${CLAUDE_SKILL_DIR}/scripts/frames.py screenshot \
--input in.mp4 --time 00:01:30 --output thumb.jpg
uv run ${CLAUDE_SKILL_DIR}/scripts/frames.py sheet \
--input in.mp4 --cols 4 --rows 4 --interval 5 --width 320 --output sheet.jpg
uv run ${CLAUDE_SKILL_DIR}/scripts/frames.py gif \
--input in.mp4 --fps 15 --width 480 --output out.gif
uv run ${CLAUDE_SKILL_DIR}/scripts/frames.py images-to-video \
--pattern 'img_%04d.png' --fps 30 --output out.mp4
Reference docs
Read references/recipes.md when:
- You need more than 8 recipes (there are 30+ in there).
- You need the JPEG quality scale, PNG compression flags, or image2 demuxer options (
pattern_type, start_number, loop, framerate).
- You're tuning GIF size vs quality (dither tables, fps trade-offs).
- You're computing sprite sheet geometry for
EXT-X-IMAGE-STREAM-INF playlists.
Gotchas
-ss before -i is FAST but keyframe-snapped — the chosen time snaps to the nearest preceding keyframe. Off by up to the GOP length (often ~2s). Great for thumbnails.
-ss after -i is FRAME-ACCURATE but slow — ffmpeg decodes from frame 0 to the target. Use only when exact frame matters.
-q:v 2 is best-JPEG-quality. Scale is 2–31, LOWER is better. 2 is visually lossless-ish, 5 is web-quality, 31 is unusable.
-frames:v 1 vs -vframes 1 — both work. Prefer -frames:v (stream-specific, modern form).
image2 numeric pattern zero-padding must match the %0Nd width. img_%04d.png means files must be img_0001.png, not img_1.png, or you'll get "could not find codec parameters" errors.
- Glob only works if you add
-pattern_type glob. Otherwise ffmpeg treats img_*.png as a literal filename.
- GIF quality REQUIRES the two-pass palette workflow. There is no acceptable single-command alternative; a naked
ffmpeg -i in.mp4 out.gif produces banded, washed-out garbage.
paletteuse=dither=bayer:bayer_scale=5 is the quality/size sweet spot. dither=none bands heavily; dither=floyd_steinberg is prettiest but noticeably larger.
-framerate vs -r. -framerate is an INPUT option for the image2 demuxer (rate at which images are READ). -r is an OUTPUT option (rate STAMPED on the output). For image sequence → video, set -framerate BEFORE -i.
- JPG kills alpha. If the source has transparency (captures, overlays), use PNG or WebP.
tile must match what you feed it. A 4x4 tile expects exactly 16 frames — use -frames:v 1 to stop after the first tiled output and trim the fps to produce at least 16 frames' worth.
- needs N frames in the window. If the source is shorter than N frames, the filter still works but samples what it has. Default is 100.
Examples
Example 1: YouTube-style poster frame at 1:30
ffmpeg -ss 00:01:30 -i lecture.mp4 -vframes 1 -q:v 2 -vf "scale=1280:-1" poster.jpg
Result: 1280-wide JPEG, keyframe-snapped (fast).
Example 2: Scrubber sprite sheet for a 1h video
For a 3600s video, sampling every 10s = 360 thumbs. A 19x19 grid (361 cells) covers it:
ffmpeg -i movie.mp4 \
-vf "fps=1/10,scale=160:-1,tile=19x19" \
-frames:v 1 sprites.jpg
Result: one JPG, ~3040x1710 pixels at 16:9, ready to slice on the client.
Example 3: Animated GIF of a 10s clip
ffmpeg -ss 10 -t 10 -i clip.mp4 \
-vf "fps=15,scale=480:-1:flags=lanczos,palettegen" -y palette.png
ffmpeg -ss 10 -t 10 -i clip.mp4 -i palette.png \
-lavfi "fps=15,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=5" \
-y demo.gif
Result: ~2–3 MB GIF, no visible banding.
Example 4: Blender render → MP4
Blender wrote render_0001.png … render_0240.png at 24 fps:
ffmpeg -framerate 24 -i render_%04d.png \
-c:v libx264 -pix_fmt yuv420p -crf 18 render.mp4
Result: 10-second MP4, high quality, broadly compatible.
Troubleshooting
Error: Could not find codec parameters for stream 0 (Video: none): unknown codec
Cause: image pattern's zero-padding doesn't match the actual filenames.
Solution: rename files, or change %04d to match the real padding, or switch to -pattern_type glob -i 'img_*.png'.
Error: Output file #0 does not contain any stream
Cause: -ss placed after -i with an offset past the end of the video, OR thumbnail/select filtered everything out.
Solution: verify the timestamp with ffprobe -show_entries format=duration; for select filters, loosen the predicate or pair with -vsync vfr.
Error: GIF looks washed out / banded / has dithering noise halo
Cause: skipped the palette step, or used dither=none.
Solution: run the two-pass palettegen + paletteuse=dither=bayer:bayer_scale=5 workflow in section 2g.
Error: Image sequence imports at wrong speed / too fast / too slow
Cause: you used -r (output rate) when you needed -framerate (input rate). ffmpeg defaults to 25 fps on the image2 demuxer if neither is set.
Solution: put -framerate N BEFORE -i img_%04d.png.
Error: tile filter complains about missing frames / produces a partial grid
Cause: fps didn't produce enough frames to fill MxN.
Solution: either lower the interval (more frames), reduce the grid (2x2), or accept a partial tile.
Error: JPG is huge / low-quality
Cause: missing -q:v.
Solution: add -q:v 2 (best) through -q:v 7 (web). Remember: lower = better.