【Android 美颜相机】第十天:YUV420SP和RGB

YUV420SP转RGB及位图

本文将详细解析yuv-decoder.c文件中的JNI代码,该代码运行在Android平台,通过C语言实现YUV420SP格式图像数据到RGBA/ARBG格式的转换,以及位图像素调整功能,借助JNI完成Java与C层的交互,兼顾图像处理效率与跨层调用兼容性。

理论知识

在Android/iOS等移动端图像处理、音视频开发领域,YUV420SP和RGBA是最常接触的两种像素格式------前者主导了相机预览、视频编码/传输的高效存储与传输,后者则是屏幕渲染、位图处理的核心格式。

为什么需要两种像素格式?

在深入格式细节前,首先要明确一个核心问题:为什么不只用一种格式?

  • RGBA:面向「显示」设计,直接映射人眼对红、绿、蓝三原色的感知,是屏幕渲染的「原生语言」,但存储/传输效率低;
  • YUV:面向「存储/传输」设计,将图像分为亮度(Y)和色度(U/Cb、V/Cr),利用「人眼对亮度更敏感、对色度不敏感」的生理特性,对色度分量降采样,大幅降低数据量(YUV420仅为RGB的1/2~1/3)。

YUV420SP是YUV家族中移动端最主流的子格式,RGBA是RGB家族的扩展(增加Alpha通道),二者共同构成了移动端图像「传输-处理-显示」的完整链路。

RGBA格式:面向显示的像素编码

1. 格式定义与核心组成

RGBA是RGB格式的扩展,包含四个通道:

  • R(Red):红色通道,取值范围0~255;
  • G(Green):绿色通道,取值范围0~255;
  • B(Blue):蓝色通道,取值范围0~255;
  • A(Alpha):透明度通道,取值范围0~255(0全透明,255完全不透明)。

四个通道共同构成一个32位(4字节) 的像素单元,是移动端最常用的「真彩色」格式,也是Android Bitmap默认的ARGB_8888格式(位序略有差异,本质一致)。

2. 存储结构:逐像素连续存储

RGBA采用「逐像素、通道连续」的存储方式,每个像素占4字节,位序通常为:

复制代码
31~24位:Alpha(A) | 23~16位:Red(R) | 15~8位:Green(G) | 7~0位:Blue(B)

示例:一个不透明的纯红色像素,存储值为0xFF0000FF(A=255, R=255, G=0, B=0)。

对于分辨率为W×H的图像,RGBA的总数据量计算公式为:

复制代码
总字节数 = W × H × 4

例:1080×1920的图像,RGBA格式总大小=1080×1920×4=8,294,400字节≈8MB。

3. 优缺点与应用场景

优点 缺点
1. 直接映射显示原理,无需转换即可渲染; 2. 色彩还原精准,无采样损失; 3. Alpha通道支持透明效果 1. 数据量庞大,存储/传输成本高; 2. 相机/视频原生数据不采用,需格式转换

核心应用场景

  • Android/iOS的Bitmap/Image对象存储;
  • OpenGL/Metal等图形API的纹理渲染;
  • UI界面、静态图片(PNG/JPEG解码后)的显示。

YUV420SP格式:面向传输的高效编码

YUV420SP(也称为NV21)是YUV 4:2:0采样格式的「半平面(Semi-Planar)」变体,是Android相机预览、视频编码的默认格式(iOS多为NV12,与NV21仅Cb/Cr顺序相反)。

1. YUV 4:2:0采样规则

YUV的核心是「色度降采样」,4:2:0表示:

  • Y(亮度):全分辨率采样,每个像素对应一个Y值;
  • Cb(色度蓝差)/Cr(色度红差):水平和垂直方向均降采样为1/2,即每4个Y像素共享一组Cb/Cr值(2×2像素块)。

直观理解:1个2×2的Y像素块(4个Y值),仅对应1个Cb值和1个Cr值,而非4组(如RGB)。

2. YUV420SP的存储结构

YUV420SP采用「半平面」存储,数据分为两个连续区域:

  1. Y平面 :所有Y分量连续存储,占W×H字节;
  2. UV交错平面 :Cb和Cr分量交错存储(Cb、Cr、Cb、Cr...),占W×H/2字节(总长度为Y平面的1/2)。

整体存储布局如下:

