🎬 第七章 FFmpeg命令行实现音视频特效及复杂滤镜应用
📄 一句话概括

geq 的表达式能用坐标、帧号、时间,还有 abs/min/max/sin/if 这些函数。 浮雕那类「有正有负」的运算,必须加 128 的偏置。

📝 本节和前面几节的关系

上一节讲了参数与两个坑,这一节把官网那张变量表、函数表翻成能用的东西。 六个例子都能跑通,数字都是量出来的。

一、变量表

官网文档列了一整页,常用的就这几个:

变量 含义


X / Y 当前像素的横、纵坐标 W / H 画面的宽、高 N 当前是第几帧,从 0 开始 T 当前时间(秒) SW / SH 表达式的求值步长,一般不用 p(x,y) 取某坐标的像素值(当前平面) lum/cb/cr(x,y) 取某坐标指定平面的值

X 与 Y 大写,N 是帧号、T 是秒——这套命名跟 crop、overlay 的表达式统一。 所以写过 crop 表达式的人,geq 上手很快。

SW、SH 是「求值步长」,意思是「隔几个像素算一次」。 默认 1,也就是每个像素都算;调大一点算得快,但结果会出现块状。

二、函数表

这几个函数凑起来,几乎能做任何逐像素效果:

函数 作用


abs(x) 绝对值 min / max 取小、取大 sin / cos 三角函数,用来做周期变化 if(cond,a,b) 三目:条件成立取 a,否则取 b lt(a,b) / gt(a,b) 小于、大于,返回真假

再复杂的就写不下去了——geq 的表达式不是编程语言,没有循环、没有变量赋值。 想做复杂的事,只能把公式堆得更长,或者拆成多个 geq 串起来。

if 和 lt 一起用就能做「局部处理」: if(lt(X,80),p(X,Y),255-p(X,Y)) 的意思是「左半边原样、右半边反色」。 实测均值 143.9,介于原样 141.9 与全反色 114.8 之间,符合「只反一半」的预期。

三、六个能跑的表达式

把官网的例子实际跑一遍,看均值落在哪:

效果 表达式 均值


反色 lum='255-p(X,Y)' 114.8 压暗 lum='p(X,Y)0.6' 59.4 轮廓 lum='abs(p(X,Y)-p(X+1,Y))' 50.3 浮雕 lum='p(X,Y)-p(X-1,Y-1)+128':cb='128':cr='128' 130.6 左半反色 lum='if(lt(X,80),p(X,Y),255-p(X,Y))' 143.9 横向渐变 lum='p(X,Y)(X/W)' 65.7

原样是 141.9。反色落到 114.8(差不多是 255 减原值),压暗落到 59.4, 轮廓落到 50.3(相邻差本来就小),浮雕回到 130.6 附近。

浮雕那一条把 cb 与 cr 都钉成 128,画面就是纯灰的—— 色度清零等于去色,跟 7.12 的 hue=s=0 一个道理。

横向渐变那条 p(X,Y)*(X/W) 是「左边暗、右边亮」: X/W 从 0 走到接近 1,相当于给画面乘一个从左到右的斜坡。

四、浮雕为什么以 128 为底

p(X,Y)-p(X-1,Y-1) 是两个相邻像素的差,有正有负。 直接存会一半变黑(负数回绕成 0 附近),画面就废了。

加 128 就是把这个差抬到中间:差为 0 的地方是灰的, 差大的地方偏亮或偏暗,看起来像浮雕的凸起与凹陷。

所以「+128」不是装饰,是必需的偏置。

同理做边缘检测时,加 128 得到「灰底白线」,加 abs 得到「黑底白线」。 实测 abs 那一版均值只有 50.3——负的一半被折成正的,整体偏暗。

两种写法各有用途:要叠在原画面上做「线条特效」用 abs(黑底好叠), 要单独看边缘图用「+128」(灰底更接近人眼习惯)。

五、动手 Demo:六个表达式、三个极值

下面这段代码把六个表达式各跑一遍,量均值与极值。 极值那一列能看出像素有没有被压到 0 或顶到 255。

import av
from fractions import Fraction

TB = Fraction(1, 25)
c0 = av.open(u"s7.mp4")
BASE = next(c0.decode(video=0)).to_ndarray(format=u"yuv420p")
c0.close()

def geq(expr):
    g = av.filter.Graph()
    b = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
    f = g.add(u"geq", expr)
    sk = g.add(u"buffersink")
    b.link_to(f)
    f.link_to(sk)
    g.configure()
    vf = av.VideoFrame.from_ndarray(BASE, format=u"yuv420p")
    vf.pts = 0
    vf.time_base = TB
    b.push(vf)
    return sk.pull().to_ndarray(format=u"rgb24")

CASES = ((u"反色", u"lum='255-p(X,Y)'"),
         (u"压暗", u"lum='p(X,Y)*0.6'"),
         (u"轮廓", u"lum='abs(p(X,Y)-p(X+1,Y))'"),
         (u"浮雕", u"lum='p(X,Y)-p(X-1,Y-1)+128':cb='128':cr='128'"),
         (u"左半反色", u"lum='if(lt(X,80),p(X,Y),255-p(X,Y))'"),
         (u"横向渐变", u"lum='p(X,Y)*(X/W)'"))

