FFMPEG:从可变长度的视频中提取 20 张图像

Posted

技术标签:

【中文标题】FFMPEG:从可变长度的视频中提取 20 张图像【英文标题】:FFMPEG: Extracting 20 images from a video of variable length 【发布时间】:2012-01-30 13:14:57 【问题描述】:

我已经非常深入地浏览了互联网,但我没有找到我需要的东西,只有它的变体,这不是我想要使用的东西。

我有几个不同长度的视频,我想从每个视频中从头到尾提取 20 张图片,以展示视频的最广泛印象。

所以一个视频是 16m 47s 长 => 总共 1007s => 我必须每 50 秒制作一个视频快照。

所以我想使用 ffmpeg 的 -r 开关,其值为 0.019860973(eq 20/1007),但 ffmpeg 告诉我帧速率太小了...

我想出的唯一方法是编写一个脚本,该脚本使用操纵的 -ss 开关调用 ffmpeg 并使用 -vframes 1 但这对我来说非常慢而且有点偏离,因为 ffmpegs 自己计算图像...

【问题讨论】:

【参考方案1】:

我也试图找到这个问题的答案。我使用了radri的答案,但发现它有一个错误。

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

每 2 秒生成一帧,因为 -r 表示帧速率。在这种情况下,每秒 0.5 帧,或每 2 秒 1 帧。

使用相同的逻辑,如果您的视频长度为 1007 秒,而您只需要 20 帧,则每 50.53 秒需要一帧。换算成帧率,就是每秒 0.01979 帧。

所以你的代码应该是

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

我希望这对某人有所帮助,就像它帮助了我一样。

【讨论】:

【参考方案2】:

我知道我参加聚会有点晚了,但我认为这可能会有所帮助:

对于每个遇到 ffmpeg 为每一帧生成图像的问题的人,这是我解决它的方法(使用 blahdiblah 的答案):

首先,我抓取了视频中的总帧数:

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

然后我尝试使用 select 来抓取帧:

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

但是,无论 mod(n,100) 设置为什么,ffmpeg 都会吐出太多帧。我必须添加 -vsync 0 来纠正它,所以我的最终命令如下所示:

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

(其中 100 是您想要使用的帧频,例如,每 100 帧)

希望能给大家省点麻烦!

【讨论】:

非常感谢您提供 -vsync 选项!节省了我很多时间!【参考方案3】:

您可以尝试将视频转换为 N 个图像吗?

ffmpeg -i video.avi image%d.jpg

更新:

或每 2 秒提取一次帧:

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

更新:

获取视频时长:

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

然后,根据您的编程语言,将其转换为秒。例如,在 php 中,你可以这样做:

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

其中 $time 是视频时长。你可以执行 $duration_in_seconds / 20

ffmepg -i video.avi -r $duration_in_seconds/20 -f image2 output_%05d.jpg

【讨论】:

是的,我可以,但这不是我要找的... 第一个命令提取视频的每一帧。我只需要 20 张图像(意味着不同位置的 20 帧)。第二条命令每2秒提取一次,是的,但是如果电影的长度超过40秒,结果是超过20张图片... 是的。但是得到视频时长,除以 20,你会得到秒数。然后,你得到了 ffmepg -i video.avi -r NUMBER_OF_SECONDS_RESULTED -f image2 output_%05d.jpg 不,不幸的是它不起作用。如果我尝试使用 -r 50.53(即 1007/20)ffmpeg 会产生 1000 多张图像...【参考方案4】:

通常,ffmpeg 会在帧到来时对其进行处理,因此任何基于总持续时间/总帧数的内容都需要进行一些预处理。

我建议编写一个简短的 shell 脚本来使用类似的东西来获取总帧数

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

然后使用select video filter 选择您需要的帧。所以 ffmpeg 命令看起来像

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

除了不是每百分之一帧,100 将替换为在 shell 脚本中计算的数字,从而为您提供总共 20 帧。

【讨论】:

我喜欢这种方法!但不知何故,它对我不起作用,但我想我遇到了计算问题。所以我用一个总帧数为 60406 --> 60406 帧/20 张图像 --> 每 3020 帧的视频进行了尝试。所以我的命令看起来像 ffmpeg -i &lt;input_file&gt; -vf "select='not(mod(n, 3020))" -f image2 &lt;output_file&gt; 但我得到了超过 6000 张图片 --> ???【参考方案5】:

我遇到了同样的问题,并想出了这个似乎可以解决问题的脚本:

#/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

要使用它,请将其保存为 .sh 文件并使用以下参数运行它以导出 20 帧:

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

这将为您提供在整个视频中均匀分布的指定帧数。

【讨论】:

非常好。我也将 $1 输入文件用于total_frames 计算。在输出之前我还必须create the output folder,否则我会收到“输入/输出错误”。 帧似乎在整个视频长度中分布不均。视频最后一部分的帧丢失了。是因为scale=0 截断了速率值吗?我删除了vframes 部分并计算了 189 帧的输出,得到了 236 帧。即使没有vframes,我不应该得到 189 吗?【参考方案6】:

我有一个类似的问题,即如何在未知长度的电影中提取一帧。感谢这里的答案,我想出了这个确实非常有效的解决方案,我认为发布 php 代码会很有用:

<?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 );
?> 

【讨论】:

【参考方案7】:

我也遇到了同样的问题,使用选择过滤器对我不起作用,而且大型视频的使用速度非常慢,所以我在 python 中使用了这个脚本

它的作用是以秒为单位获取持续时间,根据屏幕截图的数量计算间隔,最后在输入文件之前使用 seek 在每个间隔截取屏幕截图

#!/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: (\d2:\d2:\d2\.\d2)', 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()

【讨论】:

您从ffprobe 获得持续时间,这很好,但就像这里的许多答案一样,您正在解析“人类”输出,而不是机器可解析的输出。请参阅How to get video duration with ffprobe? 了解更高效、更不易损坏的方法。【参考方案8】:

我确实喜欢这样做,并且 100% 成功。

$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");

【讨论】:

以上是关于FFMPEG:从可变长度的视频中提取 20 张图像的主要内容,如果未能解决你的问题,请参考以下文章

ffmpeg4版本,提取视频帧保存成ppm图片

ffmpeg4版本,提取视频帧保存成ppm图片

如何使用 php 从图像创建视频?

VLC 冻结从使用 ffmpeg 的图像创建的低 1 FPS 视频

FFMPEG 视频图像解封装解码

如何使用 ffmpeg 提取 8khz 的音频