1、YUV简介
YUV格式有两大类:planar和packed。
对于planar的YUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V。
对于packed的YUV格式,每个像素点的Y,U,V是连续交*存储的。
YUV,分为三个分量,“Y”表示明亮度(Luminance或Luma),也就是灰度值;而“U”和“V” 表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。
与我们熟知的RGB类似,YUV也是一种颜色编码方法,主要用于电视系统以及模拟视频领域,它将亮度信息(Y)与色彩信息(UV)分离,没有UV信息一样可以显示完整的图像,只不过是黑白的,这样的设计很好地解决了彩色电视机与黑白电视的兼容问题。并且,YUV不像RGB那样要求三个独立的视频信号同时传输,所以用YUV方式传送占用极少的频宽。
2、YUV存储格式
YUV码流的存储格式其实与其采样的方式密切相关,主流的采样方式有三种,YUV4:4:4,YUV4:2:2,YUV4:2:0,关于其详细原理,可以通过网上其它文章了解,这里我想强调的是如何根据其采样格式来从码流中还原每个像素点的YUV值,因为只有正确地还原了每个像素点的YUV值,才能通过YUV与RGB的转换公式提取出每个像素点的RGB值,然后显示出来。
用三个图来直观地表示采集的方式吧,以黑点表示采样该像素点的Y分量,以空心圆圈表示采用该像素点的UV分量。
先记住下面这段话,以后提取每个像素的YUV分量会用到。
1. YUV 4:4:4采样,每一个Y对应一组UV分量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个字节。
3、YUV420类型
3.1) YUV420p和YUV420sp区别
因为YUV420比较常用, 在这里就重点介绍YUV420。YUV420分为两种:YUV420p和YUV420sp。
YUV420sp格式如下图:
YUV420p数据格式如下图:
3.2) YUV420p和YUV420sp具体分类和详情
YUV420p:又叫planer平面模式,Y ,U,V分别再不同平面,也就是有三个平面。
YUV420p又分为:他们的区别只是存储UV的顺序不一样而已。
I420:又叫YU12,安卓的模式。存储顺序是先存Y,再存U,最后存V。YYYYUUUVVV
YV12:存储顺序是先存Y,再存V,最后存U。YYYVVVUUU
YUV420sp:又叫bi-planer或two-planer双平面,Y一个平面,UV在同一个平面交叉存储。
YUV420sp又分为:他们的区别只是存储UV的顺序不一样而已。
NV12:IOS只有这一种模式。存储顺序是先存Y,再UV交替存储。YYYYUVUVUV
NV21:安卓的模式。存储顺序是先存Y,再存U,再VU交替存储。YYYYVUVUVU
官方文档如下:
YV12
All of the Y samples appear first in memory as an array of unsigned char values. This array is followed immediately by all of the V (Cr) samples. The stride of the V plane is half the stride of the Y plane, and the V plane contains half as many lines as the Y plane. The V plane is followed immediately by all of the U (Cb) samples, with the same stride and number of lines as the V plane (Figure 12).
大致意思是:先存储完所有的Y,后面紧跟着存V,V的步长(也就是宽)是Y的步长的一半,V的行高是Y的一半。V存储完后面紧跟着存U,所有的U,步长河行高和V相同,也就是都是Y的一半。
Figure 12:
NV12
All of the Y samples are found first in memory as an array of unsigned char values with an even number of lines. The Y plane is followed immediately by an array of unsigned char values that contains packed U (Cb) and V (Cr) samples, as shown in Figure 13. When the combined U-V array is addressed as an array of little-endian WORD values, the LSBs contain the U values, and the MSBs contain the V values. NV12 is the preferred 4:2:0 pixel format for DirectX VA. It is expected to be an intermediate-term requirement for DirectX VA accelerators supporting 4:2:0 video.
Figure 13:
3.3)YUV420的内存计算
width * hight =Y(总和)
U = Y / 4 V = Y / 4
所以YUV420 数据在内存中的长度是 width * hight * 3 / 2(即一个YUV是1.5个字节),所以计算采集的数据大小:width * hight * 1.5*frame*time
以720×488大小图象YUV420 planar为例,
其存储格式是: 共大小为720×480×3 × 1.5字节,
分为三个部分:Y,U和V
Y分量: (720×480)个字节
U(Cb)分量:(720×480 × 1/4)个字节
V(Cr)分量:(720×480 × 1/4)个字节
三个部分内部均是行优先存储,三个部分之间是Y,U,V 顺序存储。
即YUV数据的0--720×480字节是Y分量值,
720×480--720×480×5/4字节是U分量
720×480×5/4 --720×480×3/2字节是V分量。
一般来说,直接采集到的视频数据是RGB24的格式,RGB24一帧的大小size=width×heigth×3 Bit,RGB32的size=width×heigth×4,YUV标准格式4:2:0 的数据量是 size=width×heigth×1.5 Bit。
在采集到RGB24数据后,需要对这个格式的数据进行第一次压缩。即将图像的颜色空间由RGB2YUV。因为,X264在进行编码的时候需要标准的YUV(4:2:0)。
经过第一次数据压缩后RGB24->YUV(I420)。这样,数据量将减少一半,经过X264编码后,数据量将大大减少。将编码后的数据打包,通过RTP实时传送。到达目的地后,将数据取出,进行解码。完成解码后,数据仍然是YUV格式的,所以,还需要一次转换,就是YUV2RGB24。
YUVI420转换到NV12代码实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/**
* @brief 将I420格式转换为NV12格式
* @param i420_src 输入的I420数据指针(只读)
* @param nv12_dst 输出的NV12数据指针(预分配内存)
* @param width 图像宽度(像素)
* @param height 图像高度(像素)
* @return 0成功,-1参数错误
*
* I420存储结构:
* YYYYYYYY
* YYYYYYYY
* UUUU
* VVVV
*
* NV12存储结构:
* YYYYYYYY
* YYYYYYYY
* UVUVUVUV
*/
int I420_to_NV12(const unsigned char* i420_src,
unsigned char* nv12_dst,
int width,
int height)
{
// 参数有效性检查
if (!i420_src || !nv12_dst || width <= 0 || height <= 0) {
fprintf(stderr, "Error: Invalid parameters\n");
return -1;
}
// 检查宽高是否为偶数(YUV420要求)
if ((width % 2 != 0) || (height % 2 != 0)) {
fprintf(stderr, "Error: Width and height must be even\n");
return -1;
}
const int y_size = width * height;
const int uv_size = y_size / 4;
// 拷贝Y分量(直接内存复制)
memcpy(nv12_dst, i420_src, y_size);
// 获取UV分量指针
const unsigned char* u_src = i420_src + y_size; // U平面起始位置
const unsigned char* v_src = i420_src + y_size + uv_size; // V平面起始位置
unsigned char* uv_dst = nv12_dst + y_size; // NV12的UV平面起始位置
// UV分量交错处理
const int half_width = width >> 1;
const int half_height = height >> 1;
for (int row = 0; row < half_height; ++row) {
for (int col = 0; col < half_width; ++col) {
// 计算目标内存偏移(交错存储)
const int dst_offset = row * width + (col << 1);
// 获取对应位置的UV值
const int uv_index = row * half_width + col;
uv_dst[dst_offset] = u_src[uv_index]; // U分量
uv_dst[dst_offset + 1] = v_src[uv_index];// V分量
}
}
return 0;
}
/‌********************** 使用示例 **********************‌/
int main()
{
// 假设4x4图像
const int w = 4, h = 4;
// I420数据分配(Y + U + V)
unsigned char* i420 = malloc(w*h + (w*h)/2);
// NV12数据分配(Y + UV交错)
unsigned char* nv12 = malloc(w*h + (w*h)/2);
// 生成测试数据(示例)
memset(i420, 0x7F, w*h); // Y分量全为0x7F
memset(i420 + w*h, 0x80, (w*h)/4); // U分量全为0x80
memset(i420 + w*h + (w*h)/4, 0x90, (w*h)/4); // V分量全为0x90
// 执行转换
if (I420_to_NV12(i420, nv12, w, h) == 0) {
printf("Conversion success!\n");
// 验证第一个UV对
printf("First UV pair: 0x%02X 0x%02X\n",
nv12[w*h], nv12[w*h + 1]);
}
free(i420);
free(nv12);
return 0;
}
结果分析
# 原I420格式 (4x4图像)
Y分量 (16字节):
[Y][Y][Y][Y]
[Y][Y][Y][Y]
[Y][Y][Y][Y]
[Y][Y][Y][Y]
U分量 (4字节):
[U][U]
[U][U]
V分量 (4字节):
[V][V]
[V][V]
# 转换后NV12格式
Y分量 (16字节):
[Y][Y][Y][Y]
[Y][Y][Y][Y]
[Y][Y][Y][Y]
[Y][Y][Y][Y]
UV交错平面 (8字节):
[U][V][U][V]
[U][V][U][V]