RGBA、YUV色彩格式及libyuv的使用

最近一段时间因为工作的需要,要使用到libyuv。因为之前写录制视频的时候,也要用到rgb转yuv,自己结合网上的资料做了个实现,记录了点笔记,现在索性一起整理下。

常用的色彩格式

常见的色彩格式主要分为两类,一类是RGBA系列,一类是YUV系列。

RGBA系列

首先就是rgba系列的格式,RGBA色彩主要用于色彩的显示和描述。常见的有RGBA/ARGB/BGRA/ABGR/RGB/BGR。这些格式都比较好理解了。R、G、B、A分别表示红绿蓝及透明通道。
以RGBA为例,就是4个字节表示一个颜色值,排列方式就是RGBARGBARGBA这样排列。而RGB,就是三个字节表示一个颜色值,没有透明通道,排列方式就是RGBRGBRGB。在通常的视频中,也是没有透明通道的(也有例外,比如MOV格式,是可以包含透明通道的)。所以当RGBA编码为视频色彩时,A是会被丢掉的。
当然,上面说的,是将每个色彩都用一个字节来表示的情况。RGBA也有RGBA_8888,RGBA_4444,RGB565等等众多格式,也就是并不是每个颜色都用一个字节来表示。以RGB565为例,是用两个字节来表示RGB三个色彩,R占5位,G占6位,B占5位。RGB565与RGB24相比,色彩上稍有损失,一般情况下,不细致对比,不容易发现这个损失,但是内存上会节约1/3的大小。

YUV系列

YUV主要用于优化彩色视频信号的传输,相比RGBA色彩来说,YUV格式占用更少的内存。YUV系列的格式,与RGBA一样,也是五花八门,常见的有YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420、YUV422等等。Y表示亮度,U、V都表示色度。如果只有Y分量,没有UV分量,那么得到的就是黑白灰度图像。
与YUV类似的还有YCrCb,YIQ等色彩格式。YIQ模型与YUV模型类似,用于NTSC制式的电视系统。YIQ颜色空间中的I和Q分量相当于将YUV空间中的UV分量做了一个33度的旋转。而YCrCb是YUV的一种派生色彩,Y包含了绿色色度和亮度,Cr表示红色色度,Cb表示蓝色色度。
我们在实际使用时,遇到最多的大概就是NV21、NV12、YUV420P、YUV420SP、I420等这些格式。他们有什么区别呢?
实际上I420就是标准的YUV420P,以4*4的图像来说,YUV排列顺序为YYYYYYYYYYYYYYYYUUUUVVVV。YUV大小分别为4*4、2*2、2*2。
Y1Y2Y7Y8U1V1 可以表示四个像素点,其他的同色区域一样,都是表示四个像素点,像素位置与Y对应。
借用Wiki上的图片表示下:
这里写图片描述
NV21为标准的YUV420SP,以4*4的图像来说,YUV排列顺序为YYYYYYYYYYYYYYYYUVUVUVUV。Y大小分别为4*4、UV大小为4*2。如图:
这里写图片描述
NV12与NV21类似,也是YUV420SP,只是排列顺序上UV换了个边,变为YYYYYYYYYYYYYYYYVUVUVUVU。
当然像YUV411,YUV420对比,差异主要在于采样点上。YUV虽然格式众多,但是使用起来也是大同小异。更多可参考Wiki上的YUV介绍

常用RGB与YUV之间的转换

很多时候,我们在网上找RGB转YUV格式或者YUV转RGB格式的转换公式时,总会得到不一样的公式,让我们无法选择,不知道哪个是正确的。实际上,RGB转YUV或者YUV转RGB的确会有不同的公式。这是由于不同的标准以及转换校正造成的。我们利用RGB转成YUV来传输,然后显示时又需要被还原成RGB。
根据BT.601标准(SDTV,标清),定义参数如下:

Wr=0.299
Wb=0.114
Wg=1-Wr-Wb=0.587
Umax=0.436
Vmax=0.615

RGB转YUV公式如下:
Y=WrR+WgG+WbB=0.299R+0.587G+0.114B
U=UmaxBY1Wb0.492(BY)
V=VmaxRY1Wr0.877(RY)

反向推导YUV转RGB的,得到公式如下:

R=Y+1.14V
G=Y0.395U0.581V
B=Y+2.033U

即得到RGB和YUV根据BT.601标准的公式为:

#RGB转YUV
#[Y]        [0.299  0.587   0.114   ][R]
#[U]    =   [-0.147 -0.289  0.436   ][G]
#[V]        [0.615  -0.515  -0.100  ][B]
Y = 0.299 R + 0.587 G + 0.114 B
U = -0.147 R - 0.289 G + 0.436 B 
V = 0.615 R - 0.515 G - 0.100 B
#YUV转RGB
#[R]        [1  0       1.140   ][Y]
#[G]    =   [1  -0.395  -0.581  ][U]
#[B]        [1  2.032   0       ][V]
R = Y + 1.402 V
G= Y - 0.395 U - 0.581V
B= Y + 2.032U

# 在老式的非SIMD体系结构中,浮点运算慢与定点运算,所以变换下:
# RGB转YUV,studio-swing,Y的范围为[16-235],UV的范围为[16-240]
Y = ((66R+129G+25B+128)>>8)+16
U = ((-38R-74G+112B+128)>>8)+128
V = ((112R-94G-18B+128)>>8)+128

# RGB转YUV,full-swing,YUV的范围都为[0-255]
Y = (77R+150G+29B+128)>>8
U = (-43R-84G+127B+128)>>8)+128
V = ((127R-106G-21B+128)>>8)+128

# YUV转RGB
C = Y-16
D = U-128
E = V-128
R = clamp((298*C + 409 * E +128)>>8)
G = clamp((298*C - 100* D - 208* E+ 128)>>8)
B = clamp((298*C + 516* D- 128)>>8)

RGBA转YUV具体实现,可参考Android视频编码——RGBA、RGB、BGRA、BGR转YUV420P、YUV420SP

libyuv的使用

libyuv提供了非常方面好用的色彩空间转换、旋转、缩放的功能,转换效率也非常高。如果有色彩空间转换、旋转、缩放等功能的需求,不妨使用此库来完成。在使用libyuv进行转换或者自己写代码进行转换的过程中,可以使用RawGfx这个软件,来查看你的转换是否正确,无论是RGBA格式还是YUV格式的原始数据,都可以用它进行查看。

libyuv的jni封装

在libyuv中,提供了非常丰富的方法,我们实际使用时往往只需要使用到其中的一小部分。为了在Android中调用libyuv,我们需要编写Jni代码,调用libyuv中的方法。
以RGBA转I420为例,libyuv中提供了许多不同的方法来针对RGBA、ARGB、RGB565等等一系列的不同的RGBA格式转I420格式的方法。为了简化我们的工作我们可以做一个简单的封装来实现Java层调用RGBA转I420的方法。
我们可以先写出一个native的接口,来表述我们需要的功能:


public class YuvUtils {

    //rgba也会有很多类型,所以我们加一个type的值,来表示rgba是什么类型
    //也可以用直接写一个rgba转yuv的,rgba和yuv类型都不固定,用type来表示所有类型的rgba到yuv的转换
    public static native int RgbaToI420(int type,byte[] rgba,byte[] yuv,int width,int height);

}

然后就是编写Jni代码了。
Jni代码中定义了一个函数指针数组,包含将会对Java提供的RGBA转I420的类型,值得注意的是在Java层传入byte[]以RGBA顺序排列时,libyuv是用ABGR来表示这个排列,如果期望传入的数据是RGBA排列,使用libyuv是用libyuv::RGBAToI420这个方法,得到的YUV数据将是错误的数据。

#include <assert.h>
#include "libyuv.h"
#include "jni.h"
#include "android/log.h"

#define YUV_UTILS_JAVA "com/wuwang/libyuv/YuvUtils"