复制代码
[Y0 Y1 Y2 Y3 ... Y(W×H-1)] [Cb0 Cr0 Cb1 Cr1 ... Cb(W×H/4-1) Cr(W×H/4-1)]

示例:2×2分辨率的YUV420SP数据布局:

复制代码
Y平面:Y0 Y1 Y2 Y3
UV平面:Cb0 Cr0

(4个Y像素共享1组Cb/Cr,总字节数=4+2=6,而RGBA需4×4=16字节)

3. 数据量计算

对于W×H的图像,YUV420SP总字节数计算公式:

复制代码
总字节数 = W×H(Y平面) + W×H/2(UV平面) = W×H × 1.5

例:1080×1920的图像,YUV420SP总大小=1080×1920×1.5=3,110,400字节≈3MB,仅为RGBA的37.5%。

4. 优缺点与应用场景

优点 缺点
1. 数据量仅为RGBA的3/8,存储/传输效率极高; 2. 相机传感器原生输出格式,无需前期转换; 3. 视频编码(H.264/H.265)的基础格式 1. 无法直接渲染,需转换为RGB/RGBA; 2. 色度降采样导致轻微色彩损失(人眼不易察觉); 3. 存储结构复杂,处理逻辑比RGBA繁琐

核心应用场景

  • Android相机预览数据输出;
  • 视频流(直播、短视频)的编码与传输;
  • 摄像头采集、视频解码的中间格式。

YUV420SP与RGBA的转换原理

YUV420SP无法直接用于显示,必须转换为RGBA,核心遵循ITU-R BT.601 (标清)或ITU-R BT.709(高清)标准转换公式。

1. 标准浮点转换公式(BT.601)

首先将Y/Cb/Cr归一化到标准范围(Y:16235,Cb/Cr:16240),再转换为RGB:

复制代码
R = 1.164×(Y-16) + 2.018×(Cr-128)
G = 1.164×(Y-16) - 0.813×(Cb-128) - 0.391×(Cr-128)
B = 1.164×(Y-16) + 1.596×(Cb-128)
A = 255(固定不透明)

2. 移动端优化:整数近似转换

浮点运算在移动端性能较低,实际工程中会用「移位运算」替代浮点乘法(如前文JNI代码中的实现):

复制代码
// 1.164×Y ≈ Y + (Y>>3) + (Y>>5) + (Y>>7)
Y = Y + (Y >> 3) + (Y >> 5) + (Y >> 7);
// 近似计算R/G/B
R = Y + (Cr << 1) + (Cr >> 6);
G = Y - Cb + (Cb >> 3) + (Cb >> 4) - (Cr >> 1) + (Cr >> 3);
B = Y + Cb + (Cb >> 1) + (Cb >> 4) + (Cb >> 5);
// 限制范围0~255
R = clamp(R, 0, 255);
G = clamp(G, 0, 255);
B = clamp(B, 0, 255);
// 组装RGBA:0xAARRGGBB
RGBA = 0xFF000000 | (R << 16) | (G << 8) | B;

这种近似方式牺牲了极轻微的色彩精度,但性能提升5~10倍,是移动端的主流实现。

两种格式的核心对比与链路应用

1. 核心参数对比

特性 RGBA YUV420SP(NV21)
单像素大小 4字节(32位) 1.5字节(平均)
1080P数据量 ~8MB ~3MB
色彩精度 无损 色度降采样(轻微损失)
渲染兼容性 直接渲染 需转换为RGBA
原生数据源 静态图片、UI 相机、视频

2. 移动端图像链路中的应用

一个典型的「相机预览→滤镜处理→显示」链路:

复制代码
相机传感器 → YUV420SP数据 → JNI层转换为RGBA → OpenGL加载为纹理 → 滤镜处理 → 屏幕渲染(RGBA)

一个典型的「视频播放」链路:

复制代码
视频文件(H.264)→ 解码为YUV420SP → 转换为RGBA → Bitmap/纹理 → 屏幕显示

代码分析

c 复制代码
#include <jni.h> // 引入JNI核心头文件,提供Java与C交互的基础API(数组操作、JNIEnv函数等)
#include <android/bitmap.h> // 引入Android Bitmap操作头文件,提供位图信息获取、像素锁定/解锁接口
#include <GLES2/gl2.h> // 引入OpenGL ES 2.0头文件,提供glReadPixels等像素读取的OpenGL接口

