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

去色有三条路:hue=s=0 最快、format=gray 最彻底、colorchannelmixer 最可控。 三条都能出灰,但灰的数值不一样。

📝 本节和前面几节的关系

7.11 动的是时间轴上的亮度,这一节动的是颜色。 去色本身很简单,难的是「灰得对不对」。

一、三条路

三个滤镜都能去色,但它们做的事不一样:

写法 做了什么 输出格式


hue=s=0 饱和度归零 还是 yuv420p format=gray 只留亮度平面 gray colorchannelmixer=… 按权重混三通道 看输入格式

hue=s=0 是最常用的一条:只改饱和度,其它一概不动,格式也不变, 接在别的滤镜后面毫无负担。

format=gray 更彻底——它把两个色度平面直接扔掉,只留亮度。 后面接只吃灰度输入的滤镜时用它,否则会因为多转一道而变慢。

二、实测三通道均值

去色后 R、G、B 三个均值应该相等,量一下就知道成没成:

写法 R G B 三通道相等


原图 145.5 140.2 140.2 不相等 hue=s=0 141.8 141.8 141.8 是 format=gray 142.2 142.2 142.2 是 colorchannelmixer 141.6 141.6 141.6 是

三条都出灰,但灰的数值差零点几:141.8、142.2、141.6。 差别来自算法:前者在 YUV 域把色度清零,中间那个扔掉色度再按 bt601 还原, 后者在 RGB 域按 0.3 / 0.4 / 0.3 的权重混。

0.3 / 0.4 / 0.3 是一组「看着舒服」的经验权重,不是标准的亮度公式。 标准公式是 0.299 / 0.587 / 0.114(bt601),用它出来的黑白更接近「真实明暗」。

三、去色不等于黑白照片

「黑白」在日常语境里包含两层:没有颜色、只剩明暗。 hue=s=0 只做前者:亮度曲线一点没动,原来亮的地方还是那么亮。

想做「老照片」那种质感,得在去色之后再压对比、降亮度、加颗粒:

ffmpeg -i s7.mp4 -vf "hue=s=0,lutyuv=y=val*0.85,noise=alls=12" old.mp4

lutyuv=y=val*0.85 把亮度整体压到 85%,noise=alls=12 加一层颗粒。 这两步合起来就有「胶片感」了,光去色只会得到一段灰扑扑的视频。

再加一步暗角(vignette)就更像:hue=s=0,vignette,lutyuv=y=val*0.85。

四、什么时候用哪条

按需求挑,别一律用 hue=s=0:

想要 用哪条


最快去色、格式不变 hue=s=0 后面接只吃灰度的滤镜 format=gray 要自己定权重(比如偏红) colorchannelmixer

第三条的实际用途是「模拟特定胶片的感光」: 把红的权重调高,天空会变暗、皮肤会变亮,出来的是「红滤镜黑白」的味道。 拍黑白风景时在镜头前加黄镜、橙镜,就是这个原理。

一个容易忽略的点:去色之后就没法再「只调某一个颜色」了。 想保留一点红色(比如只让红色物体有颜色),得用 colorchannelmixer 或者 hue=s=0 之后再拿 selectivecolor 补回来,顺序不能反。

五、动手 Demo:三条路的灰值对比

下面这段代码先量原图的三通道均值,再用三种写法各去一次色, 逐个量 R、G、B 是不是真的相等。

import av
from fractions import Fraction

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

def run(filters, n=50):
    g = av.filter.Graph()
    b = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
    p = b
    for name, args in filters:
        f = g.add(name, args) if args else g.add(name)
        p.link_to(f)
        p = f
    sk = g.add(u"buffersink")
    p.link_to(sk)
    g.configure()
    out = []
    for i in range(n):
        vf = av.VideoFrame.from_ndarray(BASE[i % len(BASE)], format=u"yuv420p")
        vf.pts = i
        vf.time_base = TB
        b.push(vf)
        while True:
            try:
                out.append(sk.pull().to_ndarray(format=u"rgb24"))
            except Exception:
                break
    b.push(None)
    while True:
        try:
            out.append(sk.pull().to_ndarray(format=u"rgb24"))
        except Exception:
            break
    return out

def mean3(a):
    return [a[:, :, i].mean() for i in range(3)]

