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

geq 让你逐像素写公式,lum/cb/cr 管 YUV,r/g/b 管 RGB。 两个坑:没有 val,超 255 不回夹会回绕。

📝 本节和前面几节的关系

前面所有滤镜都是「给参数」,geq 是「给公式」。 它是本章最难的一个,也是能做「任意逐像素效果」的那一个。

一、它跟别的滤镜不一样

前面的滤镜都是「给个参数,它按规则改」。geq 不一样:它让你写表达式, 每个像素的新值由你算——geq 是 generic equation 的缩写,通用方程。

代价是慢。别的滤镜内部是编译好的循环,geq 要对每个像素解释一遍表达式。 160×90 无所谓,1080p 一帧两百万个像素,慢得很明显。

所以 geq 适合「一次算完就存下来」的场景,不适合放在直播链路上。 要实时做逐像素效果,得用 GLSL 或者自己写 C。

二、两套参数,别混

输入是 YUV 就写三个平面,输入是 RGB 就写四个通道:

参数 什么时候用 写什么


lum 输入是 YUV 亮度平面的表达式 cb / cr 输入是 YUV 两个色度平面 r / g / b 输入是 RGB 三个色彩通道 a 输入带 alpha 透明度通道

写 lum 就只改亮度、色度原样,这是最省的做法; 写 r/g/b 就整个换到 RGB 域算,两套不能混在一个滤镜里。

往 yuv420p 的输入上写 r= 也不会报错,它会先转 RGB 再算、再转回来。 结果能看,但两头各转一次,颜色会有轻微偏移,速度也慢一截。

三、第一个坑:没有 val

lut 与 lutyuv 里「当前像素」叫 val,geq 里不叫这个:

写法 结果


lum='255-val' returned 22 lum='255-p(X,Y)' 能跑

val 那一行直接报 Invalid argument returned 22, 错误信息里不会告诉你「没有这个变量」,只说参数不合法。 这种错最难查,因为语法看着完全正常。

geq 里当前像素是 p(X,Y)——一个函数,要带坐标。 同理,取别的平面要用 lum(X,Y)、cb(X,Y)、cr(X,Y)。

记住这个区别的办法:lut 是「查表」,表里只有当前值; geq 是「算公式」,公式能拿坐标,所以当前值也要用坐标去取。

四、第二个坑:超 255 会回绕

想提亮就乘个系数,看着很自然,结果却反常:

写法 画面均值 比原样


lum='p(X,Y)' 141.9 100% lum='p(X,Y)1.5' 126.4 89% lum='min(255,p(X,Y)1.5)' 184.1 130% lum='p(X,Y)2' 84.7 60% lum='min(255,p(X,Y)2)' 193.2 136%

乘 1.5 之后均值从 141.9 掉到 126.4,乘 2 掉到 84.7——越乘越暗,这不对。 原因是它不做截断:亮部乘完超过 255 之后从 0 重新数,255 变回 0 附近。

所以亮点全变成暗点,画面反而更暗。加上 min(255,...) 才正常,均值升到 184。 这一条对 geq 的所有表达式都成立:凡是有可能超范围的,都得自己夹一下。

反过来,低于 0 也一样会绕到 255 附近——做浮雕那种「有正有负」的运算时, 必须加偏置(比如 +128)把结果抬进合法范围,否则一半像素会变成刺眼的白点。

五、动手 Demo:val 报错、四档亮度回绕

下面这段代码先试 val 与 p(X,Y) 两种写法,看哪个报错; 再用四档亮度系数对比「加不加 min 夹」的差别。

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")

print(u"--- 没有 val,只有 p(X,Y)")
for e in (u"lum='255-val'", u"lum='255-p(X,Y)'"):
    try:
        geq(e)
        print(u"  %-22s 能跑" % e)
    except Exception as ex:
        msg = str(ex)
        print(u"  %-22s %s" % (e, u"Invalid argument returned 22"
                                if u"returned 22" in msg else msg))

print(u"--- 超 255 是回绕,不是截断")
base = geq(u"lum='p(X,Y)'").mean()
print(u"  原样 %.1f" % base)
for e in (u"lum='p(X,Y)*1.5'", u"lum='min(255,p(X,Y)*1.5)'",
          u"lum='p(X,Y)*2'", u"lum='min(255,p(X,Y)*2)'"):
    v = geq(e).mean()
    print(u"  %-30s 均值 %6.1f  是原样 %.0f%%" % (e, v, v / base * 100))

