iOS RGBA轉YV12

引言

由於項目中要作畫面共享,因此須要學一點圖像相關的知識,首當其衝就是RGB轉YUV了,由於圖像處理壓縮這一塊是由專業對口的同事作的,因此呢,我這就是寫一下本身的理解,若有不對的地方,還望指正,謝謝。html

你能夠在這裏看到更好的排版。git

正文

知識準備

RGB

三原色光模式RGB color model),又稱RGB顏色模型紅綠藍顏色模型,是一種加色模型,將Red)、Green)、Blue)三原色的色光以不一樣的比例相加,以合成產生各類色彩光。github

RGB32

RGB32使用32位來表示一個像素,RGB份量各用去8位,剩下的8位用做Alpha通道或者不用。(ARGB32就是帶Alpha通道的RGB24。)注意在內存中RGB各份量的排列順序爲:BGRA BGRA BGRA…。一般可使用RGBQUAD數據結構來操做一個像素,它的定義爲:數組

typedef struct tagRGBQUAD {
BYTE rgbBlue; // 藍色份量
BYTE rgbGreen; // 綠色份量
BYTE rgbRed; // 紅色份量
BYTE rgbReserved; // 保留字節(用做Alpha通道或忽略)
} RGBQUAD。

YUV

YUV,是一種顏色編碼方法。常使用在各個影像處理組件中。 YUV在對照片或影片編碼時,考慮到人類的感知能力,容許下降色度的帶寬。數據結構

YUV是編譯true-color顏色空間(color space)的種類,Y'UV, YUV, YCbCrYPbPr等專有名詞均可以稱爲YUV,彼此有重疊。「Y」表示明亮度(Luminance、Luma),「U」和「V」則是色度濃度(Chrominance、Chroma)。ui

YUV Formats分紅兩個格式:編碼

  • 緊縮格式(packed formats):將Y、U、V值存儲成Macro Pixels數組,和RGB的存放方式相似。
  • 平面格式(planar formats):將Y、U、V的三個份量分別存放在不一樣的矩陣中。

YV12

YV12是每一個像素都提取Y,在UV提取時,將圖像2 x 2的矩陣,每一個矩陣提取一個U和一個V。YV12格式和I420格式的不一樣處在V平面和U平面的位置不一樣。在YV12格式中,V平面緊跟在Y平面以後,而後纔是U平面(即:YVU);但I420則是相反(即:YUV)。NV12與YV12相似,效果同樣,YV12中U和V是連續排列的,而在NV12中,U和V就交錯排列的。spa

排列舉例: 2*2圖像YYYYVU; 4*4圖像YYYYYYYYYYYYYYYYVVVVUUUU。.net

ps:以上介紹摘自於維基百科、百度百科。指針

進入正題

獲取RGBA數據

在這裏主要介紹從Image中獲取RGBA數據,會用到CoreGraphics庫。

首先咱們須要建立bitmap context

+ (CGContextRef) newBitmapRGBA8ContextFromImage:(CGImageRef) image {
    CGContextRef context = NULL;
    CGColorSpaceRef colorSpace;
    uint32_t *bitmapData;
    
    size_t bitsPerPixel = 32; //每個像素 由4個通道構成(RGBA),每個通道都是1個byte,4個通道也就是32個bit
    size_t bitsPerComponent = 8; //能夠理解爲每一個通道的bit數
    size_t bytesPerPixel = bitsPerPixel / bitsPerComponent; //每一個像素點的byte大小
    
    size_t width = CGImageGetWidth(image); 
    size_t height = CGImageGetHeight(image);
    
    size_t bytesPerRow = width * bytesPerPixel; //每一行的字節數
    size_t bufferLength = bytesPerRow * height; //整個buffer的size
    
    colorSpace = CGColorSpaceCreateDeviceRGB(); //指定顏色空間爲RGB
    
    if(!colorSpace) {
        NSLog(@"Error allocating color space RGB\n");
        return NULL;
    }
    
    // 開闢存儲位圖的內存
    bitmapData = (uint32_t *)malloc(bufferLength);
    
    if(!bitmapData) {
        NSLog(@"Error allocating memory for bitmap\n");
        CGColorSpaceRelease(colorSpace);
        return NULL;
    }
    
    // 建立bitmap context
    context = CGBitmapContextCreate(bitmapData, 
                                    width, 
                                    height, 
                                    bitsPerComponent, 
                                    bytesPerRow, 
                                    colorSpace, 
                                    kCGImageAlphaPremultipliedLast);    // RGBA
    
    if(!context) {
        free(bitmapData);
        NSLog(@"Bitmap context not created");
    }
    
  CGColorSpaceRelease(colorSpace);
    return context; 
}

