0

Basically I want to extract all frames from the video where the images will be resized to be 720p. All of this I know how to do. But for testing I want to extract every Nth frame. So every 30th image from a 30FPS video or every 10th image, etc. How can I do that?

Also the extracted image filenames should correspond to the frame numbers so not sure if the first frame is 0 or 1?

2 Answers 2

3

This prints the frame number on the left upper corner of each frame output and selects only 1 frame every 30 frames:

ffmpeg -reinit_filter 0 -i "input.mkv" -vf "drawtext=text='%{n}':start_number=1:fontcolor=white:bordercolor=black:borderw=3:fontsize=200,select='not(mod(n,30))',scale=-1:720" -fps_mode vfr -y "IMG_Folder\%6d.jpg"

If you want the counter to start at 0 then 30,60,90 instead of 1,31,61,91 remove the startnumber part.

In Windows you can use something like this to do this to all videos inside a folder and save the pictures in a folder same name as video:

for %a in (*.mp4 *.mkv) do if not exist "%~na" md "%~na"& ffmpeg -reinit_filter 0 -i "%~a" -vf "drawtext=text='%{n}':fontcolor=white:bordercolor=black:borderw=3:fontsize=200,select='not(mod(n,30))',scale=-1:720" -fps_mode vfr -y "%~na\%6d.jpg"
13
  • Thanks it's returning some errors like cannot load default config file: no such file for fontconfig error. also could not open file IMG_Folder/000001.jpg. Can that folder be created relative to the input video file?
    – Joan Venge
    Commented Jun 1 at 17:36
  • @Joan Venge I think you can ignore the default config error maybe because I didn't specify a font to be used but it uses the default font if none specified. I guess there is no option to automatically create a folder with ffmpeg unless you use a script that creates it for you. Commented Jun 1 at 17:39
  • @Joan Venge please see updated answer to save pics in output folder same name as video. Commented Jun 1 at 17:47
  • Thanks I am not sure where to specify the path in your for loop but I changed it to python using subprocess and call ffmpeg like this but I am only getting 7 images out of a 5 min video when i specify 60 as frame interval in a 60 fps video, and i didnt see any text printed on the images. Not sure if I am doing something wrong: paste.ofcode.org/NVw33RZbt8mmEKLaVYdMb7
    – Joan Venge
    Commented Jun 1 at 17:56
  • You say "only" how many images are you expecting? The intention is to run the code in the windows command line at the same path where the videos are. Commented Jun 1 at 18:07
1

At it's most simple, extract single image from frame of video:

ffmpeg -i video.mp4 -ss HH:MM:SS -vframes 1 image.jpg
#!/bin/bash

file="$1"
name="${file%.*}"

# Extract the first frame
ffmpeg -hide_banner -i "$file" -vf "select=eq(n\,0)" -vsync vfr "$name-first_frame.jpg"

# Set the frame interval
frame_interval=30

# Calculate digits required for frame numbering
total=$(ffmpeg -i "$file" -map 0:v:0 -c copy -f null -y /dev/null 2>&1 | grep -Eo 'frame= *[0-9]+' | grep -Eo '[0-9]+' | tail -1)
digits=${#total}

# Extract every nth frame and rename to include actual frame number
ffmpeg -hide_banner -i "$file" -vf "select='not(mod(n\,$frame_interval))',showinfo" -vsync vfr temp_%0${digits}d.jpg 2>&1 | grep 'pts_time' | awk -v name="$name" -v digits="$digits" -F'|' '{ 
    split($5, a, "="); 
    split($8, b, "="); 
    printf "mv temp_%0" digits "d.jpg " name "-frame_%0" digits "d.jpg\n", b[2], b[2]*30 
}' | bash

# Remove temporary files
rm temp_*.jpg

exit 0

Save as: frame_extract.sh
Change mode executable: chmod +x frame_extract.sh
Usage: ./frame_extract.sh input_file.mkv


# Define the file path as a parameter
param(
    [Parameter(Mandatory=$true)]
    [string]$FilePath
)

# Get the file name without extension
$name = [System.IO.Path]::GetFileNameWithoutExtension($FilePath)

# Extract the first frame
& ffmpeg -hide_banner -i $FilePath -vf "select=eq(n\,0)" -vsync vfr "$name-first_frame.jpg"

# Set the frame interval
$frameInterval = 30

# Calculate digits required for frame numbering with zero padding
$totalFramesOutput = & ffmpeg -i $FilePath -hide_banner -map 0:v:0 -c copy -f null - 2>&1
$totalFrames = ($totalFramesOutput | Select-String -Pattern 'frame=\s*[0-9]+' | ForEach-Object { $_.Matches } | ForEach-Object { $_.Value.TrimStart("frame=") } | Select-Object -Last 1)
$digits = $totalFrames.Length

# Extract every nth frame and rename to include actual frame number
$output = & ffmpeg -hide_banner -i $FilePath -vf "select='not(mod(n\,$frameInterval))',showinfo" -vsync vfr "temp_%0${digits}d.jpg" 2>&1
$output | Select-String -Pattern 'pts_time' | ForEach-Object {
    if ($_ -match 'pts_time:(\d+)') {
        $frameNumber = [int]($matches[1] * $frameInterval)
        $paddedFrameNumber = $frameNumber.ToString("D$digits")
        $tempFileName = "temp_" + $frameNumber.ToString("D$digits") + ".jpg"
        $newFileName = "$name-frame_$paddedFrameNumber.jpg"
        Rename-Item -Path $tempFileName -NewName $newFileName
    }
}

# Remove temporary files
Remove-Item "temp_*.jpg"

I'm not the greatest at PowerShell, but this should be close.
I have no way to test it, but it should help. (Hopefully!)


Added logic to grab the first frame.
Output filename uses frame count, not sequential numbers.

2
  • Thanks I am on windows so I will try to convert this into powershell.
    – Joan Venge
    Commented Jun 1 at 17:37
  • @JoanVenge Added a .ps1 version.
    – JayCravens
    Commented Jun 1 at 19:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .