Extract Video Frames
GUIDE 06 / Frames & extraction

FFmpeg Select Frames by Number, Timestamp, or Every Nth Frame

Choose decoded indices or presentation times, avoid duplicated image outputs, and verify your selection against a source-frame manifest.

ExtractVideoFrames.com editorialReviewed November 3, 2026Methods & evidence ↓
SELECTION LAB / COUNT PICTURES OR READ THE CLOCK20-frame VFR model
0001020304050607080910111213141516171819
Highlighted pictures are retained; their output filenames will count from the first image written. The VFR window follows the downloadable fixture’s timestamps.

FFmpeg's select filter can pull exact pictures out of a video, but only if you first decide what kind of reference you are using: a decoded-frame index, an Nth-frame rule, a presentation-time window, or one requested moment. Those are four different questions, and the same command line does not answer all of them. This guide gives one short command per task, explains what each one does and does not guarantee, and then shows how to check that the exported images are the source frames you meant to get.

Every example uses the same small test clip, a numbered-frame fixture described in the verification section, so a filename, a frame number, and a timestamp can be compared directly.

Pick the selection rule first

Decide which reference you hold before writing a command. Each row below maps to a different select expression, and the rows only agree with each other when frame timing is perfectly regular.

What you haveSelection ruleExpression inside select='…'What you get
A short list of frame positionsZero-based decoded-frame indexeq(n,0)+eq(n,5)+eq(n,12)One image per listed index that exists
"Every 5th frame"Every Nth decoded framenot(mod(n,5))Indices 0, 5, 10, 15, …
A time rangePresentation-time windowgte(t,0.35)*lt(t,1.25)Zero, one, or many frames
One moment in timeA stated policy: first frame at or after, last at or before, or nearestgte(t,0.63) with -frames:v 1; the other two need a lookupOne frame, but which one depends on the policy
"One image every N seconds"A time rule, not an Nth ruleisnan(prev_selected_t)+gte(t-prev_selected_t,0.5)Depends on the timestamps (section 4)

Five different references are easy to blur together. Here is how they differ on the two test clips used throughout this guide: both hold the same 20 pictures, labeled F00 to F19 on screen. In the constant-frame-rate (CFR) clip they are exactly 0.1 s apart. In the variable-frame-rate (VFR) clip the gaps are uneven.

ReferenceWhat it identifiesOn the test clips
Decoded-frame index (n)Position in the sequence reaching the filter, starting at 0F05 is n=5 in both clips
Presentation timestamp (pts, t)When the picture is shownF05 starts at 0.5 s in the CFR clip and at 1.0 s in the VFR clip
Every Nth frameIndex arithmetic, mod(n,N)Every 5th is F00, F05, F10, F15 in both clips, but at different times in the VFR clip
Interval in elapsed secondsSpacing on the clockMatches every-Nth only when the timing is regular
Output image number (%03d)A counter of files writtenimg_001.png holds F00, not F01

Frame-rate details beyond this are out of scope here. The one fact you need: a VFR file can have an average or nominal rate, but not one uniform presentation interval, so an average such as a reported average cannot tell you which index sits at a given time.

Select an explicit list of frame numbers

To export decoded frames 0, 5, and 12 from the first video stream:

bash
mkdir idx &&
ffmpeg -i vfr.mkv -map 0:v:0 -vf "select='eq(n,0)+eq(n,5)+eq(n,12)'" -fps_mode passthrough idx/img_%03d.png

On both test clips this wrote exactly three files, and decoding the burned-in marker in each output showed F00, F05, and F12 in that order. The file names were img_001.png, img_002.png, and img_003.png.

