Android OpenGLES2.0开发(十一):渲染YUV

人生如逆旅,我亦是行人

  • Android OpenGLES开发:EGL环境搭建
  • Android OpenGLES2.0开发(一):艰难的开始
  • Android OpenGLES2.0开发(二):环境搭建
  • Android OpenGLES2.0开发(三):绘制一个三角形
  • Android OpenGLES2.0开发(四):矩阵变换和相机投影
  • Android OpenGLES2.0开发(五):绘制正方形和圆形
  • Android OpenGLES2.0开发(六):着色器语言GLSL
  • Android OpenGLES2.0开发(七):纹理贴图之显示图片
  • Android OpenGLES2.0开发(八):Camera预览
  • Android OpenGLES2.0开发(九):图片滤镜
  • Android OpenGLES2.0开发(十):FBO离屏渲染
  • Android OpenGLES2.0开发(十一):渲染YUV

引言

还记的我们在Android OpenGLES2.0开发(八):Camera预览章节显示Camera预览中提到的一种方式吗,渲染NV21数据,这种方式略复杂我们没有详细讲解。但是在音视频开发中,由于YUV数据量比RGB小很多常常用于传输,而我们拿到YUV数据会要求显示。本章我们就来详细介绍如何使用OpenGL高效渲染YUV数据。

YUV格式

YUV格式是一种颜色编码方法,常用于视频处理和压缩中,特别是在电视广播、视频会议、视频播放等领域。它将颜色信息分成了三个部分:亮度(Y)和色度(U和V)。

  • Y (亮度): 表示图像的亮度信息,决定了图像的明暗。
  • U (蓝色差): 表示蓝色与亮度的差异,记录蓝色通道的信息。
  • V (红色差): 表示红色与亮度的差异,记录红色通道的信息。

由于Y分量与U、V分量是分开存储的,YUV格式可以有效地进行颜色压缩,特别适合视频传输和存储。

常见的几中YUV格式:

YUV420P

Y、U、V分布图如下,U和V分开存储,又称为I420格式,UV交换顺序后为YV12格式

I420:
I420

YV12:
在这里插入图片描述

YUV420SP

该格式,UV交错分布,根据UV的先后顺序分为NV12NV21(Android默认)

NV12:
在这里插入图片描述

NV21:
在这里插入图片描述

YUV和RGB转换

我们知道OpenGL纹理最终渲染的都是RGBA数据,因此我们需要将YUV转换为RGB。通用的转换公式如下:

R = Y + 1.402 * (V - 128)G = Y - 0.344136 * (U - 128) - 0.714136 * (V - 128)B = Y + 1.772 * (U - 128)

上面YUV转RGB的公式我使用了BT.601标准,实际上有多种标准,每种标准系数不同

  1. ITU-R BT.601(SDTV 标准,适用于 Android NV21)
  2. ITU-R BT.709(HDTV 标准,适用于 1080p 及以上视频)
  3. ITU-R BT.2020(UHDTV 标准,适用于 4K、8K 视频)

NV21转换示例

应该有很多人和我有一样的疑问,Y的数据量是UV的4倍,一个Y是如何和UV映射的呢,下面我们来举例说明。

假设我们有一个 4×4 的 Y 纹理(每个像素一个 Y 值),而 UV 纹理是 2×2,示意如下:

Y 纹理(4×4)

Y00  Y01  Y02  Y03  
Y10  Y11  Y12  Y13  
Y20  Y21  Y22  Y23  
Y30  Y31  Y32  Y33  

UV 纹理(2×2)

UV0   UV1  
UV2   UV3  

每个 UV 采样点对应 4 个 Y 像素:

(UV0) → {Y00, Y01, Y10, Y11}  
(UV1) → {Y02, Y03, Y12, Y13}  
(UV2) → {Y20, Y21, Y30, Y31}  
(UV3) → {Y22, Y23, Y32, Y33}  

但在 OpenGL 中,每个 Y 片段着色器 都需要一个 UV 值,而 UV 纹理比 Y 纹理小 4 倍(2x2),所以 OpenGL 会对 UV 进行插值。

OpenGL转换YUV

有了上面的理论基础,我们知道只需要将YUV数据传到OpenGL中,shader程序一个像素一个像素的转换即可。YUV数据如何传入到OpenGL中,答案是通过sampler2D纹理传递。我们又知道OpenGL中sampler2D纹理中有四个值RGBA,而YUV中Y只是单通道,I420中UV是单通道,NV12和NV21中UV交错存储可以理解为双通道。

那么现在的问题就是找到创建单通道和双通道的纹理了, OpenGL为我们提供了GL_LUMINANCEGL_LUMINANCE_ALPHA 格式的纹理,其中 GL_LUMINANCE 纹理用来加载 NV21 Y Plane 的数据,GL_LUMINANCE_ALPHA 纹理用来加载 UV Plane 的数据。

GL_LUMINANCE:
单通道纹理,纹理对象中RGBA值都相同

GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0,GLES20.GL_LUMINANCE, width, height, 0,GLES20.GL_LUMINANCE,GLES20.GL_UNSIGNED_BYTE, imageData);

GL_LUMINANCE_ALPHA
双通道纹理,UV存储到R和A值中

GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0,GLES20.GL_LUMINANCE_ALPHA, width, height, 0,GLES20.GL_LUMINANCE_ALPHA,GLES20.GL_UNSIGNED_BYTE, imageData
);

顶点着色器

顶点着色器代码没有变化,和之前Image中一样

// 顶点着色器代码
private final String vertexShaderCode ="uniform mat4 uMVPMatrix;\n" +// 顶点坐标"attribute vec4 vPosition;\n" +// 纹理坐标"attribute vec2 vTexCoordinate;\n" +"varying vec2 aTexCoordinate;\n" +"void main() {\n" +"  gl_Position = uMVPMatrix * vPosition;\n" +"  aTexCoordinate = vTexCoordinate;\n" +"}\n";

片段着色器代码

// 片段着色器代码
private final String fragmentShaderCode ="precision mediump float;\n" +"uniform sampler2D samplerY;\n" +"uniform sampler2D samplerU;\n" +"uniform sampler2D samplerV;\n" +"uniform sampler2D samplerUV;\n" +"uniform int yuvType;\n" +"varying vec2 aTexCoordinate;\n" +"void main() {\n" +"  vec3 yuv;\n" +"  if (yuvType == 0) {" +"    yuv.x = texture2D(samplerY, aTexCoordinate).r;\n" +"    yuv.y = texture2D(samplerU, aTexCoordinate).r - 0.5;\n" +"    yuv.z = texture2D(samplerV, aTexCoordinate).r - 0.5;\n" +"  } else if (yuvType == 1) {" +"    yuv.x = texture2D(samplerY, aTexCoordinate).r;\n" +"    yuv.y = texture2D(samplerUV, aTexCoordinate).r - 0.5;\n" +"    yuv.z = texture2D(samplerUV, aTexCoordinate).a - 0.5;\n" +"  } else {" +"    yuv.x = texture2D(samplerY, aTexCoordinate).r;\n" +"    yuv.y = texture2D(samplerUV, aTexCoordinate).a - 0.5;\n" +"    yuv.z = texture2D(samplerUV, aTexCoordinate).r - 0.5;\n" +"  }" +"  vec3 rgb = mat3(1.0, 1.0, 1.0,\n" +"  0.0, -0.344, 1.772,\n" +"  1.402, -0.714, 0.0) * yuv;\n" +"  gl_FragColor = vec4(rgb, 1);\n" +"}\n";

片段着色器中代码乍一看很复杂,待我来详细解释

sampler2D

我们上面声明了4个变量samplerYsamplerUsamplerVsamplerUV

  • yuvType=0:YUV格式为I420,U和V是分开存储的,我们需要把U和V分别映射到不同的纹理中,用到samplerY、samplerU、samplerV
  • yuvType=1:YUV格式为NV12,UV和交错存储的,UV映射到一个纹理上,用到samplerY、samplerUV
  • yuvType=2:YUV格式为NV21,UV和交错存储的,UV映射到一个纹理上,用到samplerY、samplerUV

texture2D

texture2D方法我们在前面的章节应该很熟悉了,就是获取对应纹理坐标下的RGBA的值

  • yuvType=0:YUV格式为I420,YUV都是分开存储,所以只需获取r就可以得到对应的YUV的值
  • yuvType=1:YUV格式为NV12,Y和上面一样,UV交错存储,通过获取r和a可得到对应的UV
  • yuvType=1:YUV格式为NV21,Y和上面一样,UV交错存储,通过获取a和r可得到对应的UV

计算RGB

vec3 rgb = mat3(1.0, 1.0, 1.0,
0.0, -0.344, 1.772,
1.402, -0.714, 0.0) * yuv;gl_FragColor = vec4(rgb, 1);

上面我们使用了矩阵乘法,其实和上面的提到的公式一样,如果你不熟悉这种方式,你可以分开计算:

float r = yuv.x + 1.402 * yuv.z;
float g = yuv.x - 0.344 * yuv.y - 0.714 * yuv.z;
float b = yuv.x + 1.772 * yuv.y;gl_FragColor = vec4(r, g, b, 1.0);

这两种方式的效果是一样的,为了计算效率我们只取float的后三位小数

YUVFilter

接下来我们看下YUVFilter的完整代码,这个类也是从之前Image拷贝而来,并做了修改如下:

public class YUVFilter {/*** 绘制的流程* 1.顶点着色程序 - 用于渲染形状的顶点的 OpenGL ES 图形代码* 2.片段着色器 - 用于渲染具有特定颜色或形状的形状的 OpenGL ES 代码纹理。* 3.程序 - 包含您想要用于绘制的着色器的 OpenGL ES 对象 一个或多个形状* <p>* 您至少需要一个顶点着色器来绘制形状,以及一个 fragment 着色器来为该形状着色。* 这些着色器必须经过编译,然后添加到 OpenGL ES 程序中,该程序随后用于绘制形状。*/// 顶点着色器代码private final String vertexShaderCode ="uniform mat4 uMVPMatrix;\n" +// 顶点坐标"attribute vec4 vPosition;\n" +// 纹理坐标"attribute vec2 vTexCoordinate;\n" +"varying vec2 aTexCoordinate;\n" +"void main() {\n" +"  gl_Position = uMVPMatrix * vPosition;\n" +"  aTexCoordinate = vTexCoordinate;\n" +"}\n";// 片段着色器代码private final String fragmentShaderCode ="precision mediump float;\n" +"uniform sampler2D samplerY;\n" +"uniform sampler2D samplerU;\n" +"uniform sampler2D samplerV;\n" +"uniform sampler2D samplerUV;\n" +"uniform int yuvType;\n" +"varying vec2 aTexCoordinate;\n" +"void main() {\n" +"  vec3 yuv;\n" +"  if (yuvType == 0) {" +"    yuv.x = texture2D(samplerY, aTexCoordinate).r;\n" +"    yuv.y = texture2D(samplerU, aTexCoordinate).r - 0.5;\n" +"    yuv.z = texture2D(samplerV, aTexCoordinate).r - 0.5;\n" +"  } else if (yuvType == 1) {" +"    yuv.x = texture2D(samplerY, aTexCoordinate).r;\n" +"    yuv.y = texture2D(samplerUV, aTexCoordinate).r - 0.5;\n" +"    yuv.z = texture2D(samplerUV, aTexCoordinate).a - 0.5;\n" +"  } else {" +"    yuv.x = texture2D(samplerY, aTexCoordinate).r;\n" +"    yuv.y = texture2D(samplerUV, aTexCoordinate).a - 0.5;\n" +"    yuv.z = texture2D(samplerUV, aTexCoordinate).r - 0.5;\n" +"  }" +"  vec3 rgb = mat3(1.0, 1.0, 1.0,\n" +"  0.0, -0.344, 1.772,\n" +"  1.402, -0.714, 0.0) * yuv;\n" +"  gl_FragColor = vec4(rgb, 1);\n" +"}\n";private int mProgram;// 顶点坐标缓冲区private FloatBuffer vertexBuffer;// 纹理坐标缓冲区private FloatBuffer textureBuffer;// 此数组中每个顶点的坐标数static final int COORDS_PER_VERTEX = 2;/*** 顶点坐标数组* 顶点坐标系中原点(0,0)在画布中心* 向左为x轴正方向* 向上为y轴正方向* 画布四个角坐标如下:* (-1, 1),(1, 1)* (-1,-1),(1,-1)*/private float vertexCoords[] = {-1.0f, 1.0f,   // 左上-1.0f, -1.0f,  // 左下1.0f, 1.0f,    // 右上1.0f, -1.0f,   // 右下};/*** 纹理坐标数组* 这里我们需要注意纹理坐标系,原点(0,0s)在画布左下角* 向左为x轴正方向* 向上为y轴正方向* 画布四个角坐标如下:* (0,1),(1,1)* (0,0),(1,0)*/private float textureCoords[] = {0.0f, 1.0f, // 左上0.0f, 0.0f, // 左下1.0f, 1.0f, // 右上1.0f, 0.0f, // 右下};private int positionHandle;// 纹理坐标句柄private int texCoordinateHandle;// Use to access and set the view transformationprivate int vPMatrixHandle;private IntBuffer mPlanarTextureHandles = IntBuffer.wrap(new int[3]);private int[] mSampleHandle = new int[3];private int mYUVTypeHandle;private final int vertexCount = vertexCoords.length / COORDS_PER_VERTEX;private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertexprivate int mTextureWidth;private int mTextureHeight;public YUVFilter() {// 初始化形状坐标的顶点字节缓冲区vertexBuffer = ByteBuffer.allocateDirect(vertexCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(vertexCoords);vertexBuffer.position(0);// 初始化纹理坐标顶点字节缓冲区textureBuffer = ByteBuffer.allocateDirect(textureCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(textureCoords);textureBuffer.position(0);}public void setTextureSize(int width, int height) {mTextureWidth = width;mTextureHeight = height;}public void surfaceCreated() {// 加载顶点着色器程序int vertexShader = GLESUtils.loadShader(GLES20.GL_VERTEX_SHADER,vertexShaderCode);// 加载片段着色器程序int fragmentShader = GLESUtils.loadShader(GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);// 创建空的OpenGL ES程序mProgram = GLES20.glCreateProgram();// 将顶点着色器添加到程序中GLES20.glAttachShader(mProgram, vertexShader);// 将片段着色器添加到程序中GLES20.glAttachShader(mProgram, fragmentShader);// 创建OpenGL ES程序可执行文件GLES20.glLinkProgram(mProgram);// 获取顶点着色器vPosition成员的句柄positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");// 获取顶点着色器中纹理坐标的句柄texCoordinateHandle = GLES20.glGetAttribLocation(mProgram, "vTexCoordinate");// 获取绘制矩阵句柄vPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");// 获取yuvType句柄mYUVTypeHandle = GLES20.glGetUniformLocation(mProgram, "yuvType");// 生成YUV纹理句柄GLES20.glGenTextures(3, mPlanarTextureHandles);}public void surfaceChanged(int width, int height) {GLES20.glViewport(0, 0, width, height);}public void onDraw(float[] matrix, YUVFormat yuvFormat) {// 将程序添加到OpenGL ES环境GLES20.glUseProgram(mProgram);// 重新绘制背景色为黑色GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);// 为正方形顶点启用控制句柄GLES20.glEnableVertexAttribArray(positionHandle);// 写入坐标数据GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);// 启用纹理坐标控制句柄GLES20.glEnableVertexAttribArray(texCoordinateHandle);// 写入坐标数据GLES20.glVertexAttribPointer(texCoordinateHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, textureBuffer);// 将投影和视图变换传递给着色器GLES20.glUniformMatrix4fv(vPMatrixHandle, 1, false, matrix, 0);int yuvType = 0;// 设置yuvTypeif (yuvFormat == YUVFormat.I420) {yuvType = 0;} else if (yuvFormat == YUVFormat.NV12) {yuvType = 1;} else if (yuvFormat == YUVFormat.NV21) {yuvType = 2;}GLES20.glUniform1i(mYUVTypeHandle, yuvType);// yuvType: 0是I420,1是NV12int planarCount = 0;if (yuvFormat == YUVFormat.I420) {planarCount = 3;mSampleHandle[0] = GLES20.glGetUniformLocation(mProgram, "samplerY");mSampleHandle[1] = GLES20.glGetUniformLocation(mProgram, "samplerU");mSampleHandle[2] = GLES20.glGetUniformLocation(mProgram, "samplerV");} else {//NV12、NV21有两个平面planarCount = 2;mSampleHandle[0] = GLES20.glGetUniformLocation(mProgram, "samplerY");mSampleHandle[1] = GLES20.glGetUniformLocation(mProgram, "samplerUV");}for (int i = 0; i < planarCount; i++) {GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mPlanarTextureHandles.get(i));GLES20.glUniform1i(mSampleHandle[i], i);}// 绘制GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);// 禁用顶点阵列GLES20.glDisableVertexAttribArray(positionHandle);GLES20.glDisableVertexAttribArray(texCoordinateHandle);}public void release() {GLES20.glDeleteProgram(mProgram);mProgram = -1;}/*** 将图片数据绑定到纹理目标,适用于UV分量分开存储的(I420)** @param yPlane YUV数据的Y分量* @param uPlane YUV数据的U分量* @param vPlane YUV数据的V分量* @param width  YUV图片宽度* @param height YUV图片高度*/public void feedTextureWithImageData(ByteBuffer yPlane, ByteBuffer uPlane, ByteBuffer vPlane, int width, int height) {//根据YUV编码的特点,获得不同平面的基址textureYUV(yPlane, width, height, 0);textureYUV(uPlane, width / 2, height / 2, 1);textureYUV(vPlane, width / 2, height / 2, 2);}/*** 将图片数据绑定到纹理目标,适用于UV分量交叉存储的(NV12、NV21)** @param yPlane  YUV数据的Y分量* @param uvPlane YUV数据的UV分量* @param width   YUV图片宽度* @param height  YUV图片高度*/public void feedTextureWithImageData(ByteBuffer yPlane, ByteBuffer uvPlane, int width, int height) {//根据YUV编码的特点,获得不同平面的基址textureYUV(yPlane, width, height, 0);textureNV12(uvPlane, width / 2, height / 2, 1);}/*** 将图片数据绑定到纹理目标,适用于UV分量分开存储的(I420)** @param imageData YUV数据的Y/U/V分量* @param width     YUV图片宽度* @param height    YUV图片高度*/private void textureYUV(ByteBuffer imageData, int width, int height, int index) {// 将纹理对象绑定到纹理目标GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mPlanarTextureHandles.get(index));// 设置放大和缩小时,纹理的过滤选项为:线性过滤GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);// 设置纹理X,Y轴的纹理环绕选项为:边缘像素延伸GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);// 加载图像数据到纹理,GL_LUMINANCE指明了图像数据的像素格式为只有亮度,虽然第三个和第七个参数都使用了GL_LUMINANCE,// 但意义是不一样的,前者指明了纹理对象的颜色分量成分,后者指明了图像数据的像素格式// 获得纹理对象后,其每个像素的r,g,b,a值都为相同,为加载图像的像素亮度,在这里就是YUV某一平面的分量值GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0,GLES20.GL_LUMINANCE, width, height, 0,GLES20.GL_LUMINANCE,GLES20.GL_UNSIGNED_BYTE, imageData);}/*** 将图片数据绑定到纹理目标,适用于UV分量交叉存储的(NV12、NV21)** @param imageData YUV数据的UV分量* @param width     YUV图片宽度* @param height    YUV图片高度*/private void textureNV12(ByteBuffer imageData, int width, int height, int index) {GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mPlanarTextureHandles.get(index));GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0,GLES20.GL_LUMINANCE_ALPHA, width, height, 0,GLES20.GL_LUMINANCE_ALPHA,GLES20.GL_UNSIGNED_BYTE, imageData);}
}

具体改动点主要就是增加了根据YUV类型生成对应的纹理,并将纹理闯入OpenGL中

DisplayYUVGLSurfaceView

新建一个GLSurfaceView,在其中使用YUVFilter,完整代码如下:

public class DisplayYUVGLSurfaceView extends GLSurfaceView {private static final String TAG = DisplayYUVGLSurfaceView.class.getSimpleName();private Context mContext;private MyRenderer mMyRenderer;public DisplayYUVGLSurfaceView(Context context) {super(context);init(context);}public DisplayYUVGLSurfaceView(Context context, AttributeSet attrs) {super(context, attrs);init(context);}private void init(Context context) {mContext = context;mMyRenderer = new MyRenderer();setEGLContextClientVersion(2);setRenderer(mMyRenderer);setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);}public void feedYUVData(byte[] yuvData, int width, int height, YUVFormat yuvFormat, int rotate) {if (yuvData == null) {return;}mMyRenderer.feedData(yuvData, width, height, yuvFormat, rotate);requestRender();}public void setCameraId(int id) {mMyRenderer.setCameraId(id);}static class MyRenderer implements Renderer {private YUVFilter mYUVFilter;private YUVFormat mYUVFormat;private int mWidth;private int mHeight;// vPMatrix is an abbreviation for "Model View Projection Matrix"private float[] mMVPMatrix = new float[16];// y分量数据private ByteBuffer y = ByteBuffer.allocate(0);// u分量数据private ByteBuffer u = ByteBuffer.allocate(0);// v分量数据private ByteBuffer v = ByteBuffer.allocate(0);// uv分量数据private ByteBuffer uv = ByteBuffer.allocate(0);// 标识GLSurfaceView是否准备好private boolean hasVisibility = false;private boolean isMirror = false;private int mRotate;private int mCameraId;public MyRenderer() {mYUVFilter = new YUVFilter();}public void setCameraId(int cameraId) {mCameraId = cameraId;}@Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {mYUVFilter.surfaceCreated();}@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {mYUVFilter.surfaceChanged(width, height);hasVisibility = true;}@Overridepublic void onDrawFrame(GL10 gl) {synchronized (this) {if (y.capacity() > 0) {y.position(0);if (mYUVFormat == YUVFormat.I420) {u.position(0);v.position(0);mYUVFilter.feedTextureWithImageData(y, u, v, mWidth, mHeight);} else {uv.position(0);mYUVFilter.feedTextureWithImageData(y, uv, mWidth, mHeight);}MatrixUtils.getMatrix(mMVPMatrix, MatrixUtils.TYPE_FITXY, mWidth, mHeight, mWidth, mHeight);MatrixUtils.flip(mMVPMatrix, false, true);if (mCameraId == 1) {MatrixUtils.flip(mMVPMatrix, true, false);}MatrixUtils.rotate(mMVPMatrix, mRotate);try {long start = System.currentTimeMillis();mYUVFilter.onDraw(mMVPMatrix, mYUVFormat);Log.i(TAG, "drawTexture " + mWidth + "x" + mHeight + " 耗时:" + (System.currentTimeMillis() - start) + "ms");} catch (Exception e) {Log.w(TAG, e.getMessage());}}}}/*** 设置渲染的YUV数据的宽高** @param width  宽度* @param height 高度*/public void setYuvDataSize(int width, int height) {if (width > 0 && height > 0) {// 初始化容器if (width != mWidth || height != mHeight) {this.mWidth = width;this.mHeight = height;int yarraySize = width * height;int uvarraySize = yarraySize / 4;synchronized (this) {y = ByteBuffer.allocate(yarraySize);u = ByteBuffer.allocate(uvarraySize);v = ByteBuffer.allocate(uvarraySize);uv = ByteBuffer.allocate(uvarraySize * 2);}}}}public void feedData(byte[] yuvData, int width, int height, YUVFormat yuvFormat, int rotate) {setYuvDataSize(width, height);synchronized (this) {mWidth = width;mHeight = height;mYUVFormat = yuvFormat;mRotate = rotate;if (hasVisibility) {if (yuvFormat == YUVFormat.I420) {y.clear();u.clear();v.clear();y.put(yuvData, 0, width * height);u.put(yuvData, width * height, width * height / 4);v.put(yuvData, width * height * 5 / 4, width * height / 4);} else {y.clear();uv.clear();y.put(yuvData, 0, width * height);uv.put(yuvData, width * height, width * height / 2);}}}}}
}

