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上的示例代碼中有其他常用的轉換、縮放、旋轉等方法。有需要的可自行下載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章