pure = av.VideoFrame.from_ndarray(BASE[0], format=u"yuv420p")
pure = pure.to_ndarray(format=u"rgb24")
print(u"--- 原图三通道 %s" % [u"%.1f" % v for v in mean3(pure)])

ways = ((u"hue=s=0", [(u"hue", u"s=0")]),
        (u"format=gray", [(u"format", u"gray")]),
        (u"colorchannelmixer",
         [(u"colorchannelmixer", u".3:.4:.3:0:.3:.4:.3:0:.3:.4:.3")]))
for tag, fl in ways:
    a = run(fl, 1)[0]
    v = mean3(a)
    same = max(v) - min(v) < 1.0
    print(u"  %-18s %s  三通道相等=%s"
          % (tag, [u"%.1f" % x for x in v], same))

实跑输出:

--------------------------------------------------------------------
7.12 黑白效果
--------------------------------------------------------------------

**① 三条路都能去色,结果不完全一样**

**`hue=s=0`、`format=gray`、`colorchannelmixer` 三条写法都常见:**

  写法                    做了什么           输出格式
  -----------------------------------------------------------
  hue=s=0                 饱和度归零         还是 yuv420p
  format=gray             只留亮度平面       gray
  colorchannelmixer=…     按权重混三通道     看输入格式

**② 实测:三通道均值是不是一样**

  写法                  R 均值     G 均值     B 均值     三通道相等
  ---------------------------------------------------------------------
  原图                  145.5      140.2      140.2      不相等
  hue=s=0               141.8      141.8      141.8      是
  format=gray           142.2      142.2      142.2      是
  colorchannelmixer     141.6      141.6      141.6      是

**三种写法都出灰,但灰的数值不一样:`hue=s=0` 落 141.8,`format=gray` 落 142.2。**
**差这一点是因为前者在 YUV 里把色度清零,后者直接扔掉色度平面再按 bt601 还原。**

**③ 只去色不压亮,和真灰度是两件事**

**「黑白」在日常语境里包含两层:没有颜色、只剩明暗。**
**`hue=s=0` 只做前者:亮度曲线一点没动,原来亮的地方还是那么亮。**

**想做「老照片」那种,还得在去色之后再压对比、降亮度、加噪点:**

`ffmpeg -i s7.mp4 -vf "hue=s=0,lutyuv=y=val*0.85,noise=alls=12" old.mp4`

**`lutyuv` 改亮度、`noise` 加颗粒,这两步在 7.12 之后还会碰到。**

**④ 什么时候用哪条**

  想要                         用哪条
  --------------------------------------------------
  最快去色、格式不变           hue=s=0
  后面还要接只吃灰度的滤镜     format=gray
  要自己定权重(比如偏红)     colorchannelmixer

**第三条的实际用途是「模拟特定胶片的感光」:**
**人眼对绿最敏感,权重写 `.3:.59:.11` 最接近真实明暗;**
**把红的权重调高,出来的黑白就偏「红滤镜」那种感觉。**

第一行是原图:R 145.5、G 140.2、B 140.2。G 与 B 相等不是巧合, 这片素材的主色是灰底加红条,除了红条之外三通道本来就接近。

后面三行三个数都相等,说明三种写法确实都去干净了色。 数值差零点几,就是三种算法在 YUV / RGB 之间来回转的细微差别。

判断写成 max(v) - min(v) < 1.0,不是写 ==—— 整数相除的取整会让三个通道差 1,写死相等会误判成「没去干净」。

30 秒实验

  1. 换权重:把 mixer 改成 .299:.587:.114,看灰值往哪边偏。
  2. 只留红:把 mixer 写成 1:0:0:0:0:0:0:0:0,看是什么效果。
  3. 加质感:接上 lutyuv=y=val*0.85 与 noise=alls=12,量均值掉多少。

常见误区

  1. 以为去色就是黑白照片。亮度曲线没动,只是没了颜色。
  2. 用 == 判断三通道相等。取整误差会差 1,要用容差。
  3. 先去色再想单独调某个颜色。颜色信息已经没了,补不回来。
自测三问(点开对答案) 1. 三条去色写法里哪条不改变格式? —— `hue=s=0`,输出还是 yuv420p。 2. 标准亮度权重是多少? —— 0.299 / 0.587 / 0.114(bt601),不是 0.3 / 0.4 / 0.3。 3. 为什么 `format=gray` 后面不能再调颜色? —— 色度平面已经被扔掉,颜色信息不存在了。

相关笔记