What each part does

  • mkdir idx creates a fresh output directory. The && stops this command if directory creation fails, preventing an accidental reuse of old images. The command has no -y, so FFmpeg will ask before overwriting a file.

  • -map 0:v:0 names the stream: the first video stream of the first input. Without it FFmpeg chooses a video stream for you, which matters for files with more than one.

  • select runs its expression on every frame that reaches it and passes the frame on when the result is non-zero. In the filter documentation, n is the sequential number of the filtered frame, starting from 0. Each eq(n,k) is 1 for one index and 0 otherwise, and adding them gives a non-zero result for any listed index.

  • -fps_mode passthrough keeps only the frames select passed. Section 6 shows what happens without it.

  • img_%03d.png is a counter, not a frame number. img_001.png is the first image written, and here that is F00.

Frame numbers that start at 1. n starts at zero. If a player or editor calls the first frame "frame 1," subtract 1 before writing the expression.

Find an index with a structured frame manifest

bash
ffprobe -v error -select_streams v:0 -show_frames -show_entries frame=pts,best_effort_timestamp -show_entries stream=time_base -show_streams -of json vfr.mkv > source-frames.json

Enumerate the JSON frames array for the chosen stream, starting at zero. Do not count arbitrary CSV or text lines: missing values, side data or blank lines can break that shortcut. The list refers to this full decode; adding a seek or earlier filter can change the sequence later reaching select.

Shell quoting. An expression with commas passes through three layers: the shell, the filtergraph parser, and the expression parser. The filtergraph escaping notes describe the layers. The tested Bash 5.2.21 results on FFmpeg 6.1.1:

FormResult
-vf "select='eq(n,0)+eq(n,5)+eq(n,12)'"Worked: three images
-vf select='eq(n,0)+eq(n,5)+eq(n,12)' (the common older form)Worked: three images
-vf "select=eq(n,0)+eq(n,5)+eq(n,12)" (no quoting inside)Failed with exit code 8: No such filter: '0)+eq(n'
-filter_script:v sel.txt, a file containing select=eq(n,0)+eq(n,5)+eq(n,12)Worked: three images

The unquoted form fails because the comma inside eq(n,0) ends the select filter and starts a new one. The filter-script form keeps the expression out of the shell, which helps when quoting rules differ. Only Bash was tested here. PowerShell and cmd.exe quote differently, and no commands for them were checked.

Option placement. Input-specific options normally precede the applicable -i; output-specific options precede the applicable output filename. FFmpeg also has global options, so position must be interpreted for the specific option. -fps_mode is an output option: putting it before -i fails with "cannot be applied to input url."

What this command does not guarantee

  • It does not mean "frame n of the original file" if you add -ss, trim, fps, or a decoder-discard option before the filter. Section 5 shows each case.

  • It does not return anything for an index beyond the end. Asking for eq(n,5)+eq(n,99) on the CFR clip returned one image (F05) and exit code 0. Asking only for eq(n,99) wrote nothing and printed "Output file is empty," also with exit code 0. Count the output files.

  • It does not seek. select examines frames as they are decoded, so reaching a late index means decoding everything before it. Adding -frames:v 3 (the number of listed indices) after -fps_mode passthrough stops FFmpeg once three images are written. That worked on both clips, but only with passthrough; section 6 shows why.

Select every Nth decoded frame

To keep every 5th decoded frame, starting with the first:

bash
mkdir nth &&
ffmpeg -i vfr.mkv -map 0:v:0 -vf "select='not(mod(n,5))'" -fps_mode passthrough nth/img_%03d.png

Both test clips produced four files containing F00, F05, F10, and F15. The command has the same shape as the one in the previous section. Only the expression changed.

mod(n,5) is the remainder after dividing the index by 5, and not() turns a remainder of 0 into 1. So the frames kept are n = 0, 5, 10, 15, and so on. Frame 0 is always included. To start at a different phase, compare the remainder with another value: eq(mod(n,5),2) returned F02, F07, F12, and F17 on both clips. The separate framestep=5 filter returned the same F00, F05, F10, F15 as the select form here, but it too counts the sequence reaching it.

Every Nth frame is not every N seconds. The rule above counts pictures. It says nothing about the time between them. The two clips make the difference visible. The verified selections are listed in the source-to-output table below.