‌OpenGLES渲染YUV数据‌主要涉及到YUV数据的处理和渲染过程。YUV是一种颜色编码方法,其中“Y”表示明亮度(Luminance或Luma),而“U”和“V”表示色度(Chrominance或Chroma),用于描述影像色彩及饱和度。YUV格式主要用于电视系统以及模拟视频领域,它允许降低色度的带宽,同时保持图片质量,提供传输效率。在OpenGLES中渲染YUV数据,通常涉及以下几个步骤:

显示Camera的YUV数据

我们使用了Camera系列中Camera2Manager类,通过他获取YUV数据

public class DisplayYUVActivity extends AppCompatActivity implements CameraCallback {private static final String TAG = DisplayYUVActivity.class.getSimpleName();private DisplayYUVGLSurfaceView mDisplayYUVGLSurfaceView;private ICameraManager mCameraManager;private int mCameraId = 1;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_display_yuvactivity);mDisplayYUVGLSurfaceView = findViewById(R.id.displayYUVGLView);mCameraManager = new Camera2Manager(this);mCameraManager.setCameraId(mCameraId);mCameraManager.setCameraCallback(this);mCameraManager.addPreviewBufferCallback(mPreviewBufferCallback);mDisplayYUVGLSurfaceView.setCameraId(mCameraId);}@Overrideprotected void onResume() {super.onResume();mCameraManager.openCamera();}@Overrideprotected void onPause() {super.onPause();mCameraManager.releaseCamera();}@Overridepublic void onOpen() {mCameraManager.startPreview((SurfaceTexture) null);}@Overridepublic void onOpenError(int error, String msg) {}@Overridepublic void onPreview(int previewWidth, int previewHeight) {}@Overridepublic void onPreviewError(int error, String msg) {}@Overridepublic void onClose() {}private PreviewBufferCallback mPreviewBufferCallback = new PreviewBufferCallback() {@Overridepublic void onPreviewBufferFrame(byte[] data, int width, int height, YUVFormat format) {mDisplayYUVGLSurfaceView.feedYUVData(data, width, height, format, mCameraManager.getOrientation());}};
}

最后

本章我们学习了如何将YUV原数据通过OpenGL显示,该方式通过OpenGL将YUV数据转换为RGB然后显示到屏幕,性能比用CPU转换好很多。

OpenGL ES系列:https://github.com/xiaozhi003/AndroidOpenGLDemo.git,如果对你有帮助可以star下,万分感谢^_^

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/896356.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入miniqmt:创建交易对象的完整指南

深入miniqmt&#xff1a;创建交易对象的完整指南 在量化交易领域&#xff0c;miniqmt作为一个强大的工具&#xff0c;为开发者提供了执行程序化交易的接口。在进行任何交易操作之前&#xff0c;首先需要创建一个交易对象。本文将详细介绍如何在miniqmt中创建并配置交易对象&am…

日语Learn、英语的再认识

背单词这件事感觉到了某个瓶颈了&#xff0c;没有什么新的区域可以发现&#xff0c;变成了一种类似纯粹的机械记忆&#xff0c;感觉英语的规范程度很低&#xff0c;很多词明明可以通过相近的形式变换&#xff0c;达到更好的学习效果&#xff0c;但却做的很麻烦&#xff0c;同一…

内存泄漏指什么?常见的内存泄漏有哪些?

内存泄漏是指程序在运行过程中&#xff0c;由于某些原因导致程序无法释放已经不再使用的内存&#xff0c;使得这部分内存持续被占用&#xff0c;最终可能导致系统可用内存逐渐减少&#xff0c;严重时会影响系统性能甚至导致程序崩溃。&#xff08;内存泄漏是指程序中已经分配的…

