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实现的。

核心思路是这样的:

  1. 把摄像头帧数据上传到GPU纹理
  2. 编写片段着色器(Fragment Shader)来处理像素
  3. 把处理后的纹理渲染到屏幕上

来看一个简单的颜色反转滤镜的着色器:

// 片段着色器:颜色反转
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渲染 输出到SurfaceView

这个框架的核心思想是:把滤镜引擎抽象出来,上层业务不需要关心底层是用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