ffmpeg 过滤器 xfade 自定义动画的研究

171 天前
 jifengg

本文无任何推广,请放心阅读。
本文无任何推广,请放心阅读。
本文无任何推广,请放心阅读。

前前言

hello ,兄弟们,我又来分享我的 ffmpeg 脚本啦。
上次分享了一个使用 ffmpeg ,将多张图片转换成类似幻灯片的视频,支持多种转场效果,说过在研究自定义效果,目前有点小成果,于是有了本文。

本次没有新增脚本,而是增强了ffmpeg.img2video.js,预置了一些自定义效果,并支持自己添加效果。

以下的都是本次的技术分享。如果你不感兴趣,可以直接到 GitHub 更新脚本。

开源地址

GitHub 地址:https://github.com/jifengg/ffmpeg-script

前言

使用xfade过滤器做视频转场切换效果,本身 ffmpeg 已经提供了 56 种效果,能满足大部分需求。不过,更复杂的过渡效果(例如翻页)还没有。
根据文档,使用 transition=custom+expr ,可以实现自定义的效果。但是,官方文档并没有对expr如何编写做详细说明,也没有 google 到。
因此,对其进行了一番研究,尝试实现了几种效果。简单做一个使用教程,希望能够帮助到有需要的人。

效果预览(点击查看视频,视频均小于 1MB )

水滴

https://github.com/jifengg/ffmpeg-script/assets/17020523/b3cec5b1-d747-46bd-aae1-924289aaddce

百叶窗

https://github.com/jifengg/ffmpeg-script/assets/17020523/1bef9ae3-41c3-4747-ae41-9056ae4e6892

简易翻页

https://github.com/jifengg/ffmpeg-script/assets/17020523/30c810a1-7522-4829-8450-4602c8203853

ffmpeg 官方 wiki

https://trac.ffmpeg.org/wiki/Xfade

ffmpeg 官方文档翻译

以下翻译自FFmpeg xfade 官方文档

xfade

将淡入淡出从一个输入视频流应用到另一个输入视频流。淡入淡出将持续指定的时间。
两个输入必须是恒定帧速率,并且具有相同的分辨率、像素格式、帧速率和时间基准。

该过滤器接受以下选项:

transition
    'custom'
    [忽略]

duration
    设置交叉淡入淡出持续时间(以秒为单位)。范围为 0 至 60 秒。默认持续时间为 1 秒。

offset
    设置相对于第一个输入流的交叉淡入淡出开始时间(以秒为单位)。默认偏移量为 0 。

expr
    设置自定义过渡效果的表达式。
    表达式可以使用以下变量和函数:

    X
    Y
        当前样本的坐标。

    W
    H
        图像的宽度和高度。

    P
        过渡效果的进展。
         [译注] 过渡开始时,P=1.0 ,过渡结束时,P=0.0 。

    PLANE
        目前正在处理的平面。
         [译注] 这里的平面,其实就是指像素格式的分量。
         [译注] 取值范围由输入流的像素格式 pix_fmt 决定,如 yuv420p ,则取值范围是 0 ,1 ,2 ;如 rgba ,则取值范围是 0 ,1 ,2 ,3 。

    A
        返回第一个输入流在当前位置和平面的值。

    B
        返回第二个输入流在当前位置和平面的值。

    a0(x,y)
    a1(x,y)
    a2(x,y)
    a3(x,y)
        返回第一个输入的第一/第二/第三/第四个分量的 位置 (x,y) 处的像素的值。
         [译注] 例如,像素格式是 yuv420p ,a0 返回的是 Y 分量。a1 返回的是 U 分量。a2 返回的是 V 分量。没有 a3

    b0(x,y)
    b1(x,y)
    b2(x,y)
    b3(x,y)
        返回第二个输入的第一/第二/第三/第四个分量的 位置 (x,y) 处的像素的值。

理解 P

