FFMPEG: Extracting 20 images from a video of variable length
Asked Answered
F

8

20

I've browsed the internet for this very intensively, but I didn't find what I needed, only variations of it which are not quite the thing I want to use.

I've got several videos in different lengths and I want to extract 20 images out of every video from start to the end, to show the broadest impression of the video.

So one video is 16m 47s long => 1007s in total => I have to make one snapshot of the video every 50 seconds.

So I figured using the -r switch of ffmpeg with the value of 0.019860973 (eq 20/1007) but ffmpeg tells me that the framerate is too small for it...

The only way I figured out to do it would be to write a script which calls ffmpeg with a manipulated -ss switch and using -vframes 1 but this is quite slow and a little bit off for me since ffmpegs numerates the images itself...

Factual answered 30/12, 2011 at 12:22 Comment(0)
D
19

I was trying to find the answer to this question too. I made use of radri's answer but found that it has a mistake.

ffmpeg -i video.avi -r 0.5 -f image2 output_%05d.jpg

produces a frame every 2 seconds because -r means frame rate. In this case, 0.5 frames a second, or 1 frame every 2 seconds.

With the same logic, if your video is 1007 seconds long and you need only 20 frames, you need a frame every 50.53 seconds. Translated to frame rate, would be 0.01979 frames a second.

So your code should be

ffmpeg -i video.avi -r 0.01979 -f image2 output_%05d.jpg

I hope that helps someone, like it helped me.

Druse answered 5/2, 2013 at 15:0 Comment(0)
S
18

I know I'm a bit late to the party, but I figured this might help:

For everyone having the issue where ffmpeg is generating an image for every single frame, here's how I solved it (using blahdiblah's answer):

First, I grabbed the total number of frames in the video:

ffprobe -show_streams <input_file> | grep "^nb_frames" | cut -d '=' -f 2

Then I tried using select to grab the frames:

ffmpeg -i <input_file> -vf "select='not(mod(n,100))'" <output_file>

But, no matter what the mod(n,100) was set to, ffmpeg was spitting out way too many frames. I had to add -vsync 0 to correct it, so my final command looked like this:

ffmpeg -i <input_file> -vsync 0 -vf "select='not(mod(n,100))'" <output_file>

