概述
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采樣
1 YUV 4:4:4采樣,每一個Y對應一組UV分量,一個YUV占8+8+8 = 24bits 3個字節。 2 YUV 4:2:2采樣,每兩個Y共用一組UV分量,一個YUV占8+4+4 = 16bits 2個字節。 3 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)
1 YU12: YYYYYYYY UUVV => YUV420P 2 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代表紅色)
1 NV12: YYYYYYYY UVUV =>YUV420SP 2 NV21: YYYYYYYY VUVU =>YUV420SP
YUV和RGB轉換
1 Y = (0.257 * R) + (0.504 * G) + (0.098 * B) + 16
2 Cr = V = (0.439 * R) - (0.368 * G) - (0.071 * B) + 128
3 Cb = U = -(0.148 * R) - (0.291 * G) + (0.439 * B) + 128
4
5 B = 1.164(Y - 16) + 2.018(U - 128) 6 G = 1.164(Y - 16) - 0.813(V - 128) - 0.391(U - 128) 7 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
直接運行下面這段代碼,分離出所需的文件
1 void split_yuv420(char *inputPath, int width, int height) { 2
3 FILE *fp_yuv = fopen(inputPath, "rb+"); 4
5 FILE *fp_y = fopen("output_420_y.y", "wb+"); 6 FILE *fp_u = fopen("output_420_u.y", "wb+"); 7 FILE *fp_v = fopen("output_420_v.y", "wb+"); 8
9 unsigned char *data = (unsigned char *) malloc(width * height * 3 / 2); 10
11 fread(data, 1, width * height * 3 / 2, fp_yuv); 12 //Y
13 fwrite(data, 1, width * height, fp_y); 14 //U
15 fwrite(data + width * height, 1, width * height / 4, fp_u); 16 //V
17 fwrite(data + width * height * 5 / 4, 1, width * height / 4, fp_v); 18
19 //釋放資源
20 free(data); 21
22 fclose(fp_yuv); 23 fclose(fp_y); 24 fclose(fp_u); 25 fclose(fp_v); 26 }
筆者使用的是ubuntu系統
,因此運行yuvplayer.exe
文件,需要提前安裝好wine
:sudo 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分量(生成灰度圖)
1 void yuv420p_y(char *inputPath, char *outputPath, int width, int height) { 2
3 FILE *inFile = fopen(inputPath, "rb+"); 4 FILE *outFile = fopen(outputPath, "wb+"); 5
6 unsigned char *data = (unsigned char *) malloc(width * height * 3 / 2); 7
8 fread(data, 1, width * height * 3 / 2, inFile); 9
10 //Y分量
11 fwrite(data, 1, width * height, outFile); 12
13 unsigned char *buffer = (unsigned char *) malloc(width * height / 4); 14 memset(buffer, 0x80, width * height / 4); 15 //U分量
16 fwrite(buffer, 1, width * height / 4, outFile); 17 //V分量
18 fwrite(buffer, 1, width * height / 4, outFile); 19
20 free(buffer); 21 free(data); 22 fclose(inFile); 23 fclose(outFile); 24 }
1 int main() { 2
3 yuv420p_y("/home/byhook/media/input.yuv", "/home/byhook/media/output.yuv", 510, 510); 4
5 return 0; 6 }
使用ffplay
來播放yuv格式
的文件:
ffplay -f rawvideo -video_size 510x510 output.yuv
要注意這里的分辨率不能錯
分離YUV422P
YUV422P
基於YUV 4:2:2
采樣,每兩個Y共用一組UV分量,一個YUV占8+4+4 = 16bits 2個字節。分離代碼如下:
1 void split_yuv422(char *inputPath, int width, int height) { 2
3 FILE *fp_yuv = fopen(inputPath, "rb+"); 4
5 FILE *fp_y = fopen("output_422_y.y", "wb+"); 6 FILE *fp_u = fopen("output_422_u.y", "wb+"); 7 FILE *fp_v = fopen("output_422_v.y", "wb+"); 8
9 unsigned char *data = (unsigned char *) malloc(width * height * 2); 10
11 fread(data, 1, width * height * 2, fp_yuv); 12 //Y
13 fwrite(data, 1, width * height, fp_y); 14 //U
15 fwrite(data + width * height, 1, width * height / 2, fp_u); 16 //V
17 fwrite(data + width * height * 3 / 2, 1, width * height / 2, fp_v); 18
19 //釋放資源
20 free(data); 21
22 fclose(fp_yuv); 23 fclose(fp_y); 24 fclose(fp_u); 25 fclose(fp_v); 26 }
分離YUV444P
YUV444P
基於YUV 4:4:4
采樣,每一個Y對應一組UV分量,一個YUV占8+8+8 = 24bits 3個字節。分離代碼如下:
1 void split_yuv444(char *inputPath, int width, int height) { 2
3 FILE *fp_yuv = fopen(inputPath, "rb+"); 4
5 FILE *fp_y = fopen("output_444_y.y", "wb+"); 6 FILE *fp_u = fopen("output_444_u.y", "wb+"); 7 FILE *fp_v = fopen("output_444_v.y", "wb+"); 8
9 unsigned char *data = (unsigned char *) malloc(width * height * 3); 10
11 fread(data, 1, width * height * 3, fp_yuv); 12 //Y
13 fwrite(data, 1, width * height, fp_y); 14 //U
15 fwrite(data + width * height, 1, width * height, fp_u); 16 //V
17 fwrite(data + width * height * 2, 1, width * height, fp_v); 18
19 //釋放資源
20 free(data); 21
22 fclose(fp_yuv); 23 fclose(fp_y); 24 fclose(fp_u); 25 fclose(fp_v); 26 }