接下來須要向image繪製到bitmap context,而後從context中獲取bitmap data。代碼以下:

+ (unsigned char *) convertUIImageToBitmapRGBA8:(UIImage *) image {
    
    CGImageRef imageRef = image.CGImage;
    
    // 建立bitmap context
    CGContextRef context = [self newBitmapRGBA8ContextFromImage:imageRef];
    
    if(!context) {
        return NULL;
    }
    
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    
    CGRect rect = CGRectMake(0, 0, width, height);
    
    // 將image繪製到bitmap context
    CGContextDrawImage(context, rect, imageRef);
    
    // 獲取bitmap context 中的數據指針
    unsigned char *bitmapData = (unsigned char *)CGBitmapContextGetData(context);
    
    // 拷貝 bitmap text中的數據
    size_t bytesPerRow = CGBitmapContextGetBytesPerRow(context);
    size_t bufferLength = bytesPerRow * height;
    
    unsigned char *newBitmap = NULL;
    
    if(bitmapData) {
        newBitmap = (unsigned char *)malloc(sizeof(unsigned char) * bytesPerRow * height);
        
        if(newBitmap) { // 拷貝數據
            memcpy(newBitmap, bitmapData, bufferLength);
        }
        
        free(bitmapData);
        
    } else {
        NSLog(@"Error getting bitmap pixel data\n");
    }
    
    CGContextRelease(context);
    
    return newBitmap;   
}

看了這段代碼,你可能會有疑問,爲何不直接返回bitmapData呢?這是由於在咱們釋放bitmap context後,會釋放掉bitmapData,因此這就須要咱們重新申請空間將數據拷貝到從新開闢的空間了。

格式化圖像數據

由於YV12要求以像素的2 * 2矩陣來作轉換,因此在作RGB轉換YV12以前,咱們須要先格式化圖像數據,以知足要求。

首先須要格式化圖片size,在這裏咱們以8來對齊,代碼以下:

+ (CGSize)fromatImageSizeToYV12Size:(CGSize)originalSize
{
    CGSize targetSize = CGSizeMake(originalSize.width, originalSize.height);
    
    //除以8是爲了位對齊
    int widthRemainder = (int)originalSize.width % 8;
    
    if (widthRemainder != 0) {
        targetSize.width -= widthRemainder;
    }
    
    int heightRemainder = (int)originalSize.height % 8;
    if (heightRemainder != 0) {
        targetSize.height -= heightRemainder;
    }
    
    return targetSize;
}

在上個步驟咱們計算獲得了知足YV12格式的size,在這裏還須要根據計算到的size來格式化image data,以知足YV12格式要求,代碼以下:

+ (void)formatImageDataToYV12:(unsigned char *)originalData outputData:(unsigned char *)outputData originalWidth:(CGFloat)originalWidth targetSize:(CGSize)targetSize
{
    CGFloat targetHeight = targetSize.height;
    CGFloat targetWidth = targetSize.width;
    
    unsigned char *pTemp = outputData;
    
    //按照targetSize逐行拷貝數據, 乘以4是由於每一個size表示4個通道
    for (int i = 0; i < targetHeight; i++) {
        memcpy(pTemp, originalData, targetWidth * 4);
        originalData += (int)originalWidth * 4;
        pTemp += (int)targetWidth * 4;
    }
}