一般来说,ffmpeg 中支持时间轴编辑的过滤器,都有tn参数可以用在表达式中,其中t表示时间秒,n表示帧数。
但是 xfade 里却是用的 P ,它不是tn。如果你理解错了,会发现自定义效果完全没效。
因为,它表示的是过渡效果的进度,而且,重要的是,它是个递减的数。

理解 X,Y,W,H

X,Y 表示坐标,是指“当前正在计算表达式的像素的坐标”,按照我们要实现的效果,决定该像素对应的颜色码。

W,H 是图像的宽高,这个在整个渐变过程是保持不变的。

理解 PLANE,A,B,a0(x,y),...,b0(x,y),...

a0(x,y)表示第一个视频坐标 x,y 处的像素的第一个分量值。 PLANE 表示当前是计算的第几个分量值。 A 是一个简写,当 PLANE=0 时,A=a0(X,Y); PLANE=1 时,A=a1(X,Y); PLANE=2 时,A=a2(X,Y);以此类推。 b 和 B 同 a 和 A 。

注意,无法通过类似a(plane,x,y)的方法来获得指定坐标指定分量的值,因此在像素有位移的时候,表达式会比较长。如if(eq(PLANE,0),a0(X,Y),if(eq(PLANE,1),a1(X,Y),if(eq(PLANE,2),a2(X,Y),0)))

理解 expr

xfadeexpr,返回一个值,但是这个值是什么含义呢,一般人看文档很难理解。
300x200 的输入源为例,假设其像素格式是 yuv420p ,则其分量个数是 3 。( ffmpeg 支持的像素格式及格式信息,可以通过ffmpeg -pix_fmts查看)。 像素点是60000个,每一帧的像素分量总数就是60000*3=18 万个。
那么,过渡开始的第一帧,ffmpeg 会遍历每个像素点的每个分量,分别调用expr,并设置 X,Y,PLANE 等值。总共调用18 万次获得对应的值,来完成第一帧的渲染。 如果我们希望每一帧就是显示第一个视频的画面,那么可以写expr=A即可。A表示的就是第一个视频当前像素当前分量的值。

尝试 1 ,实现渐隐渐显效果

如果我们希望实现第一个视频渐渐变透明,第二个视频由透明渐渐显现,类似xfade默认的效果fade,那么可以写expr='A*P+B*(1-P)'
因为 P 是从 1.0 线性变成 0.0 的。所以一开始 P=1 ,表达式计算结果=A,看到的就是只有第一个视频画面,到一半时,P=0.5 ,结果=0.5A+0.5B,画面就是两个视频分别半透明叠加在一起。最后 P=0.0 时,结果=B,就只剩下第二个视频的画面了。

尝试 2 ,实现擦除效果

同样的,如果我们希望实现一个从右往左擦除的效果(图片引用自https://trac.ffmpeg.org/wiki/Xfade):

分析一下,分割线在画面水平线上的位置 X ,除以宽度 W ,其实就是等于 P ,于是,我们可以让分割线左边的显示画面 A ,右边的显示画面 B 。 expr='if(lt(X/W,P),A,B)':当X/W<P的时候,说明 X 在分割线左边,于是显示 A ,否则显示 B 。

分割线上显示 A 还是 B ,影响不大。这里是显示了 B ,如果要显示 A ,可以用lte代替lt

尝试 3 ,实现推走效果

从上面两个例子你大概能理解 expr 要返回什么内容了。我们接着第三个例子。 如果我们希望实现的是一个从右往左推走的效果:

你会发现,变得更复杂了。你可以先暂停试试自己能否写出来。

为什么更复杂了?以坐标(0,0)为例,他显示的像素时刻都在变化(因为画面在往左移动)。
例如,在 P=0.8 的时候,它(0,0)应该是视频 A X=W*0.2,Y=0 坐标处的像素值。(这里需要好好理解,参考下图帮忙理解)

X/W>P的地方,应该显示视频 B 的画面,其坐标转换关系是(X-P*W,Y)。
注意,此时你没法再用值AB了,因为它们是坐标(X,Y)的分量,而我们要在(X,Y)处显示别的坐标的像素,这个我们在上面理解 PLANE,A,B,a0(x,y),...,b0(x,y),...的地方说过了。

那么这个表达式要怎么写呢?

expr='if(lt(X/W,P),^
if(eq(PLANE,0),a0(X+(1-P)*W,Y),^
if(eq(PLANE,1),a1(X+(1-P)*W,Y),^
if(eq(PLANE,2),a2(X+(1-P)*W,Y),0)))^
,^
if(eq(PLANE,0),b0(X-P*W,Y),^
if(eq(PLANE,1),b1(X-P*W,Y),^
if(eq(PLANE,2),b2(X-P*W,Y),0)))^
)'