代码含义

这是代码的基础依赖层:jni.h是JNI开发必备,负责Java与C之间的数据传递(如数组、对象操作);android/bitmap.h用于操作Android Bitmap对象,实现像素数据读写;GLES2/gl2.h为OpenGL读取像素数据提供接口支持。

YUVtoRBGA函数

该JNI函数供Java层调用,完整签名为Java_jp_co_cyberagent_android_gpuimage_GPUImageNativeLibrary_YUVtoRBGA,核心功能是将YUV420SP格式字节数组转换为RGBA格式整型数组(Alpha固定为0xFF,像素格式0xAARRGGBB)。

1. 函数定义与变量声明

c 复制代码
JNIEXPORT void JNICALL // 标识该函数为JNI导出函数,可被Java层调用,返回值为void
Java_jp_co_cyberagent_android_gpuimage_GPUImageNativeLibrary_YUVtoRBGA(
    JNIEnv *env,        // JNI环境指针,访问JNI函数的核心入口
    jobject obj,        // 调用该方法的Java对象实例(对应GPUImageNativeLibrary类实例)
    jbyteArray yuv420sp,// Java层传入的YUV420SP格式图像字节数组
    jint width,         // 图像宽度
    jint height,        // 图像高度
    jintArray rgbOut) { // 输出的RGBA格式像素数组(Java层int数组)
    
    int sz;             // 存储图像总像素数(width * height,即Y分量长度)
    int i;              // 列索引,遍历图像每一列
    int j;              // 行索引,遍历图像每一行
    int Y;              // Y分量(亮度)
    int Cr = 0;         // Cr分量(色度红差),初始化为0
    int Cb = 0;         // Cb分量(色度蓝差),初始化为0
    int pixPtr = 0;     // 像素位置指针,定位当前处理的像素索引
    int jDiv2 = 0;      // 行索引除以2的结果(YUV420SP色度分量行维度下采样2倍)
    int R = 0;          // 转换后的红色分量
    int G = 0;          // 转换后的绿色分量
    int B = 0;          // 转换后的蓝色分量
    int cOff;           // 色度分量(Cb/Cr)在YUV数组中的偏移量
    int w = width;      // 图像宽度(局部变量,减少全局变量访问开销)
    int h = height;     // 图像高度(局部变量,减少全局变量访问开销)
    sz = w * h;         // 计算Y分量总像素数(YUV420SP中Y占w*h字节,Cb/Cr共占w*h/2字节)

    // 获取Java数组的临界区指针(避免GC移动数组,提升访问效率,减少数据拷贝)
    jint *rgbData = (jint *) ((*env)->GetPrimitiveArrayCritical(env, rgbOut, 0));
    jbyte *yuv = (jbyte *) (*env)->GetPrimitiveArrayCritical(env, yuv420sp, 0);

代码含义

  • JNI函数命名遵循「Java_包名_类名_方法名」规则(包名.替换为_),确保Java层能映射到该C函数;
  • sz计算Y分量总长度(YUV420SP存储结构:Y分量在前,Cb/Cr交错存储在后);
  • GetPrimitiveArrayCritical是高性能数组访问接口,获取数组直接指针(临界区),避免GC干扰,提升处理速度。

2. 核心循环:逐像素转换YUV到RGBA

c 复制代码
    // 外层循环:遍历图像每一行
    for (j = 0; j < h; j++) {
        pixPtr = j * w;         // 计算当前行第一个像素的索引(行偏移)
        jDiv2 = j >> 1;         // 等价于j/2,移位运算比除法高效,定位Cb/Cr行偏移
        // 内层循环:遍历当前行每一列
        for (i = 0; i < w; i++) {
            Y = yuv[pixPtr];    // 获取当前像素的Y分量(亮度)
            if (Y < 0) Y += 255;// 修正Y分量符号(jbyte是有符号字节,转换为0~255范围)
            
            // YUV420SP的Cb/Cr每2个像素共享一组,仅偶数列(i&0x1 !=1)更新Cb/Cr
            if ((i & 0x1) != 1) {
                // 计算Cb/Cr偏移:Y分量长度 + 色度行偏移 + 色度列偏移
                cOff = sz + jDiv2 * w + (i >> 1) * 2;
                Cb = yuv[cOff]; // 获取Cb分量
                if (Cb < 0) Cb += 127; else Cb -= 128; // 修正Cb为-128~127范围
                Cr = yuv[cOff + 1]; // 获取Cr分量
                if (Cr < 0) Cr += 127; else Cr -= 128; // 修正Cr为-128~127范围
            }

            // ITU-R BT.601标准转换公式(浮点版注释),代码用移位实现整数近似(提升效率)
            // 标准公式:
            // R = 1.164*(Y-16) + 2.018*(Cr-128);
            // G = 1.164*(Y-16) - 0.813*(Cb-128) - 0.391*(Cr-128);
            // B = 1.164*(Y-16) + 1.596*(Cb-128);
            
            // 整数近似计算1.164*Y(1.164≈1+1/8+1/32+1/128,对应移位)
            Y = Y + (Y >> 3) + (Y >> 5) + (Y >> 7);
            // 近似计算R分量(替代浮点乘法)
            R = Y + (Cr << 1) + (Cr >> 6);
            if (R < 0) R = 0; else if (R > 255) R = 255; // 限制R在0~255范围
            // 近似计算G分量
            G = Y - Cb + (Cb >> 3) + (Cb >> 4) - (Cr >> 1) + (Cr >> 3);
            if (G < 0) G = 0; else if (G > 255) G = 255; // 限制G在0~255范围
            // 近似计算B分量
            B = Y + Cb + (Cb >> 1) + (Cb >> 4) + (Cb >> 5);
            if (B < 0) B = 0; else if (B > 255) B = 255; // 限制B在0~255范围
            
            // 组装RGBA像素值:Alpha=0xFF(不透明),格式0xAARRGGBB
            rgbData[pixPtr++] = 0xff000000 + (R << 16) + (G << 8) + B;
        }
    }

代码含义

  • 双层循环遍历所有像素,是图像处理核心逻辑;
  • YUV420SP色度采样规则:4:2:0意味着2×2个Y像素共享一组Cb/Cr,偶数列更新Cb/Cr避免重复计算;
  • 移位运算替代浮点乘法:移动端浮点运算性能差,通过移位实现1.164等系数的近似计算,大幅提升效率;
  • 像素组装:0xff000000固定Alpha通道,R<<16/G<<8/B分别对应RGBA的红、绿、蓝通道位序。

3. 释放临界区资源

c 复制代码
    // 释放数组临界区指针,允许GC重新管理数组,避免内存泄漏
    (*env)->ReleasePrimitiveArrayCritical(env, rgbOut, rgbData, 0);
    (*env)->ReleasePrimitiveArrayCritical(env, yuv420sp, yuv, 0);
}