实跑输出:

--------------------------------------------------------------------
7.15.1 geq 滤镜参数简介
--------------------------------------------------------------------

**① geq 是「逐像素写公式」**

**前面的滤镜都是「给个参数,它按规则改」。`geq` 不一样:它让你写表达式,**
**每个像素的新值由你算——`geq` 是 generic equation 的缩写,通用方程。**

**代价是慢。别的滤镜内部是编译好的循环,geq 要对每个像素解释一遍表达式。**
**160×90 无所谓,1080p 一帧两百万个像素,慢得很明显。**

**② 参数:YUV 三件套或 RGB 四件套**

  参数          什么时候用       写什么
  ---------------------------------------------------
  lum           输入是 YUV       亮度平面的表达式
  cb / cr       输入是 YUV       两个色度平面
  r / g / b     输入是 RGB       三个色彩通道
  a             输入带 alpha     透明度通道

**写 `lum` 就只改亮度、色度原样;写 `r/g/b` 就整个换成 RGB 域算。**
**两套不能混:往 yuv420p 的输入上写 `r=`,它会先转 RGB 再算,慢一截。**

**③ 第一个坑:geq 里没有 `val`**

**`lut` 与 `lutyuv` 里「当前像素」叫 `val`,`geq` 里不叫这个:**

  写法                 结果
  -----------------------------------------------------
  lum='255-val'        Invalid argument returned 22
  lum='255-p(X,Y)'     能跑

**`val` 那一行直接报 `returned 22`,错误信息里不会告诉你「没有这个变量」。**
**geq 里当前像素是 `p(X,Y)`——一个函数,要带坐标。**

**④ 第二个坑:超 255 不回夹,会回绕**

**想提亮就乘个系数,看着很自然,结果却反常:**

  写法                          画面均值     比原样
  -----------------------------------------------------
  lum='p(X,Y)'                  141.9        100%
  lum='p(X,Y)*1.5'              126.4        89%
  lum='min(255,p(X,Y)*1.5)'     184.1        130%
  lum='p(X,Y)*2'                84.7         60%
  lum='min(255,p(X,Y)*2)'       193.2        136%

**乘 1.5 之后均值从 141.9 掉到 126.4,乘 2 掉到 84.7——越乘越暗,这不对。**

**原因是它不做截断:亮部乘完超过 255 之后从 0 重新数,255 变回 0 附近。**
**所以亮点全变成暗点,画面反而更暗。加上 `min(255,...)` 才正常,均值升到 184。**

**这一条对 geq 的所有表达式都成立:**
**凡是有可能超过 255 或者低于 0 的,都得自己用 `min`、`max` 夹一下。**
**geq 只负责算,不负责把结果塞进 0~255。**

前两行是那个坑的现场:val 报 Invalid argument returned 22, 换成 p(X,Y) 就通了。报错信息里完全看不出是变量名的问题。

后四行是回绕的现场:乘 1.5 是 126.4、乘 2 是 84.7,都比原样 141.9 低。 加上 min(255,...) 之后才变成 184.1 与 193.2,这才叫「提亮」。

这段代码把异常捕获成一句话,是因为 PyAV 抛出的异常串很长 (带上表达式原文),直接把 str(ex) 打出来会把表格撑爆。

30 秒实验

  1. 改系数:把 *1.5 改成 *0.9,看还会不会回绕。
  2. 试负值:写 lum='p(X,Y)-100',看暗部是不是绕成亮点。
  3. 写 cb:只写 cb='128',看画面是不是变成一片灰。

常见误区

  1. 在 geq 里写 val。geq 没有这个变量,当前像素要写 p(X,Y)。
  2. 以为 geq 会自动截断。超 255 会回绕,越乘越暗。
  3. 把 yuv 的输入按 r/g/b 算。会两头各转一次色,颜色偏、速度慢。
自测三问(点开对答案) 1. geq 里「当前像素」怎么写? —— `p(X,Y)`。`val` 是 `lut`、`lutyuv` 才有的变量。 2. `p(X,Y)*2` 为什么会让画面变暗? —— geq 不做截断,超过 255 的值回绕到 0 附近。 3. 怎么让亮度乘完不溢出? —— 用 `min(255,p(X,Y)*系数)` 自己夹住。

相关笔记