我测试的时候用的是 windows 的 bat 脚本,为了方便理解和修改,用^进行了换行。注意不要有空格,否则会报错。
测试的时候用的是 yuv420p 像素格式,因此表达式没有用到 a3 ,如果是用了 4 个分量的像素格式需要把 a3 按照上面的格式加进去。

其中,分割线左边显示视频 A 的画面,且 x 坐标左移了(1-P)*W 个像素,因此其 x 坐标表达式是X+(1-P)*W
右边显示视频 B 的画面,且 x 坐标右移到了分割线右边,因此其 x 坐标表达式是X-P*W
因为是水平移动,所以 y 坐标保持Y即可。

于是,随着 P 从 1.0 渐变到 0.0 ,视频 A 就像被视频 B 从右边推到了左边,完成了一个过渡效果。

小结

现在,你已经了解了 expr 要怎么编写来实现过渡效果了。我还实现了一些其它效果,包括示例里的,你可以在 GitHub 上查看

性能

在 windows 下创建 2 个 bat 文件,分别输入测试命令:

@echo off
@REM 使用 custom 实现 slideleft 效果
ffmpeg -y -hide_banner ^
-f lavfi -i "pal100bars=r=1/1000" ^
-f lavfi -i "colorchart=r=1/1000" ^
-filter_complex ^
[0:v]format=yuv420p,scale=960:480,fps=25,trim=duration=40[v1];^
[1:v]format=yuv420p,scale=960:480,fps=25,trim=duration=40.04[v2];^
[v1][v2]xfade=duration=40:offset=0:transition=custom:^
expr='if(lt(X/W,P),^
if(eq(PLANE,0),a0(X+(1-P)*W,Y),^
if(eq(PLANE,1),a1(X+(1-P)*W,Y),^
if(eq(PLANE,2),a2(X+(1-P)*W,Y),0)))^
,^
if(eq(PLANE,0),b0(X-P*W,Y),^
if(eq(PLANE,1),b1(X-P*W,Y),^
if(eq(PLANE,2),b2(X-P*W,Y),0)))^
)' ^
-crf 23 -c:v h264 -pix_fmt yuv420p -movflags +faststart -r 25 -aspect 960:480 ^
out1.mp4
@echo off
@REM 使用内置的 slideleft 效果
ffmpeg -y -hide_banner ^
-f lavfi -i "pal100bars=r=1/1000" ^
-f lavfi -i "colorchart=r=1/1000" ^
-filter_complex ^
[0:v]format=yuv420p,scale=960:480,fps=25,trim=duration=40[v1];^
[1:v]format=yuv420p,scale=960:480,fps=25,trim=duration=40.04[v2];^
[v1][v2]xfade=duration=40:offset=0:transition=slideleft ^
-crf 23 -c:v h264 -pix_fmt yuv420p -movflags +faststart -r 25 -aspect 960:480 ^
out2.mp4

这里使用的动画时长是 40 秒,可以自行修改成 0~60 秒。
在我电脑上运行,耗时分别是:自定义17.514 秒,内置1.605 秒
可以看出,使用自定义的效果,远比内置效果更耗时。原因我们在“理解 expr”有提过,因为每一帧需要调用 expr 次数=960×480×3=1,382,400 。一百多万次。而且是纯 CPU 运算,因此效率自然底下。

好在一般的过场时长是 3 、4 秒左右,影响还在可接受范围内。