#ifdef __cplusplus
extern "C" {
#endif

static int (*rgbaToI420Func[])(const uint8 *,int,uint8 *,int,uint8 *,int ,uint8 *,int,int,int)={
    libyuv::ABGRToI420,libyuv::RGBAToI420,libyuv::ARGBToI420,libyuv::BGRAToI420,
    libyuv::RGB24ToI420,libyuv::RGB565ToI420
};

int rgbaToI420(JNIEnv * env,jclass clazz,jbyteArray rgba,jint rgba_stride,
                jbyteArray yuv,jint y_stride,jint u_stride,jint v_stride,
                jint width,jint height,
                int (*func)(const uint8 *,int,uint8 *,int,uint8 *,int ,uint8 *,int,int,int)){
    size_t ySize=(size_t) (y_stride * height);
    size_t uSize=(size_t) (u_stride * height >> 1);
    jbyte * rgbaData= env->GetByteArrayElements(rgba,JNI_FALSE);
    jbyte * yuvData=env->GetByteArrayElements(yuv,JNI_FALSE);
    int ret=func((const uint8 *) rgbaData, rgba_stride, (uint8 *) yuvData, y_stride,
                 (uint8 *) (yuvData) + ySize, u_stride, (uint8 *) (yuvData )+ ySize + uSize,
                 v_stride, width, height);
    env->ReleaseByteArrayElements(rgba,rgbaData,JNI_OK);
    env->ReleaseByteArrayElements(yuv,yuvData,JNI_OK);
    return ret;
}

int Jni_RgbaToI420(JNIEnv * env,jclass clazz,jint type,jbyteArray rgba,jbyteArray yuv,jint width,jint height){
    uint8 cType=(uint8) (type & 0x0F);
    int rgba_stride= ((type & 0xF0) >> 4)*width;
    int y_stride=width;
    int u_stride=width>>1;
    int v_stride=u_stride;
    return rgbaToI420(env,clazz,rgba,rgba_stride,yuv,y_stride,u_stride,v_stride,width,height,rgbaToI420Func[cType]);
}

//libyuv中,rgba表示abgrabgrabgr这样的顺序写入文件,java使用的时候习惯rgba表示rgbargbargba写入文件
static JNINativeMethod g_methods[]={
        {"RgbaToI420","(I[B[BII)I",   (void *)Jni_RgbaToI420},
        //.... 其他方法映射
};

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JNIEnv* env = nullptr;

    if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
        return JNI_ERR;
    }
    assert(env != nullptr);
    jclass clazz=env->FindClass(YUV_UTILS_JAVA);
    env->RegisterNatives(clazz, g_methods, (int) (sizeof(g_methods) / sizeof((g_methods)[0])));

    return JNI_VERSION_1_4;
}

JNIEXPORT void JNI_OnUnload(JavaVM *jvm, void *reserved){
    JNIEnv* env = nullptr;

    if (jvm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
        return;
    }
    jclass clazz=env->FindClass(YUV_UTILS_JAVA);
    env->UnregisterNatives(clazz);
}


#ifdef __cplusplus
}
#endif

传入的type,被每4位表示一个具体的意义。从低位到高位,0-3 表示转换类型,4-7 表示rgba_stride的宽度的倍数,8-11 表示yuv_stride宽度移位数,12-15 表示uv左移位数。
根据Jni代码中对于type各位的解析与使用,定义出类型转换的几个常量如下。这样我们就封装了一个通用的rgba转I420的方法了。

public final class Key {

    private Key(){};

    //0-3 表示转换类型
    //4-7 表示rgba_stride的宽度的倍数
    //8-11 表示yuv_stride宽度移位数
    //12-15 表示uv左移位数

    public static final int RGBA_TO_I420=0x01001040;
    public static final int ABGR_TO_I420=0x01001041;
    public static final int BGRA_TO_I420=0x01001042;
    public static final int ARGB_TO_I420=0x01001043;
    public static final int RGB24_TO_I420=0x01001034;
    public static final int RGB565_TO_I420=0x01001025;

}

封装后的方法使用与检验

封装后的方法使用也比较简单,直接获取一个Bitmap,然后将Bitmap中的rgba数据copy出来,进行转换就可以了,转换完的结果保存到文件中,然后用RawGfx来检查下转换的结果是否正确。

Bitmap bitmap= BitmapFactory.decodeResource(getResources(),R.mipmap.bg);
width=bitmap.getWidth();
height=bitmap.getHeight();
File file=new File(getExternalFilesDir(null).getAbsolutePath()+"/cache.yuv");
OutputStream os = new FileOutputStream(file);
ByteBuffer buffer=ByteBuffer.allocate(bitmap.getWidth()*bitmap.getHeight()*4);
bitmap.copyPixelsToBuffer(buffer);
byte[] yuvData=new byte[bitmap.getWidth()*bitmap.getHeight()*3/2];
YuvUtils.RgbaToI420(Key.RGBA_TO_I420,buffer.array(),yuvData,bitmap.getWidth(),bitmap.getHeight());
//rgbToYuv(buffer.array(),bitmap.getWidth(),bitmap.getHeight(),yuvData);
Log.e("wuwang","width*height:"+bitmap.getWidth()+"/"+bitmap.getHeight());
os.write(yuvData);
os.flush();
os.close();

如下所示,是使用libyuv中的libyuv::RGBAToI420转换得到的结果:
这里写图片描述

由于上面说过,libyuv表示的排列顺序和Bitmap的RGBA表示的顺序是反向的。所以实际要调用libyuv::ABGRToI420才能得到正确的结果。

这里写图片描述

示例代码下载

libyuv提供了丰富的功能,其他功能使用与这个差不多,在github上的示例代码中有其他常用的转换、缩放、旋转等方法。有需要的可自行下载。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页