目录
yuv数据介绍:
yuv444
yuv420
yuv422
Opengl显示yuv数据
提取数据
创建纹理
shader显示
yuv数据介绍:
yuv数据存储主要分3大种:yuv420、yuv444、yuv422
图中:空心圆表示 一个uv, 实心圆表示一个y。
下面对三种格式进行一个解释:cb代表u,cr代表v。大部分数据格式再 ffmpeg的枚举类型 AVPixelFormat中都有介绍,可以拿来参考。
yuv444
yuv就是一个y对应一个uv,大多是yuv444p
- yuv444p p表示plane模式,先存储y,然后存储u、再存储v 。
yuv420
yuv420 只是表示 4个y 共一个uv,存储方式多种(下图中:16个y 和 4个 u 4个 v,图像是 y :4x4 , uv : 2x2, 每一行可能有像素补充):
- yuv420p: p表示plane模式,先存储y,然后存储u、再存储v 如:yyyyyyyyyyyyyyyy uuuu vvvv
- yu12: 也是一种plane模式,如:yyyyyyyyyyyyyyyy uuuu vvvv
- yv12:也是一种plane模式,如:yyyyyyyyyyyyyyyy vvvv uuuuuu
- nv12:y是单独存储,uv混合存储,但第一个为u:如:yyyyyyyyyyyyyyyy uvuvuvuv
- nv12:y是单独存储,uv混合存储,但第一个为v:如:yyyyyyyyyyyyyyyy uvuvuvuv
yuv422
yuv422 一行中连续2个y共用一个uv,存储方式多种:
- YUV422P:p表示plane模式,存储如:yyyyyyyyyyyyyyyy uuuuuuuu vvvvvvvv(图像是 y :4x4 , uv : 2x4, 每一行可能有像素补充)
- YUYV422 :名称代表了存储的顺序,2个y 之间夹杂一个u后面添加一个v。如:Y0 Cb Y1 Cr
- UYVY422:名称代表了存储的顺序,如: Cb Y0 Cr Y1
Opengl显示yuv数据
提取数据
上一篇FFMpeg opengl显示解码avframe以 yuv420p 为例,讲述了从avframe中提取像素数据,其实这些数据都是根据上面格式的存储方式来提取的。这里给出nv12的提取方式(nv12和nv21的提取方式一样,只是uv的顺序不同)
void getNV12Data(AVFrame *avFrame)
{
int w = avFrame->width;
int h = avFrame->height;
if(y == nullptr){
y = new unsigned char[w*h];
}
if(u == nullptr){
u = new unsigned char[w*h/2];
}
LOGE("avFrame->linesize[0] = %d" ,avFrame->linesize[0]);
LOGE("avFrame->linesize[1] = %d" ,avFrame->linesize[1]);
int l1 = avFrame->linesize[0];
int l2 = avFrame->linesize[1];
for(int i= 0 ; i < h ; i++)
{
memcpy(y + w*i,avFrame->data[0] + l1* i, sizeof( unsigned char)*w);
}
for(int i= 0 ; i < h/2 ; i++)
{
memcpy(u + w*i,avFrame->data[1] + l2 * i, sizeof( unsigned char)*w);
}
}
创建纹理
熟悉opengl都知道,通过上面提取的yuv数据 创建纹理,然后在shader中对yuv的数据转化为rgb 然后渲染显示。问题就在于纹理如何创建,下面给出创建函数和绘制函数:
void GLProgram::GetTexture(unsigned int index,int width,int height, unsigned char *buf, bool isa)
{
unsigned int format =GL_LUMINANCE; // 这里是 灰度图,单通道
if(isa)
format = GL_LUMINANCE_ALPHA; // 这表示的是 带 alpha通道的单通道图,即 2通道,r 和 a
if(yuvTexture[index] == 0)
{
//材质初始化
glGenTextures(1,&yuvTexture[index]);
LOGE("创建纹理yuvTexture[index] = %d ", yuvTexture[index]);
//设置纹理属性
glBindTexture(GL_TEXTURE_2D,yuvTexture[index]);
//缩小的过滤器
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
//设置纹理的格式和大小
glTexImage2D(GL_TEXTURE_2D,
0, //细节基本 0默认
format,//gpu内部格式 亮度,灰度图
width,height, //拉升到全屏
0, //边框
format,//数据的像素格式 亮度,灰度图 要与上面一致
GL_UNSIGNED_BYTE, //像素的数据类型
NULL //纹理的数据
);
}
//激活第1层纹理,绑定到创建的opengl纹理
glActiveTexture(GL_TEXTURE0+index);
glBindTexture(GL_TEXTURE_2D,yuvTexture[index]);
//替换纹理内容
glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width,height,format,GL_UNSIGNED_BYTE,buf);
}
void GLProgram::Draw(int width , int height,unsigned char * y, unsigned char *u, unsigned char *v,AVPixelFormat avPixelFormat, int* showSize)
{
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT |GL_STENCIL_BUFFER_BIT);
glViewport(showSize[0],showSize[1], showSize[2], showSize[3]);
glUseProgram(program);
setUniform();
glUniform1i(formatTypeLocation, avPixelFormat);
if(avPixelFormat == AV_PIX_FMT_YUV420P)
{
GetTexture(0,width,height,y, false); // Y
glUniform1i(yTextureLocation,0);
GetTexture(1,width/2,height/2,u,false); // U
glUniform1i(uTextureLocation,1);
GetTexture(2,width/2,height/2,v,false); // V
glUniform1i(vTextureLocation,2);
}
else if(avPixelFormat == AV_PIX_FMT_NV12 || avPixelFormat == AV_PIX_FMT_NV21)
{
GetTexture(0,width,height,y,false); // Y
glUniform1i(yTextureLocation,0);
GetTexture(1,width/2,height/2,u,true); // Uv
glUniform1i(uTextureLocation,1);
}
//三维绘制
glDrawArrays(GL_TRIANGLE_STRIP,0,4);
glDisableVertexAttribArray(aPositionLocation);
glDisableVertexAttribArray(aTexCoordLocation);
}
1、fomat代表创建纹理的类型,通常有RGB、RGBA,当然我们传入的是yuv数据,uv的数据与y不同,且数据得一行一行取出来,因为可能有补充像素。因此,这里format的选择就很重要。
- yuv420p的时候,yuv的数据分开的,uv的纹理大小与y不同,因此 创建三个纹理: y、u、v,并且都是单通道GL_LUMINANCE:表示 灰度图,单通道
- nv12 或者 nv21 的时候,因为uv的数据是柔和在一起的,因此创建2个纹理:y, u,创建y的时候format选择GL_LUMINANCE,而 创建uv的format为GL_LUMINANCE_ALPHA :表示的是带 alpha通道的灰度图,即有2个通道,包含r和 a。
shader显示
yuv转rgb的方法:
shader
关于顶点shader 和 纹理坐标的对应关系,之前已经讲过,这里只说下yuv数据转化为rgb数据的方法,这里先给出片元着色器shader:
#define GET_STR(x) #x
//片元着色器,软解码和部分x86硬解码
static const char *fragYUV420P = GET_STR(
precision mediump float; //精度
varying vec2 vTexCoord; //顶点着色器传递的坐标
uniform sampler2D yTexture; //输入的材质(不透明灰度,单像素)
uniform sampler2D uTexture;
uniform sampler2D vTexture;
uniform int formatType;
void main(){
vec3 yuv;
vec3 rgb;
if(formatType==1)//yuv420p
{
yuv.r = texture2D(yTexture,vTexCoord).r;
yuv.g = texture2D(uTexture,vTexCoord).r - 0.5;//
yuv.b = texture2D(vTexture,vTexCoord).r - 0.5;
}else if(formatType==25)//nv12
{
yuv.r = texture2D(yTexture,vTexCoord).r;
yuv.g = texture2D(uTexture,vTexCoord).r - 0.5 ;
// shader 会将数据归一化,而 uv 的取值区间本身存在-128到正128 然后归一化到0-1 为了正确计算成rgb,
// 则需要归一化到 -0.5 - 0.5的区间
yuv.b = texture2D(uTexture,vTexCoord).a - 0.5;
}else if(formatType==26)//nv21
{
yuv.r = texture2D(yTexture,vTexCoord).r;
yuv.g = texture2D(uTexture,vTexCoord).a - 0.5 ;
// shader 会将数据归一化,而 uv 的取值区间本身存在-128到正128 然后归一化到0-1 为了正确计算成rgb,
// 则需要归一化到 -0.5 - 0.5的区间
yuv.b = texture2D(uTexture,vTexCoord).r - 0.5;
}
rgb = mat3(1.0, 1.0, 1.0,
0.0,-0.39465,2.03211,
1.13983,-0.58060,0.0)*yuv;
//输出像素颜色
gl_FragColor = vec4(rgb,1.0);
}
);
shader是根据传入format的不同,转化方式不同。注意下面几点:
- y的纹理大小和视频帧的宽高一致,但uv的大小是y的1/2,但纹理会插值缩放到与y一样的大小
- 在获取u、v的数据时候,需 减去0.5 , 因为 uv的数据取值返回是 从 -100多到正100多,最后传入shader中会 归一化到0-1,因此,为了正确转化为rgb,需要将uv归一化到 -0.5 到 0.5 与原来的 rgb对应一样。
到这里就可以显示 yuv的数据了,当然大部分的相机的数据是nv21 或者nv12 ,相机的预览或者滤镜最后的显示都可以用这个方式。