最近一段時間因爲工作的需要,要使用到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公式如下:
反向推導YUV轉RGB的,得到公式如下:
即得到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上的示例代碼中有其他常用的轉換、縮放、旋轉等方法。有需要的可自行下載。