音視頻學習:YUV

YUV

筆記整理於網上其他人的博客和維基百科,以及雷霄驊先生的博客

。。沒想到這才2天訪問量就有300差不多,爲了方便大家學習,我把我當前的項目用到的所有圖片、用到的軟件都放在GitHub裏面了,方便大家不用去網上找原圖,找軟件了。

基本概念

YUV最初提出是爲了解決彩色電視和黑白電視的兼容問題,YUV分別爲亮度信息(Y)與色彩信息(UV)。YUV比RGB的優勢在於不要求三個獨立視頻信號同時傳輸,所以佔用帶寬(頻寬)更少。歷史原因,YUV和Y’UV通常用來編碼電視的模擬信號,而YCbCr則是用來描述數字的影像信號,適合影片與圖片壓縮以及傳輸,有時候看到有用Cb和Cr的方式來表示,其實等同於U和V,但應該嚴格區分YUV和YCbCr這兩個專有名詞有時並非完全相同,今天大家所講的YUV其實就是指YCbCr。

YUV種類很多,可以理解爲二維的,即“空間-間”,和“空間-內”這樣的表述,借鑑了h264中的幀間和幀內的思想。

  • 空間-間:不同空間,即描述一個像素的bit數不同,如YUV444,、YUV422、YUV411、YUV420
  • 空間-內:相同空間,即描述一個像素的bit數相同,但存儲方式不同,比如對於YUV420而言,又可以細分爲YUV420P、YUV420SP、NV21、NV12、YV12、YU12,I420

在理解YUV格式時,時刻記住從bit數、存儲結構兩方面考察。

YUV Formats格式分爲兩類:

  1. 平面格式(planar formats):先連續存儲所有像素點的Y,緊接着存儲所有像素點的U,隨後是所有像素點的V
  2. 緊縮格式(packed formats):每個像素點的Y、U、V是連續交叉存儲的

YUV,分爲三個分量,“Y”表示明亮度(Luminance或Luma),也就是灰度值;而“U”和“V” 表示的則是色度(Chrominance或Chroma),作用是描述影像色彩及飽和度,用於指定像素的顏色。

YUV碼流的存儲格式其實與其採樣的方式密切相關,主流的採樣方式有四種:

  • YUV444:4:4:4表示完全取樣。
  • YUV422:4:2:2表示2:1的水平取樣,垂直完全採樣。
  • YUV420:4:2:0表示2:1的水平取樣,垂直2:1採樣。
  • YUV411:4:1:1表示4:1的水平取樣,垂直完全採樣。

YUV和RGB互相轉換

U和V組件可以被表示成原始的R,G,和B(R,G,B爲γ預校正後的)

  • YUV轉換爲RGB

Y=0.299R+0.587G+0.114B Y = 0.299 * R + 0.587 * G + 0.114 * B

U=0.169R0.331G+0.5B+128 U = -0.169 * R - 0.331 * G + 0.5 * B + 128

V=0.5R0.419G0.081B+128 V = 0.5 * R - 0.419 * G - 0.081 * B + 128

  • RGB轉換爲YUV

R=Y+1.13983(V128) R = Y + 1.13983 * (V - 128)

G=Y0.39465(U128)0.58060(V128) G = Y - 0.39465 * (U - 128) - 0.58060 * (V - 128)

B=Y+2.03211(U128) B = Y + 2.03211 * (U - 128)

YUV數據的存儲方式

  1. YUYV(屬於YUV422)

    相鄰的兩個Y共用其相鄰的兩個Cb、Cr,例如對於Y’00和Y’01而言,其Cb、Cr的均值爲Cb00、Cr00,其他像素點以此類推。
    yuyv

  2. UYVY(屬於YUV422)

    相鄰的兩個Y共用其相鄰的兩個Cb、Cr,只不過和上面的YUYV不同的是Cb和Cr的順序不同,其他像素點以此類推。
    uyvy

  3. YUV422P

    YUV422P的P表示Planar formats(平面格式),也就是說YUV不是交錯存儲而是先存Y,再存U和V,對於Y’00和Y’01而言,其Cb、Cr的均值爲Cb00、Cr00。
    yuv422P

  4. YV12(屬於YUV420)

    YV12屬於YUV420,也是Planar formats(平面格式),存儲方式是先存儲Y,再存儲V,再存儲U,4個Y分量共用一組UV,所以下圖中Y’00、Y’01、Y’10、Y’11共用Cr00、Cb00。

    許多重要的編碼器都採用YV12空間存儲視頻:MPEG-4(x264XviDDivX),DVD-Video存儲格式MPEG-2,MPEG-1以及MJPEG。

    YU12則和YV12除存儲方式略有不同外,其他類似,存儲方式是先存儲Y,再存儲U,再存儲V。
    yv12

  5. NV12(YUV420)

    NV12屬於YUV420格式,只不過存儲方式爲先存儲Y,再交叉存儲U和V,其提取方式與YV12類似,即Y’00、Y’01、Y’10、Y’11共用Cb00、Cr00。

    NV21和NV12略有不同,先存儲Y,再交叉存儲V和U。
    nv12

yuvplayer查看YUV圖像

一開始打開yuv圖像全是花花綠綠的,還以爲是文件壞了,直到看到一句ffplay需要指定yuv圖像大小,因爲yuv文件不包含寬高數據所以必須用-video_size指定寬和高,這才反應過來yuvplayer需要設置寬高才能正常顯示。

  • Size->Custom->修改寬高
  • Color->選擇對應的YUV格式

ffmpeg轉換及查看YUV圖像

通過ffmpeg利用原始測試圖片,來得到YUV420P的圖像:

./ffmpeg -i ./originnal_pic/lena512color.tiff -pix_fmt yuv420p ./YUV/lena512_yuv420p.yuv

通過ffplay顯示YUV圖像:

./ffplay.exe -video_size 512*512 ./YUV/lena512_yuv420p.yuv

YUV Parser

簡單的解析YUV圖像代碼,代碼均來自於雷霄驊先生的博客

運行環境:Windows10、VS2017

1. 分離YUV420P像素數據中的Y、U、V分量

分離YUV420P的Y、U、V保存爲3個文件。

bool YuvParser::yuv420_split(const std::string input_url, int width, int height, int frame_num)
{
    FILE *input_file = fopen(input_url.c_str(), "rb+");
    FILE *output_y = fopen("output_420_y.y", "wb+");
    FILE *output_u = fopen("output_420_u.y", "wb+");
    FILE *output_v = fopen("output_420_v.y", "wb+");

    unsigned char *picture = new unsigned char[width * height * 3 / 2];

    for (int i = 0; i < frame_num; i++) {
        fread(picture, 1, width * height * 3 / 2, input_file);
        
        fwrite(picture, 1, width * height, output_y);
        fwrite(picture + width * height, 1, width * height / 4, output_u);
        fwrite(picture + width * height * 5 / 4, 1, width * height / 4, output_v);
    }

    delete[] picture;
    fclose(input_file);
    fclose(output_y);
    fclose(output_u);
    fclose(output_v);
    return true;
}

解析出來的圖像需要用yuvplayer查看,原始圖片爲512*512,如下圖:

original

解析後圖片分爲3個分量,Y、U、V,使用yuvplayer,在Color選項卡中選中分量Y,首先查看Y分量,尺寸爲512*512。

y

output_420_y.yuv

U、V如下圖,尺寸爲256*256。

output_420_u.yuv output_420_v.yuv

2. 分離YUV444P像素數據中的Y、U、V分量

分離YUV444P的Y、U、V保存爲3個文件。

bool YuvParser::yuv444_split(const std::string input_url, int width, int height, int frame_num)
{
    FILE *input_file = fopen(input_url.c_str(), "rb+");
    FILE *output_y = fopen("output_444_y.y", "wb+");
    FILE *output_u = fopen("output_444_u.y", "wb+");
    FILE *output_v = fopen("output_444_v.y", "wb+");

    unsigned char *picture = new unsigned char[width * height * 3];

    for (int i = 0; i < frame_num; i++) {
        fread(picture, 1, width * height * 3, input_file);
        
        fwrite(picture, 1, width * height, output_y);
        fwrite(picture + width * height, 1, width * height, output_u);
        fwrite(picture + width * height * 2, 1, width * height, output_v);
    }

    delete[] picture;
    fclose(input_file);
    fclose(output_y);
    fclose(output_u);
    fclose(output_v);
    return true;
}

原圖依舊是lena標準圖,用ffmpeg轉換爲YUV444P,分離後效果如下

y

output_444_y.yuv

u

output_444_u.yuv

v

output_444_v.yuv

3. 將YUV420P像素數據去掉顏色(灰度圖)

將YUV420P格式像素數據的彩色去掉,變成純粹的灰度圖。

bool YuvParser::yuv420_gray(const std::string input_url, int width, int height, int frame_num)
{
    FILE *input_file = fopen(input_url.c_str(), "rb+");
    FILE *output_gray = fopen("output_420_gray.yuv", "wb+");

    unsigned char *picture = new unsigned char[width * height * 3 / 2];

    for (int i = 0; i < frame_num; i++) {
        fread(picture, 1, width * height * 3 / 2, input_file);
        memset(picture + width * height, 128, width * height / 2);
        fwrite(picture, 1, width * height * 3 / 2, output_gray);
    }

    delete[] picture;
    fclose(input_file);
    fclose(output_gray);
    return true;
}

處理結果如下:
gray

output_420_gray.yuv

4. 將YUV420P像素數據的亮度減半

在YUV中Y代表亮度,所以只需要將Y減半,圖像便會出現亮度減半。

bool YuvParser::yuv420_half_bright(const std::string input_url, int width, int height, int frame_num)
{
    FILE *input_file = fopen(input_url.c_str(), "rb+");
    FILE *output_half_bright = fopen("output_420_half_bright.yuv", "wb+");

    unsigned char *picture = new unsigned char[width * height * 3 / 2];

    for (int i = 0; i < frame_num; i++) {
        fread(picture, 1, width * height * 3 / 2, input_file);
        for (int cur_pixel = 0; cur_pixel < width * height; cur_pixel++) {
            // half Y
            picture[cur_pixel] /= 2;
        }
        fwrite(picture, 1, width * height * 3 / 2, output_half_bright);
    }

    delete[] picture;
    fclose(input_file);
    fclose(output_half_bright);
    return true;
}

亮度減半效果如下

half_bright

output_420_half_bright.yuv

5. 將YUV420P像素數據周圍加上邊框

通過修改YUV數據中特定位置的亮度分量Y的數值,將Y值調到最亮(255),給圖像添加一個“邊框”的效果。

bool YuvParser::yuv420_border(const std::string input_url, int width, int height, int border_length, int frame_num)
{
    FILE *input_file = fopen(input_url.c_str(), "rb+");
    FILE *output_border = fopen("output_420_border.yuv", "wb+");

    unsigned char *picture = new unsigned char[width * height * 3 / 2];

    for (int i = 0; i < frame_num; i++) {
        fread(picture, 1, width * height * 3 / 2, input_file);
        for (int cur_height = 0; cur_height < height; cur_height++) {
            for (int cur_width = 0; cur_width < width; cur_width++) {
                if (cur_width < border_length || cur_width > width - border_length ||
                    cur_height < border_length || cur_height > height - border_length) {
                    picture[cur_height * width + cur_width] = 255;
                }
            }
        }
        fwrite(picture, 1, width * height * 3 / 2, output_border);
    }
    
    delete[] picture;
    fclose(input_file);
    fclose(output_border);
    return true;
}

20像素的邊框效果圖如下

border

output_420_border.yuv

6. 生成YUV420P格式的灰階測試圖

以下函數可以生成一張灰階測試圖。

bool YuvParser::yuv420_graybar(int width, int height, int y_min, int y_max, int bar_num)
{
    FILE *output_graybar = fopen("output_420_graybar.yuv", "wb+");

    unsigned char *picture = new unsigned char[width * height * 3 / 2];
    
    if (bar_num == 1 && y_max != y_min) {
        return false;
    }

    float luma_range = (float)(y_max - y_min) / (float)(bar_num > 1 ? bar_num - 1 : bar_num);
    unsigned char cur_luma = y_min;
    int cur_block = 0;
    int bar_width = width / bar_num;

    // write Y
    for (int cur_height = 0; cur_height < height; cur_height++) {
        for (int cur_width = 0; cur_width < width; cur_width++) {
            cur_block = (cur_width / bar_width == bar_num) ? (bar_num - 1) : (cur_width / bar_width);
            cur_luma = y_min + (unsigned char)(cur_block * luma_range);
            picture[cur_height * width + cur_width] = cur_luma;
        }
    }

    // NOTE: write U and write V can use memset to set,
    //       write them separately to make them easier
    //       to understand
    
    // write U
    for (int cur_height = 0; cur_height < height / 2; cur_height++) {
        for (int cur_width = 0; cur_width < width / 2; cur_width++) {
            picture[height * width + cur_height * width / 2 + cur_width] = 128;
        }
    }

    // write V
    for (int cur_height = 0; cur_height < height / 2; cur_height++) {
        for (int cur_width = 0; cur_width < width / 2; cur_width++) {
            picture[height * width * 5 / 4 + cur_height * width / 2 + cur_width] = 128;
        }
    }

    fwrite(picture, 1, width * height * 3 / 2, output_graybar);

    delete[] picture;
    fclose(output_graybar);
    return true;
}