(where 100 is the frame-frequency you'd like to use, for example, every 100th frame)

Hope that saves someone a little trouble!

Shavonda answered 1/10, 2012 at 1:21 Comment(2)
thank you so much for the -vsync option! Saved me a lot of time!Interstratify
-vsync 0 was a life saver, thank you!Farfamed
D
6

You could try convert video to N number of images ?

ffmpeg -i video.avi image%d.jpg

Update:

Or extract frame every 2 seconds:

ffmepg -i video.avi -r 0.5 -f image2 output_%05d.jpg

Update:

To get the video duration:

ffmpeg -i video.avi 2>&1 | grep 'Duration' | cut -d ' ' -f 4 | sed s/,//

Then, depends on your programming language, you convert it into seconds. For example, in PHP, you could do it like this:

$duration = explode(":",$time); 
$duration_in_seconds = $duration[0]*3600 + $duration[1]*60+ round($duration[2]);

where $time is the video duration. The you can execute $duration_in_seconds / 20

ffmepg -i video.avi -r $duration_in_seconds/20 -f image2 output_%05d.jpg
Defence answered 30/12, 2011 at 12:29 Comment(3)
Yes I could, but that's not what I'm looking for... The first command extracts EVERY SINGLE FRAME of the video. I need only 20 images (meaning 20 frames at different positions). The second command extracts every 2 seconds, yes, but if the length of the movie is more than 40 seconds, the result is more than 20 images...Factual
Yes. But get the video duration, divide it by 20, and you will get the number of seconds. Then, you got ffmepg -i video.avi -r NUMBER_OF_SECONDS_RESULTED -f image2 output_%05d.jpgDefence
No, unfurtunatly it's not working. If I try it with -r 50.53 (which is 1007/20) ffmpeg produces 1000+ images...Factual
S
5

In general, ffmpeg processes frames as they come, so anything based on the total duration/total number of frames requires some preprocessing.

I'd recommend writing a short shell script to get the total number of frames using something like

ffprobe -show_streams <input_file> | grep "^nb_frames" | cut -d '=' -f 2

and then use the select video filter to pick out the frames you need. So the ffmpeg command would look something like

ffmpeg -i <input_file> -vf "select='not(mod(n,100))'" <output_file>

except that instead of every hundredth frame, 100 would be replaced by the number calculated in the shell script to give you 20 frames total.

Shamefaced answered 9/1, 2012 at 23:56 Comment(1)
I love the approach! But somehow it's not working for me, but I guess I'm having a calculation problem. So I tried it with a video that has a total number of frames of 60406 --> 60406 frames / 20 images --> every 3020th frame. So my command looks like ffmpeg -i <input_file> -vf "select='not(mod(n, 3020))" -f image2 <output_file> but I get over 6000+ images --> ???Factual
K
3

I was having the same problem and came up with this script which seems to do the trick:

#/bin/sh
total_frames=`ffmpeg -i ./resources/iceageH264.avi -vcodec copy -acodec copy -f null dev/null 2>&1 | grep 'frame=' | cut -f 3 -d ' '`
numframes=$3 
rate=`echo "scale=0; $total_frames/$numframes" | bc`
ffmpeg -i $1 -f image2 -vf "select='not(mod(n,$rate))'" -vframes $numframes -vsync vfr $2/%05d.png

To use it, save it as a .sh file and run it using the following parameters to export 20 frames:

./getFrames.sh ~/video.avi /outputpath 20

This will give you the specified number of frames distributed equally throughout the video.

Kraul answered 12/11, 2013 at 17:4 Comment(2)
Very nice. I used the $1 input file for the total_frames calculation, as well. I also had to create the output folder before outputting or else I got an "Input/output error".Diuresis
Frames do not seem to be distributed equally throughout the length of the video. Frames from the last part of the video are missing. Is it because scale=0 truncates the rate value? I removed the vframes part and calculated an output of 189 frames and I got 236 frames. Shouldn't I get 189 even without vframes?Diuresis
D
1

i had a similar question, namely how to extract ONE frame halfway a movie of unknown length. Thanks to the answers here, I came up with this solution which works very well indeed, I thought posting the php code would be useful:

<?php 
header( 'Access-Control-Allow-Origin: *' );
header( 'Content-type: image/png' );
// this script returns a png image (with dimensions given by the 'size' parameter as wxh)
// extracted from the movie file specified by the 'source' parameter
// at the time defined by the 'time' parameter which is normalized against the 
// duration of the movie i.e. 'time=0.5' gives the image halfway the movie.
// note that this can also be done using php-ffmpeg..
$time = floatval( $_GET[ 'time' ] );
$srcFile = $_GET[ 'source' ];
$size = $_GET[ 'size' ];
$tmpFile = tempnam( '/tmp', 'WTS.txt' );
$destFile = tempnam( '/tmp', 'WTS.png' );
$timecode = '00:00:00.000';

// we have to calculate the new timecode only if the user 
// requests a timepoint after the first frame
if( $time > 0.0 ){
    // extract the duration via a system call to ffmpeg 
    $command = "/usr/bin/ffmpeg -i ".$srcFile." 2>&1 | grep 'Duration' | cut -d ' ' -f 4 | sed s/,// >> ".$tmpFile;
    exec( $command );

    // read it back in from tmpfile (8 chars needed for 00:00:00), skip framecount
    $fh = fopen( $tmpFile, 'r' );
    $timecode = fread( $fh, 8 );
    fclose( $fh );

    // retieve the duration in seconds
    $duration = explode( ":", $timecode ); 
    $seconds = $duration[ 0 ] * 3600 + $duration[ 1 ] * 60 + round( $duration[ 2 ] );
    $timepoint = floor( $seconds * $time );

    $seconds = $timepoint % 60;
    $minutes = floor( $timepoint / 60 ) % 60;
    $hours = floor( $timepoint / 3600 ) % 60;

    if( $seconds < 10 ){ $seconds = '0'.$seconds; };
    if( $minutes < 10 ){ $minutes = '0'.$minutes; };
    if( $hours < 10 ){ $hours = '0'.$hours; };

    $timecode = $hours.':'.$minutes.':'.$seconds.'.000';
}

// extract an image from the movie..
exec( '/usr/bin/ffmpeg -i '.$srcFile.' -s '.$size.' -ss '.$timecode.' -f image2 -vframes 1 '.$destFile );

// finally return the content of the file containing the extracted frame
readfile( $destFile );
?> 
Deplane answered 26/7, 2013 at 6:48 Comment(0)
C
0

I had this same issue, using select filters didn't work for me and using rate was very slow for large videos so I came with this script in python

What it does is get duration in seconds, calculate interval based on number of screenshots, and finally take a screenshot at every interval using seek before input file

#!/usr/bin/python3
import re
from os import makedirs
from os.path import basename, join, dirname, isdir
from sys import argv, exit
from subprocess import call, run, Popen, PIPE, STDOUT, CalledProcessError
from ffmpy3 import FFmpeg, FFRuntimeError, FFExecutableNotFoundError

argv.pop(0)

for ifile in argv:
    #Get video info
    duration = run(['ffprobe', '-hide_banner', '-i', ifile], stderr=PIPE, universal_newlines=True)
    #Extract duration in format hh:mm:ss
    duration = re.search(r'Duration: (\d{2}:\d{2}:\d{2}\.\d{2})', duration.stderr)
    #Convert duration to seconds
    duration = sum([a*b for a,b in zip([3600, 60, 1],
                    [float(i) for i in duration.group(1).split(':')])])
    #Get time interval to take screenshots
    interval = int (duration / 20)

    fname = basename(ifile)[:-4]
    odir = join(dirname(ifile),fname)

    if not isdir(odir):
        makedirs(odir, mode=0o750)

    ofile = join(odir,fname)

    #Take screenshots at every interval
    for i in range (20):
        run([])
        ff = FFmpeg(
            global_options='-hide_banner -y -v error',
            inputs={ifile: f'-ss {i * interval}'},
            outputs={f'{ofile}_{i:05}.jpg': f'-frames:v 1 -f image2'}
            )
        ff.run()
Consolidation answered 23/4, 2020 at 6:30 Comment(1)
You're getting duration from ffprobe, which is good, but like many answers here you're parsing the "human" output, not the machine parsable output. See How to get video duration with ffprobe? for a more efficient, less fragile method.Sign
R
0

I did like this and it was 100% successful.

$time = exec("ffmpeg -i input.mp4 2>&1 | grep 'Duration' | cut -d ' ' -f 4 | sed s/,//");
$duration = explode(":",$time);
$duration_in_seconds = $duration[0]*3600 + $duration[1]*60+ round($duration[2]);
$duration_in_seconds = $duration_in_seconds/20;

exec("ffmpeg -i input.mp4 -r 1/$duration_in_seconds thumb_%d.jpg");
Rabbin answered 7/9, 2020 at 7:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.