Extract Video Frames
GUIDE 05 / Frames & extraction

Extract Keyframes with FFmpeg: Commands, Timestamps, and Verification

Export decoder-flagged keyframes, keep a source-time manifest, and compare keyframe flags with I-pictures and visual scene changes.

ExtractVideoFrames.com editorialReviewed November 3, 2026Methods & evidence ↓
GOP LAB / A VISUAL CUT IS A DIFFERENT SIGNALTested source positions
Fixed GOP 30
0s1s2s3s
Cut; scene keys enabled
0s1s2s3s
Cut; scene keys disabled
0s1s2s3s
The visual cut is at 2 seconds in the lower two clips. With scene-key insertion disabled, frame 60 at that cut was a non-key B-picture in the verified H.264 fixture.

To extract codec keyframes from a video as PNG images, use FFmpeg's -skip_frame nokey decoder option and preserve the selected frames without forcing a new frame rate:

bash
mkdir keyframes &&
ffmpeg -hide_banner -n -skip_frame nokey -i "input.mp4" \
  -map 0:v:0 -fps_mode passthrough \
  "keyframes/key_%04d.png"

Run this in Bash or another shell that supports && and backslash line continuations. Replace input.mp4 with your file path. If keyframes already exists, mkdir fails and the extraction will not run; choose a fresh folder name instead. These examples were checked in Bash. Other shells use different line-continuation and quoting rules; do not paste the Bash backslashes into a PowerShell command.

This command writes one image for each frame marked as a keyframe by the decoder in the first video stream. It does not choose the sharpest images, detect all scene changes, or pick a particular frame number or time. The filenames key_0001.png, key_0002.png, and so on are output sequence numbers, not positions in the original video.

First, choose the kind of frame you actually need

Several different jobs are commonly called “extract keyframes”:

GoalWhat is selectedAppropriate approach
Export codec keyframesFrames flagged as keyframes by FFmpeg's decoder-skip_frame nokey
Export I-picturesFrames whose decoded picture type is Iselect='eq(pict_type,I)'
Identify visible cutsFrames with a high scene-change scoreScene-detection workflow, not a keyframe command
Find a useful cover or summary imagePictures relevant to your subject or quality criteriaReview or rank candidate stills
Retrieve a frame at an exact index or timestampA deliberately specified positionFrame-index/time-selection workflow

A keyframe flag describes how a video frame is marked for decoding or random access. An I-picture describes intra-coded picture type. These often coincide in ordinary closed-GOP H.264 files, but they are not universally identical. Depending on codec, GOP structure, and decoder, an I-picture need not represent the same random-access boundary as a flagged keyframe. Neither designation says the image is aesthetically better than nearby pictures.

A visible scene cut is yet another signal: an encoder may insert a keyframe at a cut, or it may keep the existing GOP schedule. A perfectly static shot can also contain multiple keyframes. For precise command definitions, see the FFmpeg codec options and FFmpeg select filter documentation.

Extract keyframe images without accidental duplicates

The recommended command has four important parts:

  • -skip_frame nokey goes before -i because it configures how the input video is decoded. FFmpeg documents nokey as discarding all frames except keyframes.

  • -map 0:v:0 chooses the first video stream from the first input. This is helpful when a file has more than one video stream or attached pictures. Change the mapping if you need another video stream.

  • -fps_mode passthrough passes selected frames through the output-sync stage rather than duplicating them to fill a regular playback frame rate. Avoid adding -r 1, -vf fps=1, or other cadence conversion to a keyframe-only export: those are different tasks.

  • key_%04d.png asks the image muxer for an incrementing, zero-padded filename. -n prevents overwriting existing output files. PNG preserves the decoded image without another lossy JPEG encoding stage, but it cannot undo compression already present in the video.

Why bother specifying output sync? In a controlled test reproduced with FFmpeg 6.1.1 and checked against the supplied 7.1.5 run, a four-second 30 fps H.264 file contained four flagged keyframes. The command above wrote four PNGs. The otherwise similar command with no -fps_mode wrote 119 images, mostly duplicated to fill a 30 fps output sequence. That is an observed behavior on the tested build, not a promise that every version or container makes the same default choice.

Older tutorials often use -vsync 0 or -vsync vfr. Those options explain historical examples, but current FFmpeg documents per-stream -fps_mode and identifies -vsync as deprecated. passthrough is particularly clear when the job is to retain every selected keyframe; vfr also prevents two output frames from sharing a timestamp and therefore has slightly different semantics. See the FFmpeg command-line documentation.

Get the actual keyframe timestamps with FFprobe

Export filenames alone do not provide a reliable keyframe timestamp manifest. Use FFprobe to inspect decoded frame metadata:

bash
ffprobe -v error -select_streams v:0 \
  -show_frames \
  -show_entries frame=key_frame,pict_type,pts_time,best_effort_timestamp_time \
  -of json "input.mp4" > all-frames.json

In all-frames.json, inspect each frame's key_frame value (1 means flagged as a keyframe), pict_type (I, P, or B for common codecs), and timestamp fields (*_time is in seconds when available). The JSON contains all decoded video frames; this is intentional, because it lets you compare keyframes with I-pictures and determine their ordinal positions in the full decoded sequence.

If you want a concise list, this Python 3 snippet reads that file and prints the zero-based ordinal of each flagged keyframe together with its time:

python
import json

with open("all-frames.json", encoding="utf-8") as f:
    frames = json.load(f)["frames"]

print("decoded_ordinal,seconds,picture_type")
for ordinal, frame in enumerate(frames):
    if frame.get("key_frame") == 1:
        seconds = frame.get(
            "best_effort_timestamp_time",
            frame.get("pts_time", "unknown")
        )
        print(f"{ordinal},{seconds},{frame.get('pict_type', 'unknown')}")

Here, decoded_ordinal means the frame's zero-based order in FFprobe's full decoded-frame listing for v:0; it is not a packet number or the output image's sequence number. The displayed seconds are decimal renderings of timestamp values, not mathematical proof of exact rational timing. Some streams lack usable timestamps, have a nonzero start, or expose additional codec-specific metadata. Keep the original JSON for an audit rather than inferring times from nominal FPS.

For a short keyframe-only listing, FFprobe also accepts the decoder's -skip_frame nokey option, but that filtered list cannot tell you a keyframe's ordinal among all decoded frames. For field names and the JSON writer, see the FFprobe documentation.

Example: four keyframes do not mean four consecutive source frames

In the test clip, the original stream contained 120 decoded frames at 30 fps and keyframes at the following points:

Zero-based decoded ordinalPresentation timestampDecoded picture typeKeyframe flag
00.000000 sI1
301.000000 sI1
602.000000 sI1
903.000000 sI1

The four exported PNGs were numbered 1–4. This is expected: image sequence numbering counts output images, not intervening frames that the decoder skipped.

What does -frame_pts true put in the filename?

You may see keyframe-export commands that add -frame_pts true:

bash
mkdir keyframes_pts &&
ffmpeg -hide_banner -n -skip_frame nokey -i "input.mp4" \
  -map 0:v:0 -fps_mode passthrough -frame_pts true \
  "keyframes_pts/pts_%08d.png"

FFmpeg's image2 muxer documents this as naming images with the output packet presentation timestamp (PTS). A PTS is an integer measured in the relevant time-base units; it is not automatically seconds, milliseconds, or an original frame index.

In our 30 fps test, the files were named pts_00000000.png, pts_00000030.png, pts_00000060.png, and pts_00000090.png. These numbers happened to resemble the source ordinals. When the same extraction explicitly used -enc_time_base:v 1:1000, they became 0, 1000, 2000, and 3000 instead. Both runs extracted the same four pictures. Never treat a -frame_pts filename as an original source frame number without validating its time base and any timestamp transformations.

For source times, keep the FFprobe manifest. For the precise image2 naming rule, see the FFmpeg formats documentation.

Keyframes versus I-pictures: two commands that may differ

To export frames by I-picture type rather than their keyframe flag, use a filter:

bash
mkdir i_pictures &&
ffmpeg -hide_banner -n -i "input.mp4" \
  -map 0:v:0 -vf "select='eq(pict_type,I)'" \
  -fps_mode passthrough "i_pictures/i_%04d.png"

To use the frame's keyframe flag inside the filter instead, use:

bash
mkdir flagged_frames &&
ffmpeg -hide_banner -n -i "input.mp4" \
  -map 0:v:0 -vf "select='key'" \
  -fps_mode passthrough "flagged_frames/key_%04d.png"

