V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
jifengg
V2EX  ›  FFmpeg

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

  •  
  •   jifengg · 171 天前 · 1435 次点击
    这是一个创建于 171 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    前前言

    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。如果你理解错了,会发现自定义效果完全没效。
    因为,它表示的是过渡效果的进度,而且,重要的是,它是个递减的数。

    • 过渡动画开始的时候,P=1.0 ;
    • 过渡动画结束的时候,P=0.0 ;
    • 它的值是按帧线性递减的,例如,duration=4 ,fps=25 ,那么第二帧的时候,P=1.0-1/(4*25)=0.99 ;
    • 可以通过数学函数来改变 P 的“线性”,例如 P*P*(3-2P),(Smoothstep函数图)。
      • 注意,P 是从 1.0 到 0.0 ,因此查看函数图的时候要注意从右往左看。
      • 如果你觉得从右往左看不直观,把所有 P 都改成(1-P)吧。
      • win11 自带的计算器有一个“绘图”功能,能够很好的显示各种数学函数的图形,可以用来辅助理解。

    理解 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):
    wipeleft

    分析一下,分割线在画面水平线上的位置 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 要返回什么内容了。我们接着第三个例子。 如果我们希望实现的是一个从右往左推走的效果:
    slideleft

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

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

    image

    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

    第 1 条附言  ·  166 天前
    在 GitHub 上更新了自定义动画的预览,可以点击查看: https://github.com/jifengg/ffmpeg-script/blob/main/docs/ffmpeg.img2video.custom.transitions.md
    3 条回复    2024-07-03 21:55:57 +08:00
    shinession
        1
    shinession  
       171 天前
    支持一下
    Arrowing
        2
    Arrowing  
       171 天前
    之前也研究过这个,用底层方法太蛋疼了,发现有现成的,有 node 包。
    https://gl-transitions.com/gallery
    ixinshang
        3
    ixinshang  
       170 天前
    不错 不错。 感谢分享
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2538 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 11:52 · PVG 19:52 · LAX 03:52 · JFK 06:52
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.