16、视频滤镜基础:颜色矩阵与简单滤镜实现
各位同学,今天我们来聊聊视频滤镜的基础。说实话,滤镜这东西听起来很酷,但底层原理其实没那么玄乎。说白了,就是对着每个像素的颜色值做数学运算。
我在做视频渲染的时候,经常被问到:「滤镜不就是套个模板吗?」嗯,如果你只想用现成的,那确实简单。但如果你想自己写一个滤镜引擎,那就得从颜色矩阵开始啃了。
16.1 颜色矩阵(ColorMatrix)是什么
颜色矩阵,本质上是一个 4x5 的矩阵。它负责把像素的 RGBA 四个通道,通过线性变换变成新的 RGBA 值。
你想想看,一张图片有上百万个像素,每个像素有四个值。如果要对每个像素做同样的运算,最优雅的方式就是用矩阵乘法。
矩阵长这样:
| R' | | a b c d e | | R |
| G' | | f g h i j | | G |
| B' | = | k l m n o | * | B |
| A' | | p q r s t | | A |
| 1 | | 0 0 0 0 1 | | 1 |
注意看,最后一行是固定的 [0,0,0,0,1],这是齐次坐标的写法。实际运算时,我们只关心前四行。
举个例子,如果你想让红色通道翻倍,其他不变,矩阵就是:
| 2 0 0 0 0 |
| 0 1 0 0 0 |
| 0 0 1 0 0 |
| 0 0 0 1 0 |
就这么简单。每个输出通道,是输入通道的线性组合加上一个偏移量。
核心公式:R' = a*R + b*G + c*B + d*A + e
其中 a-d 是系数,e 是偏移量。其他通道同理。
16.2 Android 中的 ColorMatrix 类
Android SDK 里已经帮我们封装好了 ColorMatrix 类。你不需要自己手写矩阵乘法,直接调用 API 就行。
我记得第一次用的时候,还觉得这玩意儿挺神秘的。后来看了源码,发现就是个 4x5 的 float 数组,加一堆 set/get 方法。
常用操作:
- 构造:
new ColorMatrix()默认是单位矩阵(什么都不变) - 设置饱和度:
setSaturation(float sat),sat=0 变灰度,sat=1 正常 - 设置缩放:
setScale(float rScale, float gScale, float bScale, float aScale) - 矩阵乘法:
postConcat(ColorMatrix other)或preConcat(ColorMatrix other)
这里有个坑,我刚开始做的时候踩过:setSaturation(0) 确实能变灰度,但它的实现方式是把 RGB 三个通道都映射到亮度值。如果你想要更精细的控制,比如只保留红色通道,那就得自己构造矩阵。
16.3 ColorMatrix 与 ColorFilter 的关系
ColorMatrix 是数据,ColorFilter 是行为。你可以把 ColorMatrix 塞进 ColorFilter 里,然后应用到 Paint 或 Drawable 上。
在视频渲染场景中,我们通常用 ColorMatrixColorFilter:
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0); // 灰度
ColorMatrixColorFilter filter = new ColorMatrixColorFilter(cm);
paint.setColorFilter(filter);
然后你用这个 paint 去画任何东西,都会被滤镜处理。在 SurfaceView 的 Canvas 上画帧的时候,这就是最直接的滤镜方式。
但注意,这种方式是在 CPU 上处理的。如果你要实时处理 30fps 的视频,CPU 扛不住。那时候就得用 OpenGL 的 fragment shader 来做矩阵运算。不过那是后面章节的内容了,今天我们先吃透 CPU 版本。
我的建议:先用 ColorMatrix 在 CPU 上验证滤镜效果,确认数学公式没问题,再迁移到 GPU 上。这样调试成本低很多。
16.4 简单滤镜实现
好,理论说完了,我们来写三个最经典的滤镜:灰度、怀旧、负片。
16.4.1 灰度滤镜
灰度滤镜的核心思想:把 RGB 三个通道变成同一个值,这个值就是亮度。
亮度公式有很多种,最常用的是:L = 0.299*R + 0.587*G + 0.114*B
为什么是这个系数?因为人眼对绿色最敏感,对蓝色最不敏感。这是心理学和生理学的研究结果,不是我瞎编的。
public static ColorMatrix createGrayMatrix() {
ColorMatrix cm = new ColorMatrix(new float[]{
0.299f, 0.587f, 0.114f, 0, 0,
0.299f, 0.587f, 0.114f, 0, 0,
0.299f, 0.587f, 0.114f, 0, 0,
0, 0, 0, 1, 0
});
return cm;
}
你看,三行完全一样,都是亮度系数。这样 R'、G'、B' 都等于亮度 L,自然就变成灰度了。
16.4.2 怀旧滤镜
怀旧滤镜,也叫复古滤镜。它的效果是让画面偏黄褐色,同时降低对比度。
我在项目中实现过一个版本,参考了 Photoshop 的「老照片」效果。公式如下:
public static ColorMatrix createSepiaMatrix() {
ColorMatrix cm = new ColorMatrix(new float[]{
0.393f, 0.769f, 0.189f, 0, 0,
0.349f, 0.686f, 0.168f, 0, 0,
0.272f, 0.534f, 0.131f, 0, 0,
0, 0, 0, 1, 0
});
return cm;
}
这个矩阵的特点是:红色通道的权重最高,蓝色最低。所以画面会偏暖色,也就是黄褐色调。
我曾经在一个老照片修复项目里用过这个滤镜,配合一些噪声和划痕纹理,效果还挺像那么回事的。
16.4.3 负片滤镜
负片最简单,就是每个通道取反:R' = 255 - R。
在矩阵里,就是系数为 -1,偏移量为 255:
public static ColorMatrix createNegativeMatrix() {
ColorMatrix cm = new ColorMatrix(new float[]{
-1, 0, 0, 0, 255,
0, -1, 0, 0, 255,
0, 0, -1, 0, 255,
0, 0, 0, 1, 0
});
return cm;
}
注意偏移量是 255,因为像素值范围是 0-255。如果你用归一化的 0.0-1.0 范围,偏移量就是 1.0。
我曾经踩过的坑:在 OpenGL 里用归一化值时,偏移量写成了 255,结果画面一片白。后来排查了半天才发现是范围问题。所以一定要搞清楚你用的颜色空间是 0-255 还是 0.0-1.0。
16.5 知识体系总览
下面这张图总结了本章的核心内容,从颜色矩阵到滤镜实现的完整链路:
16.6 实际应用中的注意事项
最后,分享几个我在项目中积累的经验:
- 性能问题:ColorMatrix 在 CPU 上处理 1080p 视频,每帧大约耗时 5-10ms。如果叠加多个滤镜,时间会累加。建议在低分辨率帧上预览,确认效果后再全分辨率输出。
- 精度问题:矩阵运算用 float,多次叠加后会有精度损失。尤其是做负片再取反,可能回不到原始值。我一般会在关键帧上保留原始数据。
- Alpha 通道:大多数滤镜不修改 Alpha,但如果你做透明度相关的效果,记得把第四行也配好。否则会出现诡异的半透明边缘。
- 组合滤镜:多个 ColorMatrix 可以用
postConcat合并成一个,减少运算次数。但要注意顺序,矩阵乘法不满足交换律。
一个小技巧:调试滤镜时,可以在屏幕上同时显示原图和滤镜效果,用 SeekBar 动态调整矩阵参数。这样你能直观看到每个系数的作用,比对着数字猜快多了。
好了,这一章的内容就到这里。颜色矩阵是视频滤镜的基石,搞懂了它,后面那些花里胡哨的滤镜效果,无非就是在这个矩阵上做文章。