14. 数据预处理优化:YUV转RGB算法优化、RenderScript vs OpenGL vs Vulkan性能对比
相机启动过程中,有一块「隐形」的耗时大户——数据预处理。说白了,就是摄像头传感器吐出来的原始数据,得先转换成我们能用的格式。最常见的场景就是 YUV 转 RGB。
我见过不少团队,相机启动卡在「黑屏」阶段,其实不是底层驱动慢,而是这层数据转换没处理好。今天咱们就聊聊这块怎么优化。
14.1 YUV 转 RGB:为什么绕不开?
Android 相机底层输出的数据,绝大多数是 YUV 格式。但 UI 层要显示、要处理,通常需要 RGB。这个转换,跑在 CPU 上就是纯计算密集型任务。
一个 1080p 的帧,YUV420p 转 RGB888,每个像素要算 3 个通道。你想想看,30fps 的话,每秒要处理 622 万像素 × 30 = 1.86 亿次运算。CPU 扛得住才怪。
核心思路:别让 CPU 干这活。交给 GPU 或者专用计算单元。
14.2 三种主流方案对比
我这些年折腾过不少方案,最常用的就三个:RenderScript、OpenGL、Vulkan。咱们一个一个说。
| 方案 | 计算方式 | 延迟(1080p) | 兼容性 | 学习成本 |
|---|---|---|---|---|
| RenderScript | CPU/GPU 自动调度 | 8-12ms | 高(API 11+) | 低 |
| OpenGL ES 3.0 | GPU 片元着色器 | 3-6ms | 中(API 18+) | 中 |
| Vulkan | GPU 计算着色器 | 1-3ms | 低(API 24+) | 高 |
嗯,这里要注意:RenderScript 虽然兼容性好,但 Google 已经在 Android 12 标记为弃用。新项目不建议再用了。
14.3 RenderScript 实现(快速原型)
如果你还在维护老项目,或者想快速验证效果,RenderScript 确实方便。写个 .rs 文件就行。
// yuv_to_rgb.rs
#pragma version(1)
#pragma rs java_package_name(com.example.camera)
rs_allocation yuvData;
uint32_t width;
uint32_t height;
uchar4 __attribute__((kernel)) convert(uint32_t x, uint32_t y) {
// YUV420p 转 RGB 核心逻辑
uint32_t yIndex = y * width + x;
uint32_t uvIndex = (y / 2) * (width / 2) + (x / 2);
uchar Y = rsGetElementAt_uchar(yuvData, yIndex);
uchar U = rsGetElementAt_uchar(yuvData, width * height + uvIndex);
uchar V = rsGetElementAt_uchar(yuvData, width * height * 5 / 4 + uvIndex);
int R = Y + 1.402 * (V - 128);
int G = Y - 0.344 * (U - 128) - 0.714 * (V - 128);
int B = Y + 1.772 * (U - 128);
return (uchar4){clamp(R,0,255), clamp(G,0,255), clamp(B,0,255), 255};
}
个人经验:RenderScript 在低端机上容易触发 CPU 回退。我遇到过某款骁龙 4 系机型,RenderScript 跑 YUV 转换比纯 Java 还慢。所以别太依赖它。
14.4 OpenGL ES 方案(主流选择)
OpenGL 方案是我个人最常用的。把 YUV 数据上传为纹理,在片元着色器里做转换。效率比 RenderScript 高不少。
// 片元着色器
#version 300 es
precision mediump float;
uniform sampler2D yTexture;
uniform sampler2D uTexture;
uniform sampler2D vTexture;
in vec2 vTexCoord;
out vec4 fragColor;
void main() {
float Y = texture(yTexture, vTexCoord).r;
float U = texture(uTexture, vTexCoord).r - 0.5;
float V = texture(vTexture, vTexCoord).r - 0.5;
float R = Y + 1.402 * V;
float G = Y - 0.344 * U - 0.714 * V;
float B = Y + 1.772 * U;
fragColor = vec4(R, G, B, 1.0);
}
这里有个坑:纹理上传本身也有开销。我建议用 PBO(Pixel Buffer Object)做异步上传,避免阻塞渲染线程。
我曾经踩过的坑:OpenGL 纹理尺寸必须是 2 的幂?那是老黄历了。ES 3.0 开始支持非 2 的幂纹理(NPOT),但要注意某些低端驱动实现有 bug。我遇到过华为某款机型,NPOT 纹理渲染出来是花的。解决方案是手动补齐到 2 的幂,或者用纹理矩形扩展。
14.5 Vulkan 方案(极致性能)
Vulkan 方案,说白了就是「自己管一切」。没有驱动黑盒,没有隐式同步。性能上限最高,但代码量也最大。
核心思路是用计算着色器(Compute Shader),直接在 GPU 上做 YUV 转换。不需要经过渲染管线,效率更高。
// Vulkan 计算着色器片段
#version 450
layout(local_size_x = 16, local_size_y = 16) in;
layout(set = 0, binding = 0) uniform sampler2D yImage;
layout(set = 0, binding = 1) uniform sampler2D uImage;
layout(set = 0, binding = 2) uniform sampler2D vImage;
layout(set = 0, binding = 3, rgba8) uniform image2D outputImage;
void main() {
ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
vec2 uv = vec2(pos) / vec2(imageSize(outputImage));
float Y = texture(yImage, uv).r;
float U = texture(uImage, uv).r - 0.5;
float V = texture(vImage, uv).r - 0.5;
vec3 rgb = vec3(
Y + 1.402 * V,
Y - 0.344 * U - 0.714 * V,
Y + 1.772 * U
);
imageStore(outputImage, pos, vec4(rgb, 1.0));
}
性能数据(实测):在骁龙 8 Gen 2 上,1080p YUV 转 RGB:
- CPU 纯计算:18-22ms
- RenderScript:8-12ms
- OpenGL ES:3-5ms
- Vulkan 计算着色器:1.2-2ms
14.6 方案选型建议
我个人的建议是这样的:
- 兼容性优先:用 OpenGL ES 3.0,覆盖 95% 以上设备
- 性能极致:Vulkan + 计算着色器,但要做好降级方案
- 快速迭代:可以用 RenderScript 做原型,但别上生产
你想想看,相机启动总共就那么几百毫秒。数据预处理如果能从 20ms 压到 2ms,省出来的时间足够做很多事了。
14.7 核心知识体系
下面这张图,是我整理的数据预处理优化全貌。你可以对照着看看自己卡在哪一环。
从图上可以看得很清楚:数据从 Camera 出来,经过三种不同的计算路径,最终输出 RGB。每种路径的耗时和兼容性都不一样。选型的时候,得根据你的目标设备群体来权衡。
我的建议:别一上来就上 Vulkan。先拿 OpenGL 把流程跑通,性能不够再换。我见过太多团队,Vulkan 写了三个月,最后发现 OpenGL 已经够用了。
数据预处理这块,说白了就是「用空间换时间,用硬件换性能」。CPU 干不了的活,交给 GPU。GPU 干不了的,交给专用硬件。咱们做系统优化的,就是要把每一毫秒都榨出来。