如果你在寻找更高效的自定义效果,可以考虑使用xfade_opencl过滤器,或者自行编译 ffmpeg ,加入gl-transition过滤器。

其它转场过滤器

xfade_opencl

要使用xfade_opencl,需要编译的时候加入--enable-opencl,且运行的机器有支持 opencl 的设备(一般指显卡)。
要查看当前机器有哪些 opencl 的设备,可以运行以下命令:

ffmpeg -v debug -init_hw_device opencl

打印出类似信息:

[AVHWDeviceContext @ 0000027894f28400] 1 OpenCL platforms found.
[AVHWDeviceContext @ 0000027894f28400] 1 OpenCL devices found on platform "NVIDIA CUDA".
[AVHWDeviceContext @ 0000027894f28400] 0.0: NVIDIA CUDA / NVIDIA GeForce RTX *****

其中0.0就是可用的 opencl 设备编号,在 ffmpeg 命令中指定使用该设备:

ffmpeg -y -hide_banner -init_hw_device opencl=ocldev:0.0 -filter_hw_device ocldev ^
-f lavfi -r 25 -t 40 -i "pal100bars" ^
-f lavfi -r 25 -t 40.04 -i "colorchart" ^
-filter_complex ^
[0:v]format=yuv420p,scale=960:480,hwupload[v0];^
[1:v]format=yuv420p,scale=960:480,hwupload[v1];^
[v0][v1]xfade_opencl=duration=40:offset=0:transition=slideleft,hwdownload,format=yuv420p ^
-c:v h264_nvenc -pix_fmt yuv420p -movflags +faststart -r 25 -aspect 960:480 ^
out3.mp4

性能比自定义 xfade 效果好很多,唯一要求就是需要支持 opencl 的设备(一般指显卡)。
且,xfade_opencl也是支持自定义效果的,官方文档
内置的几个效果的源码可以查看 GitHub 上 ffmpeg 的源码:https://github.com/FFmpeg/FFmpeg/blob/master/libavfilter/opencl/xfade.cl

gl-transition

gl-transitions是由开发者 Gilles Lamothe 创建的,它封装了大量的 GPU 加速过渡效果,包括但不限于溶解、推拉、旋转等多种类型。这些过渡效果可以轻松地整合到你的图形应用程序中,无论你是开发游戏、视频编辑软件还是实验性的艺术项目。
它使用 OpenGL 进行加速,因此,也需要支持 OpenGL 的设备(一般指显卡)。
它不是 ffmpeg 专属的,但是可以做为一个过滤器添加到 ffmpeg 中。参考这个 GitHub 项目transitive-bullshit/ffmpeg-gl-transition。 编译后,你将可以使用其官网上的所有效果,当然也可以自己编写自定义的效果。

性能方面,因为我没有自行编译测试,所以无法给出具体数据。

它使用 GLSL 语言编写,如果你看了上面 OpenCL 的部分,你会发现它们有很多共同点。
甚至,我在编写xfade自定义表达式的时候,也参考了它的 GLSL 代码。比如效果预览中的水滴,就是参考了WaterDrop

结语

不知道是 ffmpeg 官方觉得 xfade 的 expr 编写太过容易,还是觉得性能不行不建议使用,反正官方文档及 wiki 都没有示例,也没有提及如何编写。
我自己基本上是自己看着文档猜测、尝试,慢慢的摸索出来一些门道。想着网上没有一个类似的教程,于是变写了这个文章。
如果你发现文章哪里有问题,欢迎指出,大家共同进步。

本文存档:https://github.com/jifengg/ffmpeg-script/blob/main/docs/ffmpeg.xfade.md

1436 次点击
所在节点    FFmpeg
3 条回复
shinession
171 天前
支持一下
Arrowing
171 天前
之前也研究过这个,用底层方法太蛋疼了,发现有现成的,有 node 包。
https://gl-transitions.com/gallery
ixinshang
171 天前
不错 不错。 感谢分享

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1054559

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX