從零開始學習音視頻編程技術(十五) YUV420P轉RGB32

原文地址:http://blog.yundiantech.com/?log=blog&id=19

上一節講解了YUV420P格式的內容。

我說過,我們不是爲了做研究。 平白無故講了YUV420P的理論知識,要是不做點什麼總說不過去吧。 那麼,我們就練練刀,寫個播放YUV420P的程序吧,將前面保存的YUV420P圖像用自己寫的播放器播放出來。


這裏我們還是一樣使用Qt來顯示圖像。


之前做播放器的時候,是將YUV420P轉換成RGB32,然後放到QImage裏面顯示出來。

由於Qt不支持直接顯示YUV的圖像。

同樣,這裏我們也是先將YUV420P轉成RGB32。

不過,這次不是用FFMPEG來轉,畢竟我們的根本目的不是爲了寫播放器,只是爲了更加了解yuv420p,然後順帶寫下這個播放器。 


同樣一件事,目的不同,做法也就不同。

因此,我們通過自己寫代碼來實現YUV420P轉RGB32。


YUV420P已經瞭解了,現在還差RGB32,那就先看看RGB32吧:


R代表red、G代表green、B代表blue。 他們的取值都是0~255。

所以每一個分量剛好又可以用一個字節來記錄了。


這裏還需要介紹下ARGB32,RGB就是代表RGB了,A代表的是Alpha(透明度)。

因此一個ARGB32就是有4個分量:A R G B。剛好就是4x8=32位了。


可能是爲了兼容性,RGB32的存儲方式和ARGB32是一樣的,只是RGB32的A分量不存數據,

換句話說就是: ARGB32就是帶Alpha通道的RGB32。


下面是我百度到的RGB32的介紹:

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

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


這裏需要注意的就是:他的排列順序是  B G R A。


YUV轉RGB有一個公式,如下:

YUV(256 級別) 可以從8位 RGB 直接計算:

Y = 0.299 R + 0.587 G + 0.114 B

U = - 0.1687 R - 0.3313 G + 0.5 B + 128

V = 0.5 R - 0.4187 G - 0.0813 B + 128

反過來,RGB 也可以直接從YUV (256級別) 計算:

R = Y + 1.402 (V-128)

G = Y - 0.34414 (U-128) - 0.71414 (V-128)

B = Y + 1.772 (U-128)


當然,百度一下會發現YUV轉RGB的公式還有好多,至於這些公式是怎麼得出來的,本人也是沒有頭緒,經過測試,上面這個公式效果相對好一些。

現在已經掌握了YUV420P的存儲方式和RGB32的存儲方式,同時也知道了YUV轉RGB的公式,要轉換也就不難了。


先來回顧下YUV420P的格式:


好了,現在對照這個圖,應該很容易就能寫出來轉換的代碼了:

1.爲了更加方便的寫代碼,我們把RGB定義成一個結構體:

1
2
3
4
5
6
7
8
typedef unsigned char BYTE;
 
typedef struct RGB32 {
  BYTE    rgbBlue;      // 藍色分量
  BYTE    rgbGreen;     // 綠色分量
  BYTE    rgbRed;       // 紅色分量
  BYTE    rgbReserved;  // 保留字節(用作Alpha通道或忽略)
} RGB32;


2.轉換思路也很清晰了,只需要讀取出每一個YUV的像素,然後轉換成RGB就行了,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void Yuv2Rgb::Yuv420p2Rgb32(const BYTE *yuvBuffer_in,const BYTE *rgbBuffer_out,int width,int height)
{
    BYTE *yuvBuffer = (BYTE *)yuvBuffer_in;
    RGB32 *rgb32Buffer = (RGB32 *)rgbBuffer_out;
 
    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            int index = y * width + x;
 
            int indexY = y * width + x;
            int indexU = width * height + y / 2 * width / 2 + x / 2;
            int indexV = width * height + width * height / 4 + y / 2 * width / 2 + x / 2;
 
            BYTE Y = yuvBuffer[indexY];
            BYTE U = yuvBuffer[indexU];
            BYTE V = yuvBuffer[indexV];
 
            RGB32 *rgbNode = &rgb32Buffer[index];
 
            ///這轉換的公式 百度有好多 下面這個效果相對好一些
 
            rgbNode->rgbRed = Y + 1.402 * (V-128);
            rgbNode->rgbGreen = Y - 0.34413 * (U-128) - 0.71414*(V-128);
            rgbNode->rgbBlue = Y + 1.772*(U-128);
        }
    }
}