數據準備好了,接下來,能夠進入真正的主題,RGBA轉換爲YV12了。

RGBA轉YV12

對於RGB轉換爲對應的YV12,轉換規則都是大佬們研究出來的,只是實現的方式各有不一樣,我在這裏羅列了我找到的幾種方式。

void rgb2yuv(int r, int g, int b, int *y, int *u, int *v){
    // 1 常規轉換標準 - 浮點運算,精度高
    *y =  0.29882  * r + 0.58681  * g + 0.114363 * b;
    *u = -0.172485 * r - 0.338718 * g + 0.511207 * b;
    *v =  0.51155  * r - 0.42811  * g - 0.08343  * b;

    // 2 常規轉換標準 經過位移來避免浮點運算,精度低
    *y = ( 76  * r + 150 * g + 29  * b)>>8;
    *u = (-44  * r - 87  * g + 131 * b)>>8;
    *v = ( 131 * r - 110 * g - 21  * b)>>8;
    // 3 常規轉換標準 經過位移來避免乘法運算,精度低
    *y = ( (r<<6) + (r<<3) + (r<<2) + (g<<7) + (g<<4) + (g<<2) + (g<<1) + (b<<4) + (b<<3) + (b<<2) + b)>>8;
    *u = (-(r<<5) - (r<<3) - (r<<2) - (g<<6) - (g<<4) - (g<<2) - (g<<1) - g + (b<<7) + (b<<1) + b)>>8;
    *v = ((r<<7) + (r<<1) + r - (g<<6) - (g<<5) - (g<<3) - (g<<2) - (g<<1) - (b<<4) - (b<<2) - b)>>8;
    
    // 4 高清電視標準:BT.709 常規方法:浮點運算,精度高
    *y =  0.2126  * r + 0.7152  * g + 0.0722  * b;
    *u = -0.09991 * r - 0.33609 * g + 0.436   * b;
    *v =  0.615   * r - 0.55861 * g - 0.05639 * b;
    
    *v += 128;
    *u += 128;
}

以上的轉換方式都是可行的,固然要提高效率的話,還有查表法什麼的,這些有興趣的能夠自行搜索。既然轉換規則固定了,那麼不考慮效率的前提下,咱們須要作的就是如何從rgba數組中按照2 * 2矩陣來獲取YUV數據並存儲下來了。

在這裏介紹採用緊縮格式存儲YV12的數據。爲了便於理解,在這裏先舉個栗子:

下圖中的表示一張圖中的像素排列,每一個像素都包含RGBA通道,將RGBA轉換爲YV12須要按照2 * 2的像素矩陣爲單位來處理,在圖中就是按顏色分塊中的像素來獲取YUV數據,YV12是每4個像素獲取一次U、V份量的數據,每一個像素都要獲取Y份量。

要看轉換後的YUV的樣子,這裏以2 * 2爲例,8 個像素生成的YUV以下:

YYYY YYYY VV UU

有了上面的知識,就能夠直接上代碼看看了。在這裏咱們以數組的方式來存儲yuv數據,輸入的rgba數據咱們設置爲int型數組,由於每一個像素包含4個通道,每一個通道都是一個byte,這樣每一個像素是4個byte,正好是一個int。須要注意的是,在int數組中,按理說每一個int的數據應該是:RGBA,可是在iOS上,因爲iOS是小端,因此實際上每一個int的內容爲ABGR,因此在取數據的時候須要注意,避免弄錯順序。還有須要注意的是,計算出來的UV數據須要避免負數,否則顏色值會有問題。

