笔记整理于网上其他人的博客和维基百科,以及雷霄骅先生的博客。
。。没想到这才2天访问量就有300差不多,为了方便大家学习,我把我当前的项目用到的所有图片、用到的软件都放在GitHub里面了,方便大家不用去网上找原图,找软件了。
YUV最初提出是为了解决彩色电视和黑白电视的兼容问题,YUV分别为亮度信息(Y)与色彩信息(UV)。YUV比RGB的优势在于不要求三个独立视频信号同时传输,所以占用带宽(频宽)更少。历史原因,YUV和Y’UV通常用来编码电视的模拟信号,而YCbCr则是用来描述数字的影像信号,适合影片与图片压缩以及传输,有时候看到有用Cb和Cr的方式来表示,其实等同于U和V,但应该严格区分YUV和YCbCr这两个专有名词有时并非完全相同,今天大家所讲的YUV其实就是指YCbCr。
YUV种类很多,可以理解为二维的,即“空间-间”,和“空间-内”这样的表述,借鉴了h264中的帧间和帧内的思想。
在理解YUV格式时,时刻记住从bit数、存储结构两方面考察。
YUV Formats格式分为两类:
YUV,分为三个分量,“Y”表示明亮度(Luminance或Luma),也就是灰度值;而“U”和“V” 表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。
YUV码流的存储格式其实与其采样的方式密切相关,主流的采样方式有四种:
U和V组件可以被表示成原始的R,G,和B(R,G,B为γ预校正后的)
Y = 0.299 ∗ R + 0.587 ∗ G + 0.114 ∗ B Y = 0.299 * R + 0.587 * G + 0.114 * B Y=0.299∗R+0.587∗G+0.114∗B
U = − 0.169 ∗ R − 0.331 ∗ G + 0.5 ∗ B + 128 U = -0.169 * R - 0.331 * G + 0.5 * B + 128 U=−0.169∗R−0.331∗G+0.5∗B+128
V = 0.5 ∗ R − 0.419 ∗ G − 0.081 ∗ B + 128 V = 0.5 * R - 0.419 * G - 0.081 * B + 128 V=0.5∗R−0.419∗G−0.081∗B+128
R = Y + 1.13983 ∗ ( V − 128 ) R = Y + 1.13983 * (V - 128) R=Y+1.13983∗(V−128)
G = Y − 0.39465 ∗ ( U − 128 ) − 0.58060 ∗ ( V − 128 ) G = Y - 0.39465 * (U - 128) - 0.58060 * (V - 128) G=Y−0.39465∗(U−128)−0.58060∗(V−128)
B = Y + 2.03211 ∗ ( U − 128 ) B = Y + 2.03211 * (U - 128) B=Y+2.03211∗(U−128)
YUYV(属于YUV422)
相邻的两个Y共用其相邻的两个Cb、Cr,例如对于Y’00和Y’01而言,其Cb、Cr的均值为Cb00、Cr00,其他像素点以此类推。
UYVY(属于YUV422)
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。
NV12(YUV420)
NV12属于YUV420格式,只不过存储方式为先存储Y,再交叉存储U和V,其提取方式与YV12类似,即Y’00、Y’01、Y’10、Y’11共用Cb00、Cr00。
一开始打开yuv图像全是花花绿绿的,还以为是文件坏了,直到看到一句ffplay需要指定yuv图像大小,因为yuv文件不包含宽高数据所以必须用-video_size指定宽和高,这才反应过来yuvplayer需要设置宽高才能正常显示。
通过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图像代码,代码均来自于雷霄骅先生的博客。
运行环境:Windows10、VS2017
分离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。
分离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,分离后效果如下
将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;
}
在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;
}
亮度减半效果如下
通过修改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像素的边框效果图如下
以下函数可以生成一张灰阶测试图。
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 |
PSNR是最基本的视频质量评价方法,对于8bit量化的像素数据来说,计算方法如下:
P S N R = 10 ∗ lg ( 255 M S E ) PSNR = 10 * \lg(\frac{255}{MSE}) PSNR=10∗lg(MSE255)
其中MSE计算方式为:
M S E = 1 M ∗ N ∑ i = 1 M ∑ j = 1 N ( x i j − y i j ) 2 MSE = \frac{1}{M * N}\sum_{i = 1}^M\sum_{j = 1}^N(x_{ij} - y_{ij})^2 MSE=M∗N1i=1∑Mj=1∑N(xij−yij)2
其中M、N代表图像的宽和高, x i j x_{ij} xij和 y i j y_{ij} yij分别为两张图像每一个像素值,用来计算受损图像和原始图像之间的差别,评估受损图像的质量。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素材。
YUV整理弄了两天,代码跟着都敲了一遍,感觉现在格式已经搞懂了, 但是还有不懂得地方:
后面还要做的:
利用ffmpeg搞一个YUV视频分离工具。
我已经看到有人做过了,后面写~~~链接先贴着
https://blog.csdn.net/longjiang321/article/details/103229035