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.5改成*0.9,看还会不会回绕。 - 试负值:写
lum='p(X,Y)-100',看暗部是不是绕成亮点。 - 写 cb:只写
cb='128',看画面是不是变成一片灰。
常见误区
- 在 geq 里写
val。geq 没有这个变量,当前像素要写p(X,Y)。 - 以为 geq 会自动截断。超 255 会回绕,越乘越暗。
- 把 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)*系数)` 自己夹住。- ☐ (本节完,下一节 7.15.2 geq 滤镜的官网介绍)