void rgbaConvert2YV12(int *rgbData, uint8_t *yuv, int width, int height) {
    int frameSize = width * height;
    int yIndex = 0;
    int vIndex = frameSize;
    int uIndex = frameSize * 1.25;

    int R, G, B, Y, U, V, A;
    int index = 0;
    for (int j = 0; j < height; j++) {
        for (int i = 0; i < width; i++) {//RGBA
            //樣式爲ABGR iOS設備爲小端
            A = (rgbData[index] >> 24) & 0xff;
            B = (rgbData[index] >> 16) & 0xff;
            G = (rgbData[index] >> 8) & 0xff;
            R = rgbData[index] & 0xff;
            
            //轉換
            rgb2yuv(R, G, B, &Y, &U, &V);
            //避免負數
            U += 128;
            V += 128;
            
            Y = RANG_CONTROL(Y, 0, 255);
            U = RANG_CONTROL(U, 0, 255);
            V = RANG_CONTROL(V, 0, 255);
            
            yuv[yIndex++] = Y;
            
            if (j % 2 == 0 && i % 2 == 0) {//按2 * 2矩陣取
                yuv[vIndex++] = V;
                yuv[uIndex++] = U;
            }
            index ++;
        }
    }
}

YV12轉RGBA

首先仍是yuv轉rgb的方法,這與RGB轉YV12是對應的。

void yuv2rgb(int y, int u, int v, int *r, int *g, int *b){
    u -= 128;
    v -= 128;
    // 1 常規轉換標準 - 浮點運算,精度高
    *r = y                  + (1.370705 * v);
    *g = y - (0.337633 * u) - (0.698001 * v);
    *b = y + (1.732446 * u);

    // 2 常規轉換標準 經過位移來避免浮點運算,精度低
    *r = ((256 * y             + (351 * v))>>8);
    *g = ((256 * y - (86  * u) - (179 * v))>>8);
    *b = ((256 * y + (444 * u))            >>8);
    // 3 常規轉換標準 經過位移來避免乘法運算,精度低
    *r = (((y<<8) + (v<<8) + (v<<6) + (v<<4) + (v<<3) + (v<<2) + (v<<1) + v)                   >> 8);
    *g = (((y<<8) - (u<<6) - (u<<4) - (u<<2) - (u<<1) - (v<<7) - (v<<5) - (v<<4) - (v<<1) - v) >> 8);
    *b = (((y<<8) + (u<<8) + (u<<7) + (u<<5) + (u<<4) + (u<<3) + (u<<2))                       >> 8);
    // 4 高清電視標準:BT.709 常規方法:浮點運算,精度高
    *r = (y               + 1.28033 * v);
    *g = (y - 0.21482 * u - 0.38059 * v);
    *b = (y + 2.12798 * u);
}

重點來了,將YUV數組恢復爲RGBA數組

void YV12Convert2RGB(uint8_t *yuv, uint8_t *rgb, int width, int height){
    int frameSize = width * height;
    int rgbIndex = 0;
    int yIndex = 0;
    int uvOffset = 0;
    int vIndex = frameSize;
    int uIndex = frameSize * 1.25;
    int R, G, B, Y, U, V;
    
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            Y = yuv[yIndex++];
            uvOffset = i / 2 * width / 2 + j / 2;//按2 * 2矩陣 還原
            
            V = yuv[vIndex + uvOffset];
            U = yuv[uIndex + uvOffset];
            
            yuv2rgb(Y, U, V, &R, &G, &B);
            
            R = RANG_CONTROL(R, 0, 255);
            G = RANG_CONTROL(G, 0, 255);
            B = RANG_CONTROL(B, 0, 255);
            
            rgb[rgbIndex++] = R;
            rgb[rgbIndex++] = G;
            rgb[rgbIndex++] = B;
            rgb[rgbIndex++] = 255;
        }
    }
}

以上就是iOS中RGB與YUV互轉的方式了,你能夠在這裏下載demo

參考

三元光模式-維基百科

RGB百度百科

IOS rgb yuv 轉換

YUV和RGB互相轉換及OpenGL顯示YUV數據

YUV顏色編碼解析

YUV與RGB格式轉化

相關文章
相關標籤/搜索