print(u"--- 原样 %.1f" % geq(u"lum='p(X,Y)'").mean())
for tag, e in CASES:
    o = geq(e)
    print(u"  %-10s 均值 %6.1f 最小 %3d 最大 %3d"
          % (tag, o.mean(), o.min(), o.max()))

实跑输出:

--------------------------------------------------------------------
7.15.2 geq 滤镜的官网介绍
--------------------------------------------------------------------

**① 表达式里能用的变量**

**官网文档列了一整页,常用的就这几个:**

  变量               含义
  ---------------------------------------------------
  X / Y              当前像素的横、纵坐标
  W / H              画面的宽、高
  N                  当前是第几帧,从 0 开始
  T                  当前时间(秒)
  SW / SH            表达式的求值步长,一般不用
  p(x,y)             取某坐标的像素值(当前平面)
  lum/cb/cr(x,y)     取某坐标指定平面的值

**`X` 与 `Y` 大写,`N` 是帧号、`T` 是秒——这套命名跟 crop、overlay 的表达式是统一的。**
**所以写过 crop 的表达式,geq 上手很快。**

**② 常用函数**

  函数                  作用
  ------------------------------------------------------
  abs(x)                绝对值
  min / max             取小、取大
  sin / cos             三角函数,用来做周期变化
  if(cond,a,b)          三目:条件成立取 a,否则取 b
  lt(a,b) / gt(a,b)     小于、大于,返回真假

**这几个函数凑起来,几乎能做任何逐像素效果:反色、浮雕、渐变、闪烁、局部处理。**
**再复杂的就写不下去了——geq 的表达式不是编程语言,没有循环。**

**③ 六个能跑的表达式,实测**

  效果         表达式                                            均值
  ------------------------------------------------------------------------
  反色         lum='255-p(X,Y)'                                  114.8
  压暗         lum='p(X,Y)*0.6'                                  59.4
  轮廓         lum='abs(p(X,Y)-p(X+1,Y))'                        50.3
  浮雕         lum='p(X,Y)-p(X-1,Y-1)+128':cb='128':cr='128'     130.6
  左半反色     lum='if(lt(X,80),p(X,Y),255-p(X,Y))'              143.9
  横向渐变     lum='p(X,Y)*(X/W)'                                65.7

**原样是 141.9。反色落到 114.8(差不多是 255 减原值),压暗落到 59.4,**
**轮廓落到 50.3(相邻差本来就小),浮雕回到 130.6 附近(以 128 为底)。**

**浮雕那一条把 `cb` 与 `cr` 都钉成 128,画面就是纯灰的——**
**色度清零等于去色,这跟 7.12 的 `hue=s=0` 是一个道理。**

**④ 浮雕为什么以 128 为底**

**`p(X,Y)-p(X-1,Y-1)` 是两个相邻像素的差,有正有负。**
**直接存会一半变黑(负数回绕),加 128 就是把它抬到中间:**
**差为 0 的地方是灰的,差大的地方偏亮或偏暗,看起来像浮雕。**

**所以「+128」不是装饰,是必需的偏置。**
**同理,做「边缘检测」时也常加 128,或者加完再用 `abs` 折回来——**
**加 `abs` 的那一版(第三种写法)均值只有 50.3,因为负的一半被折成正值、整体偏暗。**

六行数跟本节那张表完全对上:反色 114.8、压暗 59.4、轮廓 50.3、浮雕 130.6。

极值那一列全是 0 到 255——说明这六种写法都没溢出。 要是某种写法出现「最大 255 且均值很低」,那多半是回绕了。

浮雕 那一行是唯一同时改了三个平面的:lum 算差值、cb/cr 固定 128。 少写 cb/cr,出来的浮雕会带颜色,不好看。

30 秒实验

  1. 改阈值:把 lt(X,80) 改成 lt(X,40),看反色的区域缩到多大。
  2. 用 Y 做判断:写 if(lt(Y,45),...),做成上下分屏的效果。
  3. 按帧变:把 X/W 换成 (N%25)/25.0,看是不是每 25 帧循环一次渐变。

常见误区

  1. 浮雕不加 128。负的差会回绕成白点,画面一半是刺眼的噪声。
  2. 拿 abs 版当「更好的边缘」。它是黑底白线,叠在原画面上会发黑。
  3. 指望 geq 写复杂逻辑。没有循环没有变量,堆长了就没人看得懂。
自测三问(点开对答案) 1. geq 里「第几帧」用哪个变量? —— `N`。时间用 `T`(秒)。 2. 浮雕为什么加 128? —— 相邻差有正有负,加 128 把结果抬进 0~255 的合法范围。 3. `abs` 版轮廓与「+128」版有什么区别? —— `abs` 是黑底白线,+128 是灰底、有亮有暗。

相关笔记