A minimum-time-spacing rule is different again: isnan(prev_selected_t)+gte(t-prev_selected_t,0.5) requests the first picture at least half a second after the previous pick, not the nearest picture on a regular grid. See the official select examples. Avoid exact floating-point modulo tests on t; choose an explicit policy and check boundary pictures.

What this command does not guarantee

  • Evenly spaced pictures in time. Spacing follows the clip's timestamps.

  • That "every 5th" starts where you expect if anything changes the count before select (see the counting-context section below).

  • A matching image count when the sync mode is left at its default. A default output-sync mode can add or remove output images; use explicit passthrough when the goal is to retain the selected pictures.

Select frames by presentation time

Time selection gives two different results depending on what you ask for. A window returns every frame whose start time falls inside it, which can be zero, one, or many. A single moment needs a policy, because a frame rarely starts at exactly the time you name.

A presentation-time window

To keep every frame that starts from 0.35 s up to, but not including, 1.25 s:

bash
mkdir win &&
ffmpeg -i vfr.mkv -map 0:v:0 -vf "select='gte(t,0.35)*lt(t,1.25)'" -fps_mode passthrough win/img_%03d.png

In the select documentation, t is a frame's presentation timestamp in seconds. Multiplying gte(t,0.35) by lt(t,1.25) gives 1 only when both are true, so the window is half-open: the start is included and the end is not. That choice stops a frame sitting exactly on a boundary from being counted in two adjacent windows.

The same window returned a different number of pictures on each clip:

WindowCFR clipVFR clip
0.35 s to before 1.25 s9 frames: F04 to F12 (0.4 to 1.2 s)8 frames: F04 (0.4 s), then F05 to F11 (1.0 to 1.24 s)
0.5 s to before 0.9 s4 frames: F05 to F080 frames: no frame starts between 0.4 s and 1.0 s

The empty VFR result is the important one. FFmpeg printed "Output file is empty, nothing was encoded" but still exited with code 0, so a script that only checks the exit status will not notice.

Boundaries that land exactly on a frame start. A window of 0.4 s to 1.2 s has frames starting at both edges in both clips. The two ways of writing it differ:

ExpressionCFR clipVFR clip
gte(t,0.4)*lt(t,1.2) (end excluded)8 frames: F04 to F116 frames: F04 to F09
between(t,0.4,1.2) (both ends included)9 frames: F04 to F127 frames: F04 to F10

On these clips the floating-point comparisons behaved as written at both edges. That depends on the time base and the values, so test a boundary frame before relying on it. Comparing integer timestamps avoids the conversion: gte(pts,400)*lt(pts,1200) returned the same sets, but the numbers are in the stream's time base (1/1000 s here), so check it first with FFprobe.

Seeking and a presentation-time filter have different roles. For membership in an exact time window, state whether the bounds include each frame's presentation start and verify the resulting set. Do not infer that a packet-reading limit is the same as a displayed-frame selection rule.

What this command does not guarantee

  • At least one image. A window can be empty.

  • A fixed number of images. The count follows how many frame starts the window contains.

  • That t means the time on a player's timeline. If you seek or rebase timestamps first, t changes (see the counting-context section below).

One picture for a requested moment

Before choosing a command, choose a policy. For a requested time T there are three reasonable answers, and they can all differ. With T = 0.63 s:

PolicyCFR clipVFR clip
First frame at or after TF07 (0.7 s)F05 (1.0 s)
Last frame at or before TF06 (0.6 s)F04 (0.4 s)
Nearest frame to TF06 (0.03 s away)F04 (0.23 s away)

For an exact request such as T = 0.5 s, the CFR clip has F05 starting at precisely 0.5 s, so every policy returns it. The VFR clip has no frame starting at 0.5 s: at-or-after returns F05 (1.0 s), while at-or-before and nearest both return F04 (0.4 s). "The frame at exactly 0.5 s" has an answer in one clip and no answer in the other, so a method should not be called exact-timestamp extraction unless it says what it does in the second case.

First frame at or after T is the direct case:

bash
mkdir moment &&
ffmpeg -i vfr.mkv -map 0:v:0 -vf "select='gte(t,0.63)'" -frames:v 1 -fps_mode passthrough moment/img_%03d.png

gte(t,0.63) is true for every frame from T onward, and -frames:v 1 stops after the first one written. It returned F07 on the CFR clip and F05 on the VFR clip. An input seek, -ss 0.63 -i vfr.mkv -frames:v 1, gave the same pictures on both clips in these tests. A seek and a frame selection do different jobs, though. -ss positions the read, and when FFmpeg decodes, the default is to start decoding earlier and discard frames before the target (the -accurate_seek behavior described in the ffmpeg documentation). Neither is a rule about which frame you want. The select expression is.

Resolve a requested time to a source index

For at-or-before or nearest-start policies, inspect the full timestamp list, then select the resolved index. A before-policy needs the last eligible start; a nearest-policy also needs to compare the following start. Use an explicit tie rule. Here the earlier picture wins a tie.

Save the FFprobe JSON from above, then use Python 3:

python
import json
from fractions import Fraction

with open("source-frames.json", encoding="utf-8") as f:
    report = json.load(f)
unit = Fraction(report["streams"][0]["time_base"])
starts = []
for i, frame in enumerate(report["frames"]):
    raw = frame.get("pts", frame.get("best_effort_timestamp"))
    if raw is None or raw == "N/A":
        raise SystemExit("Missing timestamp: inspect the file manually")
    starts.append((i, int(raw) * unit))
if not starts or any(b[1] < a[1] for a, b in zip(starts, starts[1:])):
    raise SystemExit("Empty or non-monotonic presentation list")
target = Fraction("0.63")
before = [item for item in starts if item[1] <= target]
print("Before:", before[-1][0] if before else "none")
print("Nearest:", min(starts, key=lambda item: (abs(item[1]-target), item[0]))[0])

Use the returned index in select='eq(n,INDEX)' on the same full input, with the same selected stream and no earlier seek or filter. The script compares exact time-base fractions, not rounded six-decimal strings. It addresses frame starts; choosing the picture held on screen at a target also requires interpreting durations, gaps and final-frame behavior.

Average FPS does not locate a frame in VFR material. An average near ten FPS might suggest index 10 at one second. The frame that starts at 1.0 s is F05. Only the actual timestamp list resolves it.

What these commands do not guarantee

  • A frame that starts exactly at T. They return the frame your policy names.

  • The same picture across files. The same T gave different frames on the two clips.

  • That the before and nearest indexes still match after you add -ss, trim, or another filter.

Understand seeking and the frame-index counting context

n counts the sequence arriving at the select filter, starting at zero. With the unmodified full decode it can represent the source decoded ordinal. An input seek, preceding trim/fps filter or decoder-discard option can change what reaches it. “Index 7 after a seek” is not necessarily index 7 of the original file.

Presentation timestamps can also be shifted or preserved by options such as -copyts. Check the filter's actual t/pts values with showinfo before combining a seek with a time predicate. A nominal player timeline and a filter's timestamps are not automatically the same reference.

Packet order is not decoded presentation order. Formats with reordered pictures can expose different PTS and DTS. Use a decoded frame manifest, not row numbers from -show_packets, when matching a select-filter index.

For a different goal—exporting the coding subset—see codec-keyframe extraction. That selection is based on decoder flags, not an arbitrary source index.

Prevent unexpected duplicate or missing output images

select only chooses frames. A separate stage then decides how frames are timed on the way to the output file, and that stage can add or remove images. FFmpeg's -fps_mode option controls it. Per the ffmpeg documentation, passthrough sends each frame on with its own timestamp, cfr duplicates and drops frames to hit an exact constant rate, vfr passes frames through but drops any that would repeat a timestamp, and the default auto picks between cfr and vfr depending on the output format.

In the separately verified keyframe fixture, leaving output sync at its default produced 119 PNGs from four selected keyframes; explicit passthrough produced four. That demonstrates a possible failure mode, not a universal default image count.

Use -fps_mode passthrough on the output when retaining the selected pictures is the goal. vfr additionally drops repeated timestamps; the two modes need not be identical on an unusual source. Modern FFmpeg documents per-stream -fps_mode; older -vsync examples should be translated with their semantics in mind, not copied mechanically.

Do not add -r or another fps filter merely to number the files. Those options change output timing and can change the selected set. Output limits such as -frames:v 3 also do not prove which three pictures survived earlier stages.

What -fps_mode passthrough does not guarantee

  • Sensible timestamps. It keeps the selected frames' own timestamps, which may be far apart. For still images that does not matter.

  • Unique output file names if you put timestamps in them. See the next section.

  • Protection from other stages that change frames. A later fps filter or a rate option still applies.

Verify filenames against source indices and timestamps

The downloadable controls contain 20 pictures labeled F00–F19. The CFR source starts at 0 ms and advances in 100 ms gaps. The VFR source starts at [0, 100, 200, 300, 400, 1000, 1040, 1080, 1120, 1160, 1200, 1240, 1280, 1320, 1360, 1400, 1800, 1900, 2000, 2100] ms.

The examples were rerun with FFmpeg 6.1.1 using FFV1/Matroska and an explicit encoder time base of 1/1000. Each selected PNG was matched by its digest to a PNG decoded from the full source.

SelectionCFR source indicesVFR source indices
Explicit 0, 5, 120, 5, 120, 5, 12
Every fifth, beginning at 00, 5, 10, 150, 5, 10, 15
0.35 ≤ start < 1.25 seconds4 through 124 through 11
First start at or after 0.63 seconds75

Use showinfo after select to log the selected timestamps. Its own n counts the frames reaching showinfo, so it restarts across the selected output rather than preserving source indices.

bash
ffmpeg -i vfr.mkv -map 0:v:0 -vf "select='eq(n,0)+eq(n,5)+eq(n,12)',showinfo" -fps_mode passthrough -f null -

Sequential image filenames count output images. With default image2 numbering, img_001.png means the first image written; it does not mean source frame 1. The image2 -frame_pts 1 alternative uses output timestamp ticks. The units depend on the output time base and timestamp transformations, not simply seconds or source ordinals. Keep a source manifest when source time matters.

Troubleshooting a selection

SymptomCheck
FFmpeg exits successfully but writes nothingRequested indices may not exist, or no frame start falls inside the time window. Inspect both count and decoded source list.
Too many images or repeated first picturesCheck output synchronization and any added cadence filter. Count files and compare their pixels.
Right count, wrong picturesCheck zero-based indexing, chosen stream, seeking and earlier filters.
Parser errors around commasUse the quoted filter expression shown above; quoting rules differ between Bash, PowerShell and cmd.
Timestamp filenames seem wrongRead the output time base and account for rebasing or rescaling.
A target lies between frame startsState whether you want at-or-after, at-or-before, nearest-start, or the picture held at that time.

Questions before saving the result

Does every fifth frame include the first picture?

not(mod(n,5)) includes index 0. A different starting phase needs a different expression, such as eq(mod(n,5),2) for indices 2, 7, 12 and 17.

Can I calculate a frame index from average FPS?

For a verified uniform timeline and an explicit start convention, arithmetic can suggest a position. It does not establish an index in a VFR file. See constant versus variable frame rate for the timestamp distinction.

Can I select pictures without a command line?

The video frame extractor can capture chosen stills or sampled batches, while the frame-by-frame viewer helps inspect neighboring pictures in supported browser codecs. These are different interfaces; neither runs the FFmpeg index-list expression for you.

Try the original examples

CFR/VFR clips, expected selections and test script ↓
Methods & evidence

Index, every-fifth, time-window and at-or-after selections were reproduced on owned CFR/VFR FFV1 files with FFmpeg 6.1.1. PNG digests were matched to full decoded source images.

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