The first filter tests pict_type; the second tests key. Unlike -skip_frame nokey, these filters normally receive frames after full input decoding. In the controlled closed-GOP and scene-cut H.264 fixtures for this guide, -skip_frame nokey, select='key', select='eq(pict_type,I)', and -skip_frame nointra happened to select identical pictures. That result verifies those fixtures, not universal equivalence across codecs, damaged streams, and open-GOP structures. If the distinction matters, compare both the key_frame and pict_type fields in the original video's FFprobe manifest.

Do not assume a PNG exported from an I-picture is automatically sharper than a PNG from a P- or B-picture. Interframe compression is a coding method, not a ranking of how well each decoded still depicts the scene.

A scene cut can occur between codec keyframes

A useful counterexample is a four-second 30 fps clip with an obvious visual change at 2.000 seconds. We encoded it with H.264 using a 90-frame GOP limit and disabled automatic scene-cut keyframe insertion. FFprobe reported only two flagged keyframes: at 0.000 seconds (ordinal 0) and 3.000 seconds (ordinal 90). FFmpeg's separate scene-change selection flagged the image at 2.000 seconds (ordinal 60), which was a non-key B-picture in that file.

That observation illustrates two different signals, not a failure of FFmpeg:

  • A codec keyframe extraction follows the markers and structure produced by the encoder.

  • A scene-detection workflow analyzes changes in the visible image and may find cuts between those markers.

Conversely, in a version of the fixture that allowed scene-cut insertion, the encoder added a keyframe at 2.000 seconds. Whether these sets overlap therefore depends on the encoding, not merely the content of the scene. If your real goal is a storyboard or useful scene thumbnails, do not treat a keyframe export as a guaranteed scene summary.

Troubleshooting unexpected results

I got dozens of nearly identical images instead of a few keyframes. Check whether -fps_mode passthrough is on the output, and remove -r, fps=..., or other framerate-conversion settings. On the tested four-keyframe file, allowing default synchronization produced 119 images rather than four.

I got one image, zero images, or a surprising count. Inspect key_frame and pict_type with FFprobe. Keyframe spacing is decided by the codec and encoder settings; it is not inherently one per second. All-intra formats may mark many or all frames as keyframes. Verify -map 0:v:0 is pointing to the intended video stream. Corrupt or partially readable streams may not decode as expected.

-skip_frame nokey does not work in my command. Place it before the input's -i, not after. -vf and -fps_mode, by contrast, belong to the output side. Check ffmpeg -version and ffmpeg -h full if using an older or customized build.

The PTS filenames are huge, negative, or do not match times. A PTS is a tick count, and its units may change between input and image output. Files can also have nonzero start timestamps. Check pts_time in FFprobe and the output time base instead of dividing filenames by a guessed FPS. Use sequential filenames if the manifest is the source of timing truth.

Two methods report different counts. Compare key_frame and pict_type on the full decoded stream before diagnosing a bug. The keyframe flag and I-picture test express different conditions. Check that both commands use the same video stream and have not applied a framerate filter, seek, or output frame limit.

I need frame 142 or the picture nearest 00:12.500. Keyframe extraction is not the right operation: it cannot promise that the chosen ordinal or timestamp is a codec keyframe. Use the specific-frame selection workflow for that task. If you only need to inspect adjacent decoded frames visually, the frame-by-frame video viewer is an alternative, subject to browser codec support. To capture chosen stills or sampled batches rather than codec-defined keyframes, use the video frame extractor.

Verify before trusting the export

For a repeatable result, preserve the original video, run ffmpeg -version and ffprobe -version, save the unfiltered FFprobe frame manifest, record the selected stream and FFmpeg command, and count the written images. Compare their count and content with the frames flagged key_frame=1. Keep timestamp values in their original field and units; do not reconstruct them from filenames unless you have independently confirmed the output time base.

-skip_frame nokey extracts decoder-flagged keyframes. select='eq(pict_type,I)' selects I-pictures. A scene-detection filter finds visual change, and a representative-image workflow chooses useful stills. They can produce overlapping images, but they answer different questions.

Try the original examples

Keyframe fixture, manifest and reproduction script ↓
Methods & evidence

Three owned H.264 fixtures were rerun with FFmpeg 6.1.1 and cross-checked against supplied 7.1.5 results. Counts and source timestamps describe those fixtures, not every codec.

Examples and calculations are scoped to their stated settings. Source links support the instructions; an illustration is not a screenshot of an application test.

← Explore all guides