文心一言AI创意画

介绍 文心一言是百度推出的新一代知识增强大语言模型&#xff0c;属于文心大模型家族的新成员。‌它能够与人对话互动、回答问题、协助创作&#xff0c;高效便捷地帮助人们获取信息、知识和灵感。‌ 特点 文心一言基于数万亿数据和数千亿知识进行融合学习&#xff0c;采用预训…

洛谷 P8705:[蓝桥杯 2020 省 B1] 填空题之“试题 E :矩阵” ← 卡特兰数

【题目来源】 https://www.luogu.com.cn/problem/P8705 【题目描述】 把 1∼2020 放在 21010 的矩阵里。要求同一行中右边的比左边大&#xff0c;同一列中下边的比上边的大。一共有多少种方案? 答案很大&#xff0c;你只需要给出方案数除以 2020 的余数即可。 【答案提交】 …

我是如何从 0 到 1 找到 Web3 工作的?

作者&#xff1a;Lotus的人生实验 关于我花了一个月的时间&#xff0c;从 0 到 1 学习 Web3 相关的知识和编程知识。然后找到了一个 Web3 创业公司实习的远程工作。 &#x1f447;&#x1f447;&#x1f447; 我的背景: 计算机科班&#xff0c;学历还可以(大厂门槛水平) 毕业工…

量子网络:构建与应用前景的展望

大家好,我是Echo_Wish,今天我们来探讨一个极具前瞻性的领域——量子网络的构建与应用前景。随着量子计算的发展,量子网络作为量子信息科学的重要组成部分,正在引起越来越多的关注。本文将深入解析量子网络的构建原理,并展望其应用前景。 量子网络的基本概念 量子网络是指…

数据库二三事(8)

高级数据查询 top词语法格式&#xff1a;TOP n &#xff08;percent&#xff09;&#xff08;with ties&#xff09; 查询前n&#xff08;%&#xff09;行数据&#xff0c;&#xff08;包括最后一行取值并列&#xff09; 搭配 order by case&#xff1a; CASE &#xff08;…

linux中conda3安装

1、下载安装包 清华源-》https://mirrors.tuna.tsinghua.edu.cn/# 本文使用Anaconda3-2022.10&#xff0c;对应的下载路径-》https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/Anaconda3-2022.10-Linux-x86_64.sh 2、将下载到的sh脚本放在Linux中用sh脚本解析器执行 …

2025最新智能优化算法:人工旅鼠算法(Artificial Lemming Algorithm, ALA)求解23个经典函数测试集,MATLAB