代码含义
ReleasePrimitiveArrayCriticalGetPrimitiveArrayCritical的配套接口,必须调用以释放临界区,否则会导致GC异常或内存泄漏;参数0表示允许JNI将修改后的数据写回数组。

YUVtoARBG函

该函数逻辑与YUVtoRBGA完全一致,仅像素值组装位序不同,适配ARBG格式(像素格式0xAABBGGRR)。

c 复制代码
JNIEXPORT void JNICALL
Java_jp_co_cyberagent_android_gpuimage_GPUImageNativeLibrary_YUVtoARBG(JNIEnv *env, jobject obj,
                                                                       jbyteArray yuv420sp,
                                                                       jint width, jint height,
                                                                       jintArray rgbOut) {
    // 变量声明、YUV读取、RGB近似计算逻辑与YUVtoRBGA完全一致,省略重复代码...
    
            // 核心差异:像素组装为ARBG格式(B<<16 + G<<8 + R)
            rgbData[pixPtr++] = 0xff000000 + (B << 16) + (G << 8) + R;
    }
    // 释放临界区资源(与YUVtoRBGA一致)
    (*env)->ReleasePrimitiveArrayCritical(env, rgbOut, rgbData, 0);
    (*env)->ReleasePrimitiveArrayCritical(env, yuv420sp, yuv, 0);
}

代码含义

函数命名对应Java层YUVtoARBG方法,唯一差异是像素位序:将蓝色分量放到1623位,红色分量放到07位,满足部分渲染场景的像素序要求。

adjustBitmap函数:位图像素调整(垂直翻转)