注:上述代碼僅僅是爲了實現功能,並沒有考慮任何效率問題。

同時爲了提高可讀性,將各種Index都分開計算了。



main函數中如下調用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>#include <string.h>
#include <stdlib.h>
#include <iostream>
#include <windows.h>
using namespace std;
 
#include "yuv2rgb.h"
 
int main()
{
    cout << "Hello World!" << endl;
 
 
    FILE *fp_yuv = fopen("in.yuv","rb");
    FILE *fp_rgb = fopen("out.rgb32","wb");
 
    int width = 1920;
    int height = 1080;
 
    int yuvSize = width * height * 3 /2;
    int rgbSize = width * height * sizeof(RGB32);
 
    BYTE *yuvBuffer = (BYTE *)malloc(yuvSize);
    BYTE *rgbBuffer = (BYTE *)malloc(rgbSize);
 
    Yuv2Rgb yuv2Rgb;
 
    DWORD start = GetTickCount();
 
    for(int i=0;;i++)
    {
        if (feof(fp_yuv)) break;
 
        int readedsize = fread(yuvBuffer,1,yuvSize,fp_yuv);
 
        DWORD t1 = GetTickCount();
        yuv2Rgb.Yuv420p2Rgb32(yuvBuffer,rgbBuffer,width,height);
        DWORD t2 = GetTickCount();
 
        fprintf(stderr,"%d use time = %ld ms
",i,t2-t1);
 
        fwrite(rgbBuffer,1,rgbSize,fp_rgb);
 
    }
 
    DWORD end = GetTickCount();
 
    fprintf(stderr,"Finished! use time = %ld ms
",end-start);
 
    return 0;
}


稍等片刻,轉換就能結束。

將生成的out.rgb32用yuvplayer打開:

yuvplayer下載地址:http://download.csdn.net/detail/qq214517703/9637191


注:記得在選擇文件的時候,將右下角的類型改成所有格式,否則會看不到這個文件:


文件打開之後,如下設置:


發現可以正常播放了,說明轉換成功了。



還別高興的太早,再仔細看下我們的代碼,上面加了打印轉換每張圖片耗費的時間,我的電腦執行後得到如下結果:


可以看出,每張圖片都發了大概三四十毫秒的時間,100張圖片總共發了5秒鐘!

這個速度也太慢了,如果後面再加上編碼,那還了得。

顯然這個速度必須要優化下。



回過頭看下我們轉換部分的代碼:

發現嵌套了2個循環,因爲需要讀取每一個像素然後轉換成RGB,所以這個循環優化的空間就不大了。

再看看循環裏面的內容:

1
2
3
rgbNode->rgbRed = Y + 1.402 * (V-128);            
rgbNode->rgbGreen = Y - 0.34413 * (U-128) - 0.71414*(V-128);
rgbNode->rgbBlue = Y + 1.772*(U-128);

全是乘法運算,還是浮點數。看來這個就是罪魁禍首了!

雖然一次乘法運算對於電腦的CPU根本不算什麼,但是這裏是1920x1080次,這個就相當可怕了!看來積少成多還是有點道理的。


自古以來,就流行犧牲空間換取時間的傳統,這裏當然也不例外。

前面說過Y U V每個分量的取值都是0~255。那麼他們與某個小數相乘之後的結果也就只有256種結果。既然乘法操作是非常耗時的,那麼我們就把相乘的結果先計算好,放入一個表中,要用的時候從這個表裏面去取,這樣就可以快很多了。


首先定義一個數組,用來存放計算好的結果:

1
2
3
unsigned short R_Y[COLORSIZE],R_U[COLORSIZE],R_V[COLORSIZE];    
unsigned short G_Y[COLORSIZE],G_U[COLORSIZE],G_V[COLORSIZE];
unsigned short B_Y[COLORSIZE],B_U[COLORSIZE],B_V[COLORSIZE];


然後在寫一個初始化的函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//表的初始化
void Yuv2Rgb::table_init()
{
    int i;
    for(i = 0; i < COLORSIZE; i++)
    {
        ///R = Y + 1.402 * (V-128);
        //R_Y[i] = 0; //沒有
        //R_U[i] = 0; //沒有
        R_V[i] = 1.402 * (i-128);
 
        ///G = Y - 0.34413 * (U-128) - 0.71414*(V-128);
        //G_Y[i] = 0;
        G_U[i] = 0.34413 * (i-128);
        G_V[i] = 0.71414 * (i-128);
 
        /// = Y + 1.772*(U-128);
        //B_Y[i] = 0;
        B_U[i] = 1.772 * (i-128);
        //B_V[i] = 0;
    }
}


這裏採用代碼的方式來初始化表,而不是直接將最終的結果寫入。是因爲:

  1. 這樣就幾句代碼,可讀性強。

  2. 初始化函數我們只需要執行一次,因此這個時間是可以接受的。

  3. 後期要修改轉換的算法也簡單方便。


轉換函數如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
///這個是優化的版本
void Yuv2Rgb::Yuv420p2Rgb32(const BYTE *yuvBuffer_in,const BYTE *rgbBuffer_out,int width,int height)
{
    BYTE *yuvBuffer = (BYTE *)yuvBuffer_in;
    RGB32 *rgb32Buffer = (RGB32 *)rgbBuffer_out;
 
    int w_h = width * height;//width * height的值
    int w_h_4 = width * height / 4;//width * height / 4 的值
 
    for (int y = 0; y < height; y++)
    {
        int y_width = y * width; //y乘以width的值
        int y_width_2_2 = y / 2 * width / 2; //y / 2 * width / 2
 
        for (int x = 0; x < width; x++)
        {
            int index = y_width + x;
 
            int x_2 = x/2;
 
            int indexU = w_h + y_width_2_2 + x_2;
            int indexV = w_h + w_h_4 + y_width_2_2 + x_2;
 
            BYTE Y = yuvBuffer[index];
            BYTE U = yuvBuffer[indexU];
            BYTE V = yuvBuffer[indexV];
 
            RGB32 *rgbNode = &rgb32Buffer[index];
 
            ///這轉換的公式 百度有好多 下面這個效果相對好一些
 
            rgbNode->rgbRed = Y + R_V[V];
            rgbNode->rgbGreen = Y - G_U[U] - G_V[V];
            rgbNode->rgbBlue = Y + B_U[U];
 
        }
    }
}



調用的方式和之前一樣,但別忘了初始化表:

1
2
Yuv2Rgb yuv2Rgb;    
yuv2Rgb.table_init(); //執行一次表的初始化


這樣優化後的效果比直接稍微快了一些(100張圖片大概快了一秒多一點),雖然還不是很理想,不過仔細一想1000張就能快10秒了,勉強有點用。

目前我只能想到這樣的優化方法了,如果要效率高些,可以直接使用ffmpeg來轉換,我們實際中基本上也是直接使用ffmpeg來操作的。


FFMPEG轉換的代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    int yuvSize = width * height * 3 /2;    
    BYTE *yuvBuffer = (BYTE *)malloc(yuvSize);
 
    AVFrame *pFrame = av_frame_alloc();
    AVFrame *pFrameRGB = av_frame_alloc();
    int numBytes = avpicture_get_size(PIX_FMT_RGB32, width,height);
 
    uint8_t * rgbBuffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
    avpicture_fill((AVPicture *) pFrameRGB, rgbBuffer, PIX_FMT_RGB32,width, height);
 
    avpicture_fill((AVPicture *) pFrame, yuvBuffer, AV_PIX_FMT_YUV420P, width, height);
 
    SwsContext *img_convert_ctx = sws_getContext(width, height, AV_PIX_FMT_YUV420P, width, height, PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL);
 
    int rgbSize = numBytes;
     
     
    for(int i=0;;i++)
    {
        if (feof(fp_yuv)) break;
 
        int readedsize = fread(yuvBuffer,1,yuvSize,fp_yuv);
 
        DWORD t1 = GetTickCount();
 
        sws_scale(img_convert_ctx,
                (uint8_t const const *) pFrame->data,
                pFrame->linesize, 0, height, pFrameRGB->data,
                pFrameRGB->linesize);
 
 
        DWORD t2 = GetTickCount();
 
        fprintf(stderr,"%d use time = %ld ms
",i,t2-t1);
 
        fwrite(rgbBuffer,1,rgbSize,fp_rgb);
    }


會發現FFMPEG速度快好多,並且轉換後的效果也好很多。。哈哈。。果然還是FFMPEG好用。

本文的目的主要是爲了加深對YUV420P的認識,因此我們自己的代碼就不去完善它了。


yuv420p轉rgb完整工程:http://download.csdn.net/detail/qq214517703/9642041


加上了Qt顯示功能的完整工程(Qt播放YUV420P文件):

http://download.csdn.net/detail/qq214517703/9642365


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