Android解码输出yuv,Android OpenGLES2.0 直接导出YUV420数据

Android OpenGLES2.0中提供的glReadPixels方法提供的格式只有RGB的几种格式,但是这并不妨碍我们导出YUV格式的数据,因为不管是RGBA还是YUV,都不是glReadPixels方法关心的,它关心的其实是每个色彩分量占多少位。可以看到glReadPixels提供的导出格式有RGBA、RGB等,却不会在提供了RGBA的导出格式后又提供ABGR、ARGB这样的导出格式,就是这个道理了。

获取YUV420的方案

如果我们要做的工作只是处理视频图像,然后再编码,我们无需这一步,可以直接利用MediaCodec+Surface编码,效率比导出YUV来编码更高,可以参考我的上一篇博客Android利用硬解硬编和OpenGLES来高效的处理MP4视频。但是我们需要对图像进行处理,然后传输的时候,就要考虑导出YUV数据了。利用OpenGLES2.0 处理完图像后,获取YUV数据的方案大致分为以下几种:

glReadPixels 获取RGB的数据,然后利用libyuv获取其他方式将RGB转换成YUV420

渲染三次,将图像分别渲染为Y平面,U平面,V平面,每次渲染后就glReadPixels得到YUV420

将图像渲染成YUV444导出,然后利用CPU将YUV444转换成YUV420

将图像渲染成YUV420导出

(注:异步读取的问题暂时先不讨论)

方案1可以算是“传统”方法,毕竟RGB是GLES2.0字面上就支持的格式,它需要读取的数据大小是width*height*3,而且还有一步CPU中RGB转YUV的处理过程。速度真是不敢恭维,利用几款手机测试,368*640大小的图像,GPU处理&glReadPixels(注:glReadPixels会等待GPU处理当前图像完成)读取时间大概为16-24ms,转换5-12ms。

方案2方案3,我还没有试过,不过我心里对这两个方案是比较抵触的,如果方案4确实行不通,我不介意选择方案3,方案2渲染3次,readPixels3次不得不说是一个比较糟糕的方案。

方案4数据大小为width*height*3/2,而且也不需要CPU处理,实现之前唯一的担心就是GPU中处理起来太麻烦,会耗时比较长。最后用几款手机及测试,368*640大小的图像,GPU处理&glReadPixels时间为12-20ms。这个时间还是比较可观的。

导出算法

之前有在CPU中实现过RGBA转YUV,GPU中转换也差不多了,主要的问题还是怎么将YUV直接导出来。了解了YUV和RGBA的格式,可以知道YUV是Y平面大小为width*height,如果我们最后glReadPixels格式为RGBA,那就占有图像的1/4大小,420P中,U平面大小为width*height/4,占有1/16,V平面大小和U平面一样。而在420SP中,UV同平面交错排列占有1/8。那么实际上我们glReadPixels的宽高就是width,height*3/8。我们在Shader中需要做的就是把YUV填充到对应的位置。

具体片元着色器如下,代码中有详细备注,就不多加分析了。思路就是在Y区域,每个像素点存储四

个Y。YUV420P格式时,U区域每个像素点存储4个U,V区域每个像素点存储4个V。YUV420SP时,UV在同一个区域交替排列,那么就是每个像素点存储UVUV(NV12)四个值或者VUVU(NV21)四个值。

为了减少计算,采样UV没有用四点平均,直接采样四个点中的左上角的点作为UV计算的输入值。

precision highp float;

precision highp int;

varying vec2 vTextureCo;

uniform sampler2D uTexture;

//为了简化计算,宽高都必须为8的倍数

uniform float uWidth; // 纹理宽

uniform float uHeight; // 纹理高

//转换公式

//Y’= 0.299*R’ + 0.587*G’ + 0.114*B’

//U’= -0.147*R’ - 0.289*G’ + 0.436*B’ = 0.492*(B’- Y’)

//V’= 0.615*R’ - 0.515*G’ - 0.100*B’ = 0.877*(R’- Y’)

//导出原理:采样坐标只作为确定输出位置使用,通过输出纹理计算实际采样位置,进行采样和并转换,

//然后将转换的结果填充到输出位置

float cY(float x,float y){

vec4 c=texture2D(uTexture,vec2(x,y));

return c.r*0.2990+c.g*0.5870+c.b*0.1140;

}

float cU(float x,float y){

vec4 c=texture2D(uTexture,vec2(x,y));

return -0.1471*c.r - 0.2889*c.g + 0.4360*c.b+0.5000;

}

float cV(float x,float y){

vec4 c=texture2D(uTexture,vec2(x,y));

return 0.6150*c.r - 0.5150*c.g - 0.1000*c.b+0.5000;

}

vec2 cPos(float t,float shiftx,float shifty){

vec2 pos=vec2(uWidth*vTextureCo.x,uHeight*(vTextureCo-shifty));

return vec2(mod(pos.x*shiftx,uWidth),(pos.y*shiftx+floor(pos.x*shiftx/uWidth))*t);

}

