Skip to main content

通过 FFmpeg 实现关键帧动画的一些尝试

·679 words·4 mins
Table of Contents

本文环境
#

  • ffmpeg version n6.1.1 Copyright (c) 2000-2023 the FFmpeg developers

  • 原始图片为 579x250 的 PNG 素材

生成静止动画
#

首先我们生成一段 10 秒的静止动画

命令如下

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" \
-pix_fmt yuva420p \
-y output.mp4

这里讲一下基本的参数,重复的参数,后面就不会提及了。

-r 60:

  • 设置输出视频的帧率为 60 fps。

-loop 1:

  • 将输入图片循环播放。

-t 10:

  • 设置输出视频的时长为 10 秒。

-i input.png:

  • 指定输入文件为 input.png

因为 libx264 要求视频的宽度和高度为偶数,如果我们不重新编辑宽和高,那么会有以下的报错

[libx264 @ 0x6094398d0e40] width not divisible by 2 (579x250)
[vost#0:0/libx264 @ 0x6094398d0a80] Error while opening encoder - maybe incorrect parameters such as bit_rate, rate, width or height.
Error while filtering: Generic error in an external library
[out#0/mp4 @ 0x6094398cfac0] Nothing was written into output file, because at least one of its streams received no packets.

参考 https://stackoverflow.com/questions/20847674/ffmpeg-libx264-height-not-divisible-by-2

-vf "pad=ceil(iw/2)\*2:ceil(ih/2)\*2":

  • 使用 vf 参数进行视频过滤,宽度和高度向上取整到最近的偶数。

-pix_fmt yuva420p:

  • 将输出视频的像素格式设置为 yuva420p,这种格式支持透明度(Alpha 通道)。

平移效果
#

平移可以通过 overlay 叠加来做,或者使用 crop 在大画布上移动裁剪

基础平移
#

假设起始帧是左上,终止帧是右下。我们描绘一个向右下角的平移过程

实现命令

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex "\
color=green:s=1280x720, fps=fps=60[bg]; \
[bg][0:v]overlay=y='t*72':x='t*128':shortest=1[video] \
" \
-preset ultrafast -map "[video]" -y output.mp4

通过 color=green:s=1280x720, fps=fps=60[bg]; 创建一个 1280x720 分辨率的绿色背景视频,帧率为 60 fps,并将其标记为 [bg]。然后将输入图片叠加到绿色背景上,随着时间 t 以每秒 72 像素的速度在 y 轴上移动,以每秒 128 像素的速度在 x 轴上移动,并将输出标记为 [video]shortest=1 参数确保输出视频长度与最短输入对齐

循环平移
#

假设起始帧是左上,中间帧左下,终止帧回到左上

实现命令如下

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex "\
color=green:s=1280x720, fps=fps=60[bg]; \
[bg][0:v]overlay=y='if(lt(t,5),t*72*2,(t-5)*72*2)':shortest=1[video] \
" \
-preset ultrafast -map "[video]" -y output.mp4

叠加的位置在 y 轴上的移动是由时间 t 控制的。使用 if(lt(t,5),t*72*2,(t-5)*72*2) 表达式:

  • if(lt(t,5),t*72*2,(t-5)*72*2):如果 t 小于 5 秒,则 y 轴的位置为 t*72*2;如果 t 大于或等于 5 秒,则 y 轴的位置为 (t-5)*72*2

简谐运动
#

研究了一下,自定义弧线运动无法支持。因为 FFmpeg 的这边都是通过公式去做,用户在画布上的轨道无法很好拟合成一个公式。这里用简谐运动做个示例,看一下就好

实现命令如下

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex "\
color=green:s=1280x720, fps=fps=60[bg]; \
[bg][0:v]overlay=y='360 - cos(2*PI*2*t/5)*360': \
x='360 - cos(2*PI*2*t/10)*360': \
shortest=1[video] \
" \
-preset ultrafast -map "[video]" -y output.mp4

移动的另一种方式
#

实现命令

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex " \
[0:v]crop=ih:ih:iw-ih-(iw-ih)*t/20:0,trim=duration=10,scale=-1:720 \
" \
-c:a copy -pix_fmt yuv420p \
-y output.mp4

旋转
#

对应文档 https://ffmpeg.org/ffmpeg-filters.html#rotate

原地旋转
#

实现命令

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex "\
[0:v]rotate=PI/6*t:c=none:ow=1280:oh=720, \
scale=1280:720, \
format=yuva420p[video] \
" \
-preset ultrafast \
-map "[video]" \
-y output.mp4

rotate=PI/6*t:

  • 以每秒 PI/6 弧度(即 30 度)旋转图像。t 表示时间。

ow=1280:oh=720:

  • 输出视频的宽度和高度分别设置为 1280 和 720 像素。

scale=1280:720:

  • 将输出视频的分辨率设置为 1280x720 像素。

我们可以发现,出现了残影般的轨迹。因为 c=none 是不绘制背景,所以我们没有重新绘制因为运动产生的这些白色轨迹, 参考 https://superuser.com/questions/1393105/rotate-jpeg-in-ffmpeg-with-transparent-color

解法是 black@0

实现命令

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex "\
[0:v]rotate=PI/6*t:c=black@0:ow=1280:oh=720, \
scale=1280:720, \
format=yuva420p[video] \
" \
-preset ultrafast \
-map "[video]" \
-y output.mp4

平移加旋转
#

实现命令

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex "\
color=green:s=1280x720, fps=fps=60[bg]; \
[0:v]rotate=PI/6*t:c=black@0:ow=1280:oh=720,scale=1280:720[rotate]; \
[bg][rotate]overlay=x='t*128':y='t*72':shortest=1[video] \
" \
-preset ultrafast \
-map '[video]' \
-y output.mp4

我们简单的将上面的两步组合到了一起,但是产生了一个问题。我们第一帧,图片并不在左上角。因为我们的旋转部分是在一个 1280x720 的区间进行的,所以 overlay 叠加的时候便在中间了

但是如果我们保持原始图片的大小作为生成视频大小进行旋转

实现命令

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex "\
[0:v]pad=ceil(iw/2)*2:ceil(ih/2)*2[pad];
[pad]rotate=PI/6*t:c=black@0,format=yuva420p[video]; \
" \
-preset ultrafast \
-map "[video]" \
-y output.mp4

我们可以发现,图片的一部分在视频的外面了。因为一个圆周的运动对于矩形图片来说,直径应该是对角线。我们在 FFmpeg 中无法自定义圆心,只能通过视频的大小来让 FFmpeg 决定,所以这里推导参数的时候就比较麻烦

我们需要减掉宽和高的一部分,比如

实现命令

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex "\
[0:v]pad=ceil(iw/2)*2:ceil(ih/2)*2[pad];
[pad]rotate=PI/6*t:c=black@0:ow='hypot(iw,ih)':oh=ow,format=yuva420p[rotate]; \
color=green:s=1280x720, fps=fps=60[bg]; \
[bg][rotate]overlay=x='t*128-(632/2)+290':y='t*72-(632/2)+125':shortest=1[video] \
" \
-preset ultrafast \
-map '[video]' \
-y output.mp4

632 这里是 hypot(iw,ih) 的值

缩放
#

对应文档参考 https://ffmpeg.org/ffmpeg-filters.html#zoompan

中心放大
#

实现命令

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex "\
[0:v]scale=1280:720, \
zoompan=z='zoom+0.001':x=iw/2-(iw/zoom/2):y=ih/2-(ih/zoom/2):d=10*60:s=1280x720:fps=60, \
format=yuva420p[video]\
" \
-t 10 \
-preset ultrafast \
-map "[video]" \
-y output.mp4

z='zoom+0.001':

  • 每一帧的缩放比例增加 0.001,实现逐渐放大的效果。

x=iw/2-(iw/zoom/2)y=ih/2-(ih/zoom/2):

  • 设置每一帧的平移中心为输入图像的中心。

直接这样会会产生抖动,解决方法参考 https://superuser.com/questions/1112617/ffmpeg-smooth-zoompan-with-no-jiggle

左上顶点 Zoom
#

实现命令

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex "\
[0:v]scale=1280:720, \
zoompan=z='pzoom+0.003':d=1:s=1280x720:fps=60, \
format=yuva420p[video]\
" \
-t 10 \
-preset ultrafast \
-map "[video]" \
-y output.mp4

平移旋转放大
#

实现命令

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex "\
[0:v]pad=ceil(iw/2)*2:ceil(ih/2)*2[pad];
[pad]rotate=PI/6*t:c=black@0:ow='hypot(iw,ih)':oh=ow,format=yuva420p[rotate]; \
color=green:s=1280x720, fps=fps=60[bg]; \
[bg][rotate]overlay=x='t*128-(632/2)+290':y='t*72-(632/2)+125':shortest=1[v1]; \
[v1]scale=3840:2160, \
zoompan=z='pzoom+0.001':x=iw/2-(iw/zoom/2):y=ih/2-(ih/zoom/2):d=1:s=1280x720:fps=60, \
format=yuva420p[video]\
" \
-t 10 \
-preset ultrafast \
-map "[video]" \
-y output.mp4

淡入淡出
#

实现命令如下

ffmpeg -r 60 -loop 1 -t 10 -i input.png \
-filter_complex "\
[0:v]pad=ceil(iw/2)*2:ceil(ih/2)*2[pad]; \
[pad]rotate=PI/6*t:c=black@0:ow='hypot(iw,ih)':oh=ow,format=yuva420p[rotate]; \
color=green:s=1280x720[bg]; \
[bg][rotate]overlay=x='t*128-(632/2)+290':y='t*72-(632/2)+125':shortest=1[v1]; \
[v1]scale=3840:2160, \
zoompan=z='pzoom+0.001':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=1:s=1280x720:fps=60[v2]; \
[v2]fade=in:st=0:d=2[video] \
" \
-t 10 \
-preset ultrafast \
-map "[video]" \
-y output.mp4

这边的问题点在于 zoompan 调节的是视角,视角拉近就是放大,参数需要推导一下。

结论
#

如果你现在的需求是临时糊一个视频关键帧的功能,比如前端 canvas 预览,后端渲染生成视频。那么直接使用 FFmpeg 的命令行来做这种比较麻烦。在 FFmpeg 中,我们无法直接通过描述起始和终止两个帧,然后自动补上中间的动画,需要推理中间的过程。另一种暴力的方法是采用 Video Rendering with Node.js and FFmpeg 中的方式,前端后端统一一套 canvas 的标准,后端直接通过图片拼接成视频