1. 程式人生 > >圖解YU12、I420、YV12、NV12、NV21、YUV420P、YUV420SP、YUV422P、YUV444P的區別

圖解YU12、I420、YV12、NV12、NV21、YUV420P、YUV420SP、YUV422P、YUV444P的區別

概述

YUV模型是根據一個亮度(Y分量)和兩個色度(UV分量)來定義顏色空間,常見的YUV格式有YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420等,其中比較常見的YUV420分為兩種:YUV420P和YUV420SP

我們在android平臺下使用相機預設影象格式是NV21屬於YUV420SP格式

YUV取樣

YUV 4:4:4取樣,每一個Y對應一組UV分量,一個YUV佔8+8+8 = 24bits 3個位元組。
YUV 4:2:2取樣,每兩個Y共用一組UV分量,一個YUV佔8
+4+4 = 16bits 2個位元組。 YUV 4:2:0取樣,每四個Y共用一組UV分量,一個YUV佔8+2+2 = 12bits 1.5個位元組。

我們最常見的YUV420P和YUV420SP都是基於4:2:0取樣的,所以如果圖片的寬為width,高為heigth,在記憶體中佔的空間為width * height * 3 / 2,其中前width * height的空間存放Y分量,接著width * height / 4存放U分量,最後width * height / 4存放V分量

YUV420P(YU12和YV12)格式

YUV420P又叫plane平面模式Y , U , V分別在不同平面,也就是有三個平面,它是YUV標準格式4:2:0

,主要分為:YU12和YV12

  • YU12格式

android平臺下也叫作I420格式,首先是所有Y值,然後是所有U值,最後是所有V值

YU12:亮度(行×列) + U(行×列/4) + V(行×列/4)

  • YV12格式

YV12格式YU12基本相同,首先是所有Y值,然後是所有V值,最後是所有U值。只要注意從適當的位置提取U和V值YU12和YV12都可以使用相同的演算法進行處理。

YV12:亮度Y(行×列) + V(行×列/4) + U(行×列/4)

YU12: YYYYYYYY UUVV    =
> YUV420P YV12: YYYYYYYY VVUU => YUV420P

YUV模型是根據一個亮度(Y分量)和兩個色度(UV分量)來定義顏色空間,常見的YUV格式有YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420等,其中比較常見的YUV420分為兩種:YUV420P和YUV420SP。

我們在android平臺下使用相機預設影象格式是NV21屬於YUV420SP格式## YUV420SP(NV21和NV12)格式

YUV420SP格式的影象陣列,首先是所有Y值,然後是UV或者VU交替儲存,NV12和NV21屬於YUV420SP格式,是一種two-plane模式,即Y和UV分為兩個plane,但是UV(CbCr)為交錯儲存,而不是分為三個平面。

  • NV21格式

android手機從攝像頭採集的預覽資料一般都是NV21,儲存順序是先存Y,再VU交替儲存,NV21儲存順序是先存Y值,再VU交替儲存:YYYYVUVUVU,以 4 X 4 圖片為例子,佔用記憶體為 4 X 4 X 3 / 2 = 24 個位元組

  • NV12格式

NV12與NV21類似,也屬於YUV420SP格式,NV12儲存順序是先存Y值,再UV交替儲存:YYYYUVUVUV,以 4 X 4 圖片為例子,佔用記憶體為 4 X 4 X 3 / 2 = 24 個位元組

注意:在DVD中,色度訊號被儲存成Cb和Cr(C代表顏色,b代表藍色,r代表紅色)

NV12: YYYYYYYY UVUV    =>YUV420SP
NV21: YYYYYYYY VUVU    =>YUV420SP

YUV和RGB轉換

Y      =  (0.257 * R) + (0.504 * G) + (0.098 * B) + 16
Cr = V =  (0.439 * R) - (0.368 * G) - (0.071 * B) + 128
Cb = U = -(0.148 * R) - (0.291 * G) + (0.439 * B) + 128

B = 1.164(Y - 16) + 2.018(U - 128)
G = 1.164(Y - 16) - 0.813(V - 128) - 0.391(U - 128)
R = 1.164(Y - 16) + 1.596(V - 128)

分離YUV420P

下面基於例項來理解Y,U,V分量的作用

先使用ffmpeg將指定的圖片轉為yuv420p格式

ffmpeg -i input.jpg -s 510x510 -pix_fmt yuv420p input.yuv
  • 分離YUV分量

筆者使用的Clion直接執行下面這段程式碼,分離出所需的檔案

void split_yuv420(char *inputPath, int width, int height) {

    FILE *fp_yuv = fopen(inputPath, "rb+");

    FILE *fp_y = fopen("output_420_y.y", "wb+");
    FILE *fp_u = fopen("output_420_u.y", "wb+");
    FILE *fp_v = fopen("output_420_v.y", "wb+");

    unsigned char *data = (unsigned char *) malloc(width * height * 3 / 2);

    fread(data, 1, width * height * 3 / 2, fp_yuv);
    //Y
    fwrite(data, 1, width * height, fp_y);
    //U
    fwrite(data + width * height, 1, width * height / 4, fp_u);
    //V
    fwrite(data + width * height * 5 / 4, 1, width * height / 4, fp_v);

    //釋放資源
    free(data);

    fclose(fp_yuv);
    fclose(fp_y);
    fclose(fp_u);
    fclose(fp_v);
}

筆者使用的是ubuntu系統,因此執行yuvplayer.exe檔案,需要提前安裝好winesudo apt install wine,執行yuvplayer之後,需要先設定畫素格式為Y,否則你看到的影象可能會有問題

先看output_420_y.y檔案:(解析度設定為510x510)

output_420_u.y顯示如下:(解析度設定為255x255)

output_420_v.y顯示如下:(解析度設定為255x255)

  • 生成灰度圖

上面的例子實際上已經生成了一個灰度圖了,但是隻保留了Y分量,你如果直接用ffplay工具檢視會有問題,下面的函式將會生成一個標準的YUV檔案並且保留Y分量,你可能會有疑問,為什麼U分量和V分量要寫入0x80,其實你可以參考上面的YUV轉RGB的公式,YUV資料是無法直接顯示的,最終需要轉成RGB顯示,因此我這裡是只需要保留Y分量,忽略UV分量的影響,因此根據上面的公式,我在Y和U分量中都寫入128就是十六進位制的0x80

  • 保留Y分量(生成灰度圖)
void yuv420p_y(char *inputPath, char *outputPath, int width, int height) {

    FILE *inFile = fopen(inputPath, "rb+");
    FILE *outFile = fopen(outputPath, "wb+");

    unsigned char *data = (unsigned char *) malloc(width * height * 3 / 2);

    fread(data, 1, width * height * 3 / 2, inFile);

    //Y分量
    fwrite(data, 1, width * height, outFile);

    unsigned char *buffer = (unsigned char *) malloc(width * height / 4);
    memset(buffer, 0x80, width * height / 4);
    //U分量
    fwrite(buffer, 1, width * height / 4, outFile);
    //V分量
    fwrite(buffer, 1, width * height / 4, outFile);

    free(buffer);
    free(data);
    fclose(inFile);
    fclose(outFile);
}
int main() {

    yuv420p_y("/home/byhook/media/input.yuv", "/home/byhook/media/output.yuv", 510, 510);

    return 0;
}

使用ffplay來播放yuv格式的檔案:

ffplay -f rawvideo -video_size 510x510 output.yuv

要注意這裡的解析度不能錯

分離YUV422P

YUV422P基於YUV 4:2:2取樣,每兩個Y共用一組UV分量,一個YUV佔8+4+4 = 16bits 2個位元組。分離程式碼如下:

void split_yuv422(char *inputPath, int width, int height) {

    FILE *fp_yuv = fopen(inputPath, "rb+");

    FILE *fp_y = fopen("output_422_y.y", "wb+");
    FILE *fp_u = fopen("output_422_u.y", "wb+");
    FILE *fp_v = fopen("output_422_v.y", "wb+");

    unsigned char *data = (unsigned char *) malloc(width * height * 2);

    fread(data, 1, width * height * 2, fp_yuv);
    //Y
    fwrite(data, 1, width * height, fp_y);
    //U
    fwrite(data + width * height, 1, width * height / 2, fp_u);
    //V
    fwrite(data + width * height * 3 / 2, 1, width * height / 2, fp_v);

    //釋放資源
    free(data);

    fclose(fp_yuv);
    fclose(fp_y);
    fclose(fp_u);
    fclose(fp_v);
}

分離YUV444P

YUV444P基於YUV 4:4:4取樣,每一個Y對應一組UV分量,一個YUV佔8+8+8 = 24bits 3個位元組。分離程式碼如下:

void split_yuv444(char *inputPath, int width, int height) {

    FILE *fp_yuv = fopen(inputPath, "rb+");

    FILE *fp_y = fopen("output_444_y.y", "wb+");
    FILE *fp_u = fopen("output_444_u.y", "wb+");
    FILE *fp_v = fopen("output_444_v.y", "wb+");

    unsigned char *data = (unsigned char *) malloc(width * height * 3);

    fread(data, 1, width * height * 3, fp_yuv);
    //Y
    fwrite(data, 1, width * height, fp_y);
    //U
    fwrite(data + width * height, 1, width * height, fp_u);
    //V
    fwrite(data + width * height * 2, 1, width * height, fp_v);

    //釋放資源
    free(data);

    fclose(fp_yuv);
    fclose(fp_y);
    fclose(fp_u);
    fclose(fp_v);
}

參考:
https://blog.csdn.net/leixiaohua1020/article/details/50534150
https://en.wikipedia.org/wiki/YUV