一、人工旅鼠优化算法 人工旅鼠算法&#xff08;Artificial Lemming Algorithm, ALA&#xff09;是2025年提出的一种新型生物启发式优化算法&#xff0c;受旅鼠的四种典型行为启发&#xff1a;长距离迁徙、挖洞、觅食和躲避捕食者。该算法通过模拟这些行为来解决复杂的优化问题…

Python游戏编程之赛车游戏6-2

3.2 move()方法的定义 Player类的move()方法用于玩家控制汽车左右移动&#xff0c;当玩家点击键盘上的左右按键时&#xff0c;汽车会相应地进行左右移动。 move()方法的代码如图7所示。 图7 move()方法的代码 其中&#xff0c;第20行代码通过pygame.key.get_pressed()函数获…

日语学习-日语知识点小记-构建基础-JLPT-N4N5阶段(12):普通(ふつう)形 :变化方式 :日常朋友家人之间对话

日语学习-日语知识点小记-构建基础-JLPT-N4&N5阶段(12):普通(ふつう)形 :变化方式 :日常朋友&家人之间对话  1、前言(1)情况说明(2)工程师的信仰2、知识点(1)普通(ふつう)形:Plain style:简体3、单词(1)日语单词(2)日语片假名单词4、相近词辨…

华为hcia——Datacom实验指南——二层交换原理

实验配置 eNSP 什么是二层交换 二层交换是指在同一个ip网段内&#xff0c;数据通过二层交换机进行转发。 什么是mac地址 mac地址也叫做硬件地址&#xff0c;是以太网协议的链路层地址。简单的来说&#xff0c;mac地址就是我们硬件的身份证&#xff0c;独一无二。它是由48个bi…

粘贴到Word里的图片显示不全

粘贴到Word里的图片显示不全&#xff0c;可从Word设置、图片本身、软件与系统等方面着手解决&#xff0c;具体方法如下&#xff1a; Word软件设置 经实践发现&#xff0c;图片在word行距的行距出现问题&#xff0c;可以按照如下调整行距进行处理 修改段落行距&#xff1a; 选…

HTML转义和反转义工具类

HTML转义和反转义工具类 package com.common.utils;import cn.hutool.http.HTMLFilter; import org.apache.commons.lang3.StringUtils;/*** 转义和反转义工具类** author lxx*/ public class EscapeUtil {public static final String RE_HTML_MARK "(<[^<]*?>…

Android之图片保存相册及分享图片

文章目录 前言一、效果图二、实现步骤1.引入依赖库2.二维码生成3.布局转图片保存或者分享 总结 前言 其实现在很多分享都是我们自定义的&#xff0c;更多的是在界面加了很多东西&#xff0c;然后把整个界面转成图片保存相册和分享&#xff0c;而且现在分享都不需要第三方&…

以绘图(绘制点、直线、圆、椭圆、多段线)为例子 通过设计模式中的命令模式实现

为了在命令模式的基础上实现撤销&#xff08;Undo&#xff09;和回退&#xff08;Redo&#xff09;功能&#xff0c;我们可以在每个命令类中记录一些必要的状态&#xff0c;允许我们撤销之前的操作&#xff0c;并在需要时回退操作。常见的做法是使用一个命令堆栈来存储历史命令…

git从本地其他设备上fetch分支

在 Git 中&#xff0c;如果你想从本地其他设备上获取分支&#xff0c;可以通过以下几种方式实现。不过&#xff0c;需要注意的是&#xff0c;Git 本身是分布式版本控制系统&#xff0c;通常我们是从远程仓库&#xff08;如 GitHub、GitLab 等&#xff09;拉取分支&#xff0c;而…

故障诊断 | Matlab实现基于DBO-BP-Bagging多特征分类预测/故障诊断

故障诊断 | Matlab实现基于DBO-BP-Bagging多特征分类预测/故障诊断 目录 故障诊断 | Matlab实现基于DBO-BP-Bagging多特征分类预测/故障诊断分类效果基本介绍模型描述DBO-BP-Bagging蜣螂算法优化多特征分类预测一、引言1.1、研究背景和意义1.2、研究现状1.3、研究目的与方法 二…

CentOS停服后的替代选择:openEuler、Rocky Linux及其他系统的未来展望

CentOS停服后的替代选择&#xff1a;openEuler、Rocky Linux及其他系统的未来展望 引言CentOS停服的背景华为openEuler&#xff1a;面向未来的开源操作系统1. 简介2. 特点3. 发展趋势 Rocky Linux&#xff1a;CentOS的精神继承者1. 简介2. 特点3. 发展趋势 其他可选的替代系统1…