音視頻學習: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格式分爲兩類:
- 平面格式(planar formats):先連續存儲所有像素點的Y,緊接着存儲所有像素點的U,隨後是所有像素點的V
- 緊縮格式(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
- RGB轉換爲YUV
YUV數據的存儲方式
-
YUYV(屬於YUV422)
相鄰的兩個Y共用其相鄰的兩個Cb、Cr,例如對於Y’00和Y’01而言,其Cb、Cr的均值爲Cb00、Cr00,其他像素點以此類推。
-
UYVY(屬於YUV422)
相鄰的兩個Y共用其相鄰的兩個Cb、Cr,只不過和上面的YUYV不同的是Cb和Cr的順序不同,其他像素點以此類推。
-
YUV422P
YUV422P的P表示Planar formats(平面格式),也就是說YUV不是交錯存儲而是先存Y,再存U和V,對於Y’00和Y’01而言,其Cb、Cr的均值爲Cb00、Cr00。
-
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(x264,XviD,DivX),DVD-Video存儲格式MPEG-2,MPEG-1以及MJPEG。
YU12則和YV12除存儲方式略有不同外,其他類似,存儲方式是先存儲Y,再存儲U,再存儲V。
-
NV12(YUV420)
NV12屬於YUV420格式,只不過存儲方式爲先存儲Y,再交叉存儲U和V,其提取方式與YV12類似,即Y’00、Y’01、Y’10、Y’11共用Cb00、Cr00。
NV21和NV12略有不同,先存儲Y,再交叉存儲V和U。
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,如下圖:
解析後圖片分爲3個分量,Y、U、V,使用yuvplayer,在Color選項卡中選中分量Y,首先查看Y分量,尺寸爲512*512。
U、V如下圖,尺寸爲256*256。
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,分離後效果如下
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;
}
處理結果如下:
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;
}
亮度減半效果如下
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像素的邊框效果圖如下
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,效果如下圖。
各個灰度條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量化的像素數據來說,計算方法如下:
其中MSE計算方式爲:
其中M、N代表圖像的寬和高,和分別爲兩張圖像每一個像素值,用來計算受損圖像和原始圖像之間的差別,評估受損圖像的質量。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:
未完待續
YUV整理弄了兩天,代碼跟着都敲了一遍,感覺現在格式已經搞懂了, 但是還有不懂得地方:
- 爲什麼U、V分量的無色是128?
- 如果像素不是4的倍數,那麼YUV是怎麼存儲的?
- YUV4:2:0這些數字的解釋是啥,雖然上面有總結到,但是還是一知半解?
後面還要做的:
-
利用ffmpeg搞一個YUV視頻分離工具。
我已經看到有人做過了,後面寫~~~鏈接先貼着
https://blog.csdn.net/longjiang321/article/details/103229035