该函数通过OpenGL读取像素数据,对Bitmap进行垂直翻转(交换上下半部分像素),解决OpenGL与Android Bitmap坐标体系不一致的问题。

1. 函数定义与位图信息获取

c 复制代码
JNIEXPORT void JNICALL
Java_jp_co_cyberagent_android_gpuimage_GPUImageNativeLibrary_adjustBitmap(JNIEnv *jenv, jclass thiz,
                                                                       jobject src) {
    unsigned char *srcByteBuffer; // Bitmap像素字节缓冲区指针
    int result = 0;               // AndroidBitmap接口调用结果(成功/失败)
    int i, j;                     // 循环索引(行/列)
    AndroidBitmapInfo srcInfo;    // 存储Bitmap元信息(宽、高、像素格式等)

    // 获取Bitmap基本信息(宽、高、像素格式),失败则返回
    result = AndroidBitmap_getInfo(jenv, src, &srcInfo);
    if (result != ANDROID_BITMAP_RESULT_SUCCESS) {
        return;
    }

    // 锁定Bitmap像素缓冲区,获取直接访问指针(避免GC修改,确保读写安全)
    result = AndroidBitmap_lockPixels(jenv, src, (void **) &srcByteBuffer);
    if (result != ANDROID_BITMAP_RESULT_SUCCESS) {
        return;
    }

    int width = srcInfo.width;    // 提取Bitmap宽度
    int height = srcInfo.height;  // 提取Bitmap高度
    // 从OpenGL帧缓冲区读取像素到Bitmap缓冲区(RGBA格式,无符号字节)
    glReadPixels(0, 0, srcInfo.width, srcInfo.height, GL_RGBA, GL_UNSIGNED_BYTE, srcByteBuffer);

代码含义

  • AndroidBitmap_getInfo获取Bitmap元信息,是操作像素的前提;
  • AndroidBitmap_lockPixels锁定像素缓冲区,返回直接指针,确保像素读写安全;
  • glReadPixels从OpenGL帧缓冲区读取像素数据,存储到Bitmap缓冲区,格式为RGBA。

2. 垂直翻转像素:交换上下半行

c 复制代码
    int *pIntBuffer = (int *) srcByteBuffer; // 转换为int指针(32位RGBA像素值)

    // 遍历上半部分行,与下半部分行逐列交换像素
    for (i = 0; i < height / 2; i++) {
        for (j = 0; j < width; j++) {
            // 保存下半部分对应位置像素
            int temp = pIntBuffer[(height - i - 1) * width + j];
            // 上半部分像素赋值给下半部分
            pIntBuffer[(height - i - 1) * width + j] = pIntBuffer[i * width + j];
            // 临时像素赋值给上半部分
            pIntBuffer[i * width + j] = temp;
        }
    }
    // 解锁像素缓冲区,允许GC管理,将修改后的像素写回Bitmap
    AndroidBitmap_unlockPixels(jenv, src);
}

代码含义

  • 转换为int*指针:RGBA像素占4字节(32位),int指针可直接访问单个像素值;
  • 垂直翻转逻辑:遍历前半行,将第i行与第height-i-1行像素交换,适配OpenGL与Bitmap的坐标差异;
  • AndroidBitmap_unlockPixels解锁缓冲区,必须调用以释放锁定,确保Bitmap正常使用。
相关推荐
Σίσυφος19002 小时前
张正友标定法原理总结2
人工智能·数码相机·计算机视觉
2501_944526422 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 收藏功能实现
android·java·开发语言·javascript·python·flutter·游戏
2501_944526422 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 个人中心实现
android·java·javascript·python·flutter·游戏
Jomurphys2 小时前
Kotlin - 引用操作符 ::
android·kotlin
恋猫de小郭2 小时前
Meta ShapeR :基于随机拍摄视频的 3D 物体生成,未来的 XR 和机器人基建支持
android·flutter·3d·ai·音视频·xr
2501_9159090611 小时前
如何保护 iOS IPA 文件中资源与文件的安全,图片、JSON重命名
android·ios·小程序·uni-app·json·iphone·webview
Root_Hacker13 小时前
include文件包含个人笔记及c底层调试
android·linux·服务器·c语言·笔记·安全·php
stevenzqzq13 小时前
android flow的背压策略
android·flow
stevenzqzq15 小时前
android mvi接口设计1
android·mvi接口设计