获取截图
首先了解如何通过ffmpeg对视频进行截图,这里介绍两种方式,通过时间和通过帧数
- 对指定帧截图,下面这个命令是获取input.mp4第666帧的图像
ffmpeg -i input.mp4 -vf "select=gte(n\,666)" -vframes 1 output.jpg -y
- 对指定时间截图,下面这个命令是获取input.mp4第47.50秒的图像
ffmpeg -ss 47.50 -i input.mp4 -vframes 1 output.jpg -y
Tips!
-y
的作用是,如果已经存在output.jpg,那么执行命令不会提示文件已经存在而是直接覆盖-ss
放在-i
选项前面-ss
的形式有两种,一种是示例中那样,代表秒数且可以有小数位;另一种的形式是00:02:44.432
。两种方式都支持反向索引,加个负号即可
图像缩放
既然要自定义,那么截图的图像分辨率大小自然也应当支持指定,通常用scale滤镜来做
如果想同时使用多个滤镜,那么用逗号分隔开来即可,示例如下。
- 获取input.mp4第666帧图像,并缩放为短边为2160像素的尺寸
ffmpeg -i input.mp4 -vf "select=gte(n\,666),scale=-1:2160" -vframes 1 output.jpg -y
Tips!
- 通常为了简便,使用scale滤镜时指定短边的尺寸即可,但是对于某些像素比不是1:1的视频(比如一些日剧),建议明确指定长边尺寸,比如
scale=3840:2160
,否则得到的图像看起来是变形了的
画一张“画布”
获取截图自定义尺寸在前面两步已经完成了,要自定义缩略图子图位置可以先做一个特定大小的画布,然后按意愿将截图放置于特定的位置,这样就得到一个自定义形式的缩略图了。
- 生成一张尺寸为3840x2160、颜色为0xcaff70的“画布”
ffmpeg -f lavfi -i color=c=0xcaff70:s=3840x2160 -vframes 1 blank.jpg -y
Tips!
- 颜色可以是16进制,也可以用对应的单词代表,具体参见文档,以及可以使用
random
随机给定颜色 - 参数s是明确的分辨率,不能像scale那样可以只指定一边
图像叠加放置
有了画布,那接下来就是在画布上放置图像了,使用overlay滤镜。
- 在一张颜色为0xcaff70、尺寸为3840x2160图像上的(100,10)位置(左上角为原点,向下是y,向右是x)放置一张分辨率为1920x1080的图像
ffmpeg -i blank.jpg -i input.jpg -vfoverlay=100:10 output.jpg -y
- 在画布上放置两张图像,第一张图位置是(100,10),第二张图位置是(500,500),均以画布左上角为原点
ffmpeg -i blank.jpg -i input1.jpg -i input2.jpg -filter_complex [0:v][1:v]overlay=100:10[img];[img][2:v]overlay=500:500 output.jpg -y
Tips!
- 多个输入文件混合处理,使用filter_complex,连续处理时使用分号隔开,如果最后要分别得到不同产物,可以用-map来分配,具体参见文档中
labeled filtergraph outputs
部分。更详细的还可以看这里 - 为了避免叠加过程中多次生成图片,在这里使用了label,也就是示例中的[img],可以简单理解为它是一个中间的临时变量,存放上一步的产物,然后可以在后续过程中作为输入或输出使用,这里是用作输入。
- label可以作为输入,但文件输入的使用形式是[file_index:stream_specifier],也就是示例中的[0:v]这样的部分,意思是选取第1张图片,更多详细使用选项见官方文档
添加文字
至此,现在可以生成自定义形式的缩略图了,但是光是图怎么行,还得加上文字对吧!
加文字使用drawtext滤镜
ffmpeg -i blank.jpg -i input1.jpg -i input2.jpg -filter_complex [0:v][1:v]overlay=100:10[img];[img][2:v]overlay=500:500[img];[img]drawtext=fontfile="Microsoft YaHei Mono.ttf":fontcolor=#cb0f70:fontsize=175:x=3200:y=1800:text='weimo' output_with_text.jpg -y
Tips!
- fontfile用于指定字体文件,如果字体不放在执行命令所在路径,那么请在命令中使用完整路径,另外请使用单引号将它引起来以避免空格带来其他的问题。如果你的文字含有中文,建议使用支持中文的字体,否则会乱码。
- fontcolor用来设定颜色,这和前面的color一样用法,这里使用
#
也是其写法之一 - fontsize是文字的大小,xy是位置,请注意文字大小和位置的搭配使用,否则会出现文字只有部分的情况
- text部分就是你要输入的文字了,同样建议使用单引号引起来。
- 不使用text参数,还可以使用textfile=text.txt来传入文字,这样直接支持文字换行,参见
Q:如果使用text且要换行怎么办?
A:根据ffmpeg源代码,当遇到\n,\v,\f,\r
时会新起一行。那么输入weimo^L66666
这样的text就能将字符换行了,这个^L并不是两个字符,它是\f
,可以键入Ctrl+L
,或者按住Alt
键,然后输入它的ascii码12后松开Alt键,以输入这个特殊字符。四个字符对应的ascii码刚好是10、11、12、13,使用Alt输入的方法还能输入中文,比如依次按下Alt+4+5+2+1+7+Alt
会得到字符啊
。另外还可以把不同行当作新的输入来处理换行需求,具体参见StockOverflow的提问
结语
通过上述的命令,可自行定义一个样式,然后基于ffmpeg截图后合成一张缩略图(以及带上文字水印)。
其他需要注意的地方:
- 根据视频尺寸计算合适的画布大小
- 根据画布尺寸和缩略图尺寸计算相对位置
- 根据文字大小和文字数量排布确定文字位置
- 尽量使用label减少中间文件的产生
如有错误请指正~
自用脚本
写了一个powershell脚本,需要手动指定视频路径
复制保存为ps1
脚本,设定视频路径,并将字体放在和视频一个位置,运行脚本即可
脚本可以自行扩展,字体文件似乎用完整路径会有问题,原因未知。
这个脚本是截图缩放到720p和360p,然后按上面说的逻辑拼图而成。
考虑到视频末尾会有黑屏现象,因此在减了300帧进行计算
通过时间截图更快,所以脚本获取了帧数后计算了每张图的对应时间,然后基于时间截图
代码和效果图如下:
$video_path = 'E:\我的莫格利男孩50.mp4'
$video_name = Split-Path -leaf $video_path
$video_folder = Split-Path -Path $video_path
$font_path = 'Microsoft YaHei Mono.ttf'
&cd $video_folder
$text_cmd = 'drawtext=fontfile=''{1}'':fontcolor=#50616d:fontsize=75:x=(w-text_w)/2:y=(360-text_h*2)/2:text=''{0}'',drawtext=fontfile=''{1}'':fontcolor=#50616d:fontsize=75:x=(w-text_w)/2:y=(360+text_h)/2:text=''@weimo#blog''' -f $video_name, $font_path
$mode = "大小图模式"
$pic_number_big = 4
$pic_number_small = 14
$pic_number = $pic_number_big + $pic_number_small
$ffmpeg_output = &ffmpeg -i $video_path -map 0:v:0 -c copy -f null - 2>&1
$ffmpeg_output
$pattern=[regex]"frame.*?(\d+)"
$f = $pattern.Matches($ffmpeg_output)
$framenumber = [int]$f[$f.count - 1].Groups[1].Value - 300
$pattern = [regex]"(\d+) fps"
$f = $pattern.Matches($ffmpeg_output)
$framerate = [int]$f.Groups[1].Value
$stepnumber = [math]::Floor($framenumber / $pic_number)
for($i=0;$i -lt $pic_number_big;$i++)
{
$frametime = $stepnumber * ($i + 1) / $framerate
$ffmpeg_cmd = '&ffmpeg -ss {0} -i "{1}" -vframes 1 -vf "scale=1280:-1" -q:v 2 "{2}.jpg" -y' -f $frametime, $video_path, ($i + 1)
iex $ffmpeg_cmd
}
for($i=$pic_number_big;$i -lt $pic_number;$i++)
{
$frametime = $stepnumber * ($i + 1) / $framerate
$ffmpeg_cmd = '&ffmpeg -ss {0} -i "{1}" -vframes 1 -vf "scale=640:-1" -q:v 2 "{2}.jpg" -y' -f $frametime, $video_path, ($i + 1)
iex $ffmpeg_cmd
}
$xy_large = (0,0),(1280,0),(2560,360),(640,720)
$xy_samll = (2560,0),(3200,0),(0,720),(1920,720),(0,1080),(1920,1080),(2560,1080),(3200,1080),(0,1440),(640,1440),(1280,1440),(1920,1440),(2560,1440),(3200,1440)
$xy = $xy_large + $xy_samll
for($i=0;$i -lt $pic_number;$i++)
{
$xy[$i][9] += 360
}
&ffmpeg -f lavfi -i color=c=#bce672:s=3840x2160 -vframes 1 backround.jpg -y
$ipart = ""
for($i=0;$i -lt $pic_number;$i++)
{
$ipart = $ipart + '-i {0}.jpg ' -f ($i + 1)
}
$fcpart = ""
for($i=0;$i -lt $pic_number;$i++)
{
if($i -eq 0){
$fcpart = $fcpart + '[{0}:v][{1}:v]overlay={2}:{3}[img];' -f $i, ($i + 1), $xy[$i][0], $xy[$i][10]
}
elseif($i -ne ($pic_number - 1)){
$fcpart = $fcpart + '[img][{0}:v]overlay={1}:{2}[img];' -f ($i + 1), $xy[$i][0], $xy[$i][11]
}
else{
$fcpart = $fcpart + '[img][{0}:v]overlay={1}:{2}[img];[img]{3}' -f ($i + 1), $xy[$i][0], $xy[$i][12], $text_cmd
}
}
$cmd = '&ffmpeg -i backround.jpg {0} -filter_complex "{1}" -q:v 2 {2}[email protected] -y' -f $ipart, $fcpart, $video_name
iex $cmd
for($i=0;$i -lt $pic_number;$i++)
{
$del_cmd = '&Remove-Item {0}.jpg ' -f ($i + 1)
iex $del_cmd
}
&Remove-Item backround.jpg