//Y分量的计算

vec4 calculateY(){

//填充点对应图片的位置

float posX=floor(uWidth*vTextureCo.x);

float posY=floor(uHeight*vTextureCo.y);

//实际采样起始点对应图片的位置

float rPosX=mod(posX*4.,uWidth);

float rPosY=posY*4.+floor(posX*4./uWidth);

vec4 oColor=vec4(0);

float textureYPos=rPosY/uHeight;

oColor[0]=cY(rPosX/uWidth,textureYPos);

oColor[1]=cY((rPosX+1.)/uWidth,textureYPos);

oColor[2]=cY((rPosX+2.)/uWidth,textureYPos);

oColor[3]=cY((rPosX+3.)/uWidth,textureYPos);

return oColor;

}

//U分量的计算

vec4 calculateU(){

//U的采样,宽度是1:8,高度是1:2,U的位置高度偏移了1/4,一个点是4个U,采样区域是宽高位8*2

float posX=floor(uWidth*vTextureCo.x);

float posY=floor(uHeight*(vTextureCo.y-0.2500));

//实际采样起始点对应图片的位置

float rPosX=mod(posX*8.,uWidth);

float rPosY=posY*16.+floor(posX*8./uWidth)*2.;

vec4 oColor=vec4(0);

oColor[0]= cU(rPosX/uWidth,rPosY/uHeight);

oColor[1]= cU((rPosX+2.)/uWidth,rPosY/uHeight);

oColor[2]= cU((rPosX+4.)/uWidth,rPosY/uHeight);

oColor[3]= cU((rPosX+6.)/uWidth,rPosY/uHeight);

return oColor;

}

//V分量计算

vec4 calculateV(){

//V的采样,宽度是1:8,高度是1:2,U的位置高度偏移了1/4,一个点是4个V,采样区域是宽高位8*2

float posX=floor(uWidth*vTextureCo.x);

float posY=floor(uHeight*(vTextureCo.y-0.3125));

//实际采样起始点对应图片的位置

float rPosX=mod(posX*8.,uWidth);

float rPosY=posY*16.+floor(posX*8./uWidth)*2.;

vec4 oColor=vec4(0);

oColor[0]=cV(rPosX/uWidth,rPosY/uHeight);

oColor[1]=cV((rPosX+2.)/uWidth,rPosY/uHeight);

oColor[2]=cV((rPosX+4.)/uWidth,rPosY/uHeight);

oColor[3]=cV((rPosX+6.)/uWidth,rPosY/uHeight);

return oColor;

}

//UV的计算,YUV420SP用,test

vec4 calculateUV(){

float posX=floor(uWidth*vTextureCo.x);

float posY=floor(uHeight*(vTextureCo.y-0.2500));

//实际采样起始点对应图片的位置

float rPosX=mod(posX*4.,uWidth);

float rPosY=posY*8.+floor(posX*4./uWidth)*2.;

vec4 oColor=vec4(0);

oColor[0]= cU((rPosX+1.)/uWidth,(rPosY+1.)/uHeight);

oColor[1]= cV((rPosX+1.)/uWidth,(rPosY+1.)/uHeight);

oColor[2]= cU((rPosX+3.)/uWidth,(rPosY+1.)/uHeight);

oColor[3]= cV((rPosX+3.)/uWidth,(rPosY+1.)/uHeight);

return oColor;

}

void main() {

//如果希望导出420SP格式,删除<0.3125的判断,在0.3750判断中换成calculateUV就可以了

//稍微改改可以支持I420,YV12,NV12,NV21四种格式,不建议用传入参数然后if else来实现,GPU中尽可能不用流程控制语句

if(vTextureCo.y<0.2500){

gl_FragColor=calculateY();

}else if(vTextureCo.y<0.3125){

gl_FragColor=calculateU();

}else if(vTextureCo.y<0.3750){

gl_FragColor=calculateV();

}else{

gl_FragColor=vec4(0,0,0,0);

}

}

源码及示例

源码及示例在github上,此项目旨在编写一套小巧实用的Android平台音频、视频(图像)的处理框架。有需要的朋友可以自己下载,如有帮助,欢迎start、fork和打赏。本篇博客相关代码为YuvOutputFilter,可以直接链入此框架使用:

//创建YUV导出的Filter

mYuvOutput=new YuvOutputFilter(YuvOutputFilter.EXPORT_TYPE_I420);

//GL相关初始化,在GL线程中

mYuvFilter.create();

//设置导出大小,在GL线程中

mYuvOutput.sizeChanged(width,height);

//YUV转换,在GL线程中

mYuvOutput.drawToTexture(texture);

//获取YUV数据

mYuvOutput.getOutput(receiveBytes);

你可能感兴趣的:(Android解码输出yuv)