23. Camera滤镜与特效:实时滤镜原理、使用RenderScript处理帧数据、OpenGL ES滤镜实现
滤镜这个东西,说白了就是给视频画面“化妆”。
我刚开始做摄像头开发时,觉得滤镜特别神秘。后来自己动手实现了一遍,发现核心原理其实不复杂。今天咱们就把这层窗户纸捅破。
实时滤镜的核心原理
实时滤镜的本质是什么?是对每一帧图像的像素进行数学变换。
你想想看,摄像头采集到的原始数据,是一堆RGB或YUV像素值。滤镜要做的事情,就是按照某种规则,把这些像素值重新计算一遍。
举个例子,黑白滤镜的规则很简单:
// 伪代码:黑白滤镜
for each pixel (r, g, b):
gray = 0.299 * r + 0.587 * g + 0.114 * b
new_r = gray
new_g = gray
new_b = gray
嗯,就是这么直白。但实时滤镜的难点在于——每一帧只有16ms到33ms的处理时间(取决于帧率)。
关键点:实时滤镜必须在帧间隔时间内完成处理,否则画面就会卡顿。
所以,我们得用高效的手段来处理。Android平台上,主要有两条路:RenderScript和OpenGL ES。
使用RenderScript处理帧数据
RenderScript是Android提供的一个高性能计算框架。我个人习惯用它来做一些像素级的滤镜,因为写起来比较直观。
先看一个简单的灰度滤镜实现:
// RenderScript脚本:gray.rs
#pragma version(1)
#pragma rs java_package_name(com.example.filter)
rs_allocation input;
rs_allocation output;
void root(const uchar4 *v_in, uchar4 *v_out, uint32_t x, uint32_t y) {
float r = (float)v_in->r;
float g = (float)v_in->g;
float b = (float)v_in->b;
uchar gray = (uchar)(0.299f * r + 0.587f * g + 0.114f * b);
v_out->r = gray;
v_out->g = gray;
v_out->b = gray;
v_out->a = v_in->a;
}
然后在Java/Kotlin中调用:
// Kotlin调用RenderScript
val rs = RenderScript.create(context)
val inputAlloc = Allocation.createTyped(rs, Type.createXY(rs, Element.RGBA_8888(rs), width, height))
val outputAlloc = Allocation.createTyped(rs, Type.createXY(rs, Element.RGBA_8888(rs), width, height))
val script = ScriptC_gray(rs)
script._input = inputAlloc
script._output = outputAlloc
// 每帧调用
inputAlloc.copyFrom(bitmap)
script.forEach_root(inputAlloc, outputAlloc)
outputAlloc.copyTo(bitmap)
小提示:RenderScript在API 23之后性能提升很大。但要注意,它不支持所有设备,有些低端机可能没有GPU加速。
我曾经在一个项目里用RenderScript做美颜滤镜,效果还不错。但后来发现一个问题——当滤镜逻辑变得复杂时,RenderScript的脚本维护起来有点头疼。
为什么会这样?因为RenderScript的调试手段有限,你没法像普通代码那样打断点。
OpenGL ES滤镜实现
说到真正的工业级方案,还得是OpenGL ES。几乎所有主流相机App的滤镜,底层都是OpenGL ES实现的。
核心思路是这样的:
- 把摄像头帧数据上传到GPU纹理
- 编写片段着色器(Fragment Shader)来处理像素
- 把处理后的纹理渲染到屏幕上
来看一个简单的颜色反转滤镜的着色器:
// 片段着色器:颜色反转
precision mediump float;
uniform sampler2D uTexture;
varying vec2 vTexCoord;
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
// 颜色反转:1.0 - 原色
gl_FragColor = vec4(1.0 - color.rgb, color.a);
}
是不是很简洁?OpenGL ES的强大之处在于,GPU可以并行处理成千上万个像素,速度极快。
再来看一个更实用的——冷暖色调滤镜:
// 片段着色器:冷暖色调
precision mediump float;
uniform sampler2D uTexture;
uniform float uTemperature; // -1.0 冷色, 1.0 暖色
varying vec2 vTexCoord;
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
// 暖色调:增强红色,减弱蓝色
// 冷色调:增强蓝色,减弱红色
float r = color.r + color.r * uTemperature * 0.1;
float b = color.b - color.b * uTemperature * 0.1;
gl_FragColor = vec4(r, color.g, b, color.a);
}
注意:OpenGL ES的纹理坐标是归一化的(0.0到1.0),和像素坐标不同。我第一次写的时候就在这里栽了跟头,画面一直显示不对。
在Android中,完整的滤镜流程是这样的:
// Kotlin:OpenGL滤镜渲染流程
class FilterRenderer {
private var program: Int = 0
private var textureId: Int = 0
fun init() {
// 1. 编译着色器
val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
// 2. 创建程序
program = GLES20.glCreateProgram()
GLES20.glAttachShader(program, vertexShader)
GLES20.glAttachShader(program, fragmentShader)
GLES20.glLinkProgram(program)
}
fun render(textureId: Int) {
// 3. 使用程序
GLES20.glUseProgram(program)
// 4. 绑定纹理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
// 5. 绘制矩形
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
}
}
RenderScript vs OpenGL ES:怎么选?
我整理了一个对比表格,方便你决策:
| 维度 | RenderScript | OpenGL ES |
|---|---|---|
| 学习曲线 | 较平缓,类似C语言 | 较陡峭,需要理解图形管线 |
| 性能 | 良好,但受限于设备 | 优秀,GPU原生支持 |
| 调试难度 | 较难,日志有限 | 中等,可用GLSL调试工具 |
| 滤镜复杂度 | 适合简单像素操作 | 支持复杂特效(模糊、边缘检测等) |
| 跨平台 | 仅Android | Android/iOS通用 |
我个人建议:
- 如果只是做简单的颜色调整、黑白滤镜,用RenderScript就够了
- 如果要实现抖音那种复杂特效、转场动画,必须上OpenGL ES
- 如果考虑跨平台,直接选OpenGL ES
实战:实现一个实时滤镜框架
下面是我在项目中常用的滤镜框架结构,用SVG画出来:
这个框架的核心思想是:把滤镜引擎抽象出来,上层业务不需要关心底层是用RenderScript还是OpenGL ES。
我封装了一个接口:
interface FilterEngine {
fun processFrame(input: Any, output: Any)
fun release()
}
class RenderScriptFilter : FilterEngine {
override fun processFrame(input: Any, output: Any) {
// RenderScript实现
}
override fun release() { }
}
class OpenGLFilter : FilterEngine {
override fun processFrame(input: Any, output: Any) {
// OpenGL ES实现
}
override fun release() { }
}
这样做的好处是,你可以随时切换滤镜引擎,甚至可以在运行时根据设备性能动态选择。
避坑指南:我曾经在低端机上用OpenGL ES做复杂滤镜,结果帧率掉到15fps。后来加了一个性能检测逻辑,如果帧率低于20fps,自动降级到简单滤镜。这个策略很实用。
总结一下
实时滤镜的核心就是像素变换。RenderScript适合快速实现简单滤镜,OpenGL ES适合做复杂特效。
我个人更推荐OpenGL ES,因为它的上限更高,而且跨平台。但如果你只是做个Demo,RenderScript上手更快。
嗯,滤镜这块内容不少,但掌握了原理,剩下的就是多写多练了。
公众号:蓝海资料掘金营,微信deep3321