簡單學雷神測試下10階灰階測試圖,寬1024像素,高512,效果如下圖。

output_420_graybar.png

output_420_graybar.yuv

各個灰度條Y、U、V值如下

Y U V
0 128 128
28 128 128
56 128 128
85 128 128
113 128 128
141 128 128
170 128 128
198 128 128
226 128 128
255 128 128

7. 計算兩個YUV420P像素數據的PSNR

PSNR是最基本的視頻質量評價方法,對於8bit量化的像素數據來說,計算方法如下:
PSNR=10lg(255MSE) PSNR = 10 * \lg(\frac{255}{MSE})
其中MSE計算方式爲:
MSE=1MNi=1Mj=1N(xijyij)2 MSE = \frac{1}{M * N}\sum_{i = 1}^M\sum_{j = 1}^N(x_{ij} - y_{ij})^2
其中M、N代表圖像的寬和高,xijx_{ij}yijy_{ij}分別爲兩張圖像每一個像素值,用來計算受損圖像和原始圖像之間的差別,評估受損圖像的質量。PSNR取值通常情況下都在20-50的範圍內,取值越高,代表兩張圖像越接近,反映出受損圖像質量越好。

bool YuvParser::yuv420_psnr(const std::string input_url1, const std::string input_url2, int width, int height, int frame_num)
{
    FILE *input_file1 = fopen(input_url1.c_str(), "rb+");
    FILE *input_file2 = fopen(input_url2.c_str(), "rb+");

    unsigned char *picture1 = new unsigned char[width * height * 3 / 2];
    unsigned char *picture2 = new unsigned char[width * height * 3 / 2];

    for (int i = 0; i < frame_num; i++) {
        fread(picture1, 1, width * height * 3 / 2, input_file1);
        fread(picture2, 1, width * height * 3 / 2, input_file2);

        double mse_total = 0, mse = 0, psnr = 0;
        for (int cur_pixel = 0; cur_pixel < width * height; cur_pixel++) {
            mse_total += pow((double)(picture1[cur_pixel] - picture2[cur_pixel]), 2);
        }
        mse = mse_total / (width * height);
        psnr = 10 * log10(255.0 * 255.0 / mse);
        printf("frame_num=%d psnr=%5.3f\n", frame_num, psnr);

        // Skip the UV component
        fseek(input_file1, width * height / 2, SEEK_CUR);
        fseek(input_file2, width * height / 2, SEEK_CUR);
    }

    delete[] picture1;
    delete[] picture2;
    fclose(input_file1);
    fclose(input_file2);
    return true;
}

由於我不曉得怎麼讓圖片損壞,所以圖片選用雷神的256*256的lena素材。

結果如下,爲26.693:
psnr

output_420_psnr.yuv

未完待續

YUV整理弄了兩天,代碼跟着都敲了一遍,感覺現在格式已經搞懂了, 但是還有不懂得地方:

  1. 爲什麼U、V分量的無色是128?
  2. 如果像素不是4的倍數,那麼YUV是怎麼存儲的?
  3. YUV4:2:0這些數字的解釋是啥,雖然上面有總結到,但是還是一知半解?

後面還要做的:

  1. 利用ffmpeg搞一個YUV視頻分離工具。

    我已經看到有人做過了,後面寫~~~鏈接先貼着

    https://blog.csdn.net/longjiang321/article/details/103229035

發佈了90 篇原創文章 · 獲贊 97 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章