当前位置: 首页 > news >正文

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下,万分感谢^_^

相关文章:

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

人生如逆旅&#xff0c;我亦是行人 Android OpenGLES开发&#xff1a;EGL环境搭建Android OpenGLES2.0开发&#xff08;一&#xff09;&#xff1a;艰难的开始Android OpenGLES2.0开发&#xff08;二&#xff09;&#xff1a;环境搭建Android OpenGLES2.0开发&#xff08;三&am…...

在linux中利用conda安装blast

在 Linux 中使用 conda 安装 BLAST 非常简单。conda 是一个流行的包管理工具&#xff0c;可以轻松安装和管理生物信息学工具&#xff0c;包括 BLAST。以下是具体步骤&#xff1a; 1. 确保已安装 Conda 如果你还没有安装 conda&#xff0c;可以参考以下步骤安装 Miniconda&…...

三、多项式环

文章目录 一、多项式环的定义二、多项式环的性质1. 多项式加法2. 多项式乘法3. 满足的运算规律4. 次数5. 单位元 三、剩余多项式环&#xff08;商多项式环&#xff09;四、有限多项式环五、多项式环的性质与特性1. 子环与理想2. 不可约性和素性3. 有限生成性 一、多项式环的定义…...

python unzip file

要在 Python 中解压文件并显示进度&#xff0c;我们需要在解压过程中跟踪文件的提取进度。由于 zipfile 模块本身不直接支持进度显示&#xff0c;我们可以通过手动计算并使用 tqdm 库来显示进度条。 安装 tqdm 首先&#xff0c;确保你已经安装了 tqdm 库&#xff0c;用于显示…...

MySQL-增删改查

一、Create(创建) &#x1f4d6; 语法&#xff1a; INSERT INTO table_name(value_list); 当我们使用表的时候&#xff0c;就可以使用这个语法来向表中插入元素~ 我们这边创建一个用于示范的表(Student)~ create table student( id int, name varchar(20), chinese int, math…...

LeetCode 热题100 15. 三数之和

LeetCode 热题100 | 15. 三数之和 大家好&#xff0c;今天我们来解决一道经典的算法题——三数之和。这道题在 LeetCode 上被标记为中等难度&#xff0c;要求我们从一个整数数组中找到所有不重复的三元组&#xff0c;使得三元组的和为 0。下面我将详细讲解解题思路&#xff0c…...

网络空间安全(1)web应用程序的发展历程

前言 Web应用程序的发展历程是一部技术创新与社会变革交织的长卷&#xff0c;从简单的文档共享系统到如今复杂、交互式、数据驱动的平台&#xff0c;经历了多个重要阶段。 一、起源与初期发展&#xff08;1989-1995年&#xff09; Web的诞生&#xff1a; 1989年&#xff0c;欧洲…...

ABAQUS功能梯度材料FGM模型

功能梯度材料&#xff08;FGM&#xff09;作为一种新型复合材料&#xff0c;通过材料内部成分或微观结构的梯度变化&#xff0c;优化特定性能适应复杂环境&#xff0c;被广泛应用于高温防护、结构优化、生物医学、光电设备等领域。本案例介绍在ABAQUS内建立功能梯度材料模型。 …...

自适应增强技术

1. 传统图像处理中的自适应增强&#xff08;如CLAHE&#xff09; 难度&#xff1a;⭐容易 实现方式&#xff1a;调用成熟的库&#xff08;如OpenCV&#xff09;函数即可完成。 示例代码&#xff08;CLAHE增强&#xff09;&#xff1a; <PYTHON> import cv2# 输入灰度或彩…...

虚拟项目:一个好用的工具平台

在当今数字化的时代&#xff0c;虚拟项目如雨后春笋般涌现&#xff0c;为人们提供了诸多便捷且充满机遇的选择。以下将为大家详细介绍几种颇具特色的虚拟项目&#xff0c;包括书签、资源站、题库、虚拟商城、专栏、证件照以及分站搭建等&#xff0c;一起来了解它们各自的独特之…...

MySQL 和 Elasticsearch 之间的数据同步

MySQL 和 Elasticsearch 之间的数据同步是常见的需求&#xff0c;通常用于将结构化数据从关系型数据库同步到 Elasticsearch 以实现高效的全文搜索、聚合分析和实时查询。以下是几种常用的同步方案及其实现方法&#xff1a; 1. 应用层双写&#xff08;双写模式&#xff09; 原…...

PS裁剪工具

裁剪&#xff1a; 多张图同一标准裁剪&#xff1a;裁剪–》前面的图像–》选择其他图像–》 确定 选区–》裁剪工具–》确定&#xff1a;选区制作矩形裁剪 裁剪–》拉直 裁剪–》内容识别&#xff1a;当裁剪大于图片大小&#xff0c;会自动填充空白区域 &#xff08;栅格化图层…...

[Web 安全] PHP 反序列化漏洞 —— PHP 序列化 反序列化

关注这个专栏的其他相关笔记&#xff1a;[Web 安全] 反序列化漏洞 - 学习笔记-CSDN博客 0x01&#xff1a;PHP 序列化 — Serialize 序列化就是将对象的状态信息转化为可以存储或传输的形式的过程&#xff0c;在 PHP 中&#xff0c;通常使用 serialize() 函数来完成序列化的操作…...

QT入门--QMainWindow

从上向下依次是菜单栏&#xff0c;工具栏&#xff0c;铆接部件&#xff08;浮动窗口&#xff09;&#xff0c;状态栏&#xff0c;中心部件 菜单栏 创建菜单栏 QMenuBar* mybar1 menuBar(); 将菜单栏放到窗口中 setMenuBar(mybar1); 创建菜单 QMenu *myfilemenu mybar1-…...

C++ | 高级教程 | 信号处理

&#x1f47b; 概念 信号 —— 操作系统传给进程的中断&#xff0c;会提早终止程序有些信号不能被程序捕获&#xff0c;有些则可以被捕获&#xff0c;并基于信号采取适当的动作 信号描述SIGABRT程序的异常终止&#xff0c;如调用 abortSIGFPE错误的算术运算&#xff0c;比如除…...

最新前端框架选型对比与建议(React/Vue/Svelte/Angular)

前端框架选型对比与建议&#xff08;React/Vue/Svelte/Angular&#xff09; 一、核心框架技术特性对比&#xff08;基于最新版本&#xff09; 维度React 19 25Vue 3.5 12Svelte 5 25Angular 19 5核心理念函数式编程、JSX语法、虚拟DOM渐进式框架、组合式API、模板语法编译时框…...

游戏引擎学习第123天

仓库:https://gitee.com/mrxiao_com/2d_game_3 黑板&#xff1a;线程同步/通信 目标是从零开始编写一个完整的游戏。我们不使用引擎&#xff0c;也不依赖任何库&#xff0c;完全自己编写游戏所需的所有代码。我们做这个节目不仅是为了教育目的&#xff0c;同时也是因为编程本…...

计算机网络:从底层原理到前沿应用,解锁数字世界的连接密码

计算机网络&#xff1a;从底层原理到前沿应用&#xff0c;解锁数字世界的连接密码 在信息如洪流般奔涌的时代&#xff0c;计算机网络宛如无形的脉络&#xff0c;贯穿于我们生活的每一个角落。它不仅是数据传输的通道&#xff0c;更是连接全球、驱动创新的核心力量。从日常的网络…...

grafana K6压测

文章目录 install and runscript.jsoptions最佳实践 report 解析 https://grafana.com/docs/k6/latest/get-started install and run install # mac brew install k6当前目录下生成压测脚本 # create file script.js k6 new [filename] # create file ‘script.js’ in …...

Vue的组合式API和选项式API有什么区别

Vue3的组合式API&#xff08;Composition API&#xff09;和选项式API&#xff08;Options API&#xff09;是两种不同的组件编写方式&#xff0c;主要区别如下&#xff1a; 1. 代码组织方式 选项式API&#xff1a; 按照选项&#xff08;如data、methods、computed等&#xff0…...

变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析

一、变量声明设计&#xff1a;let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性&#xff0c;这种设计体现了语言的核心哲学。以下是深度解析&#xff1a; 1.1 设计理念剖析 安全优先原则&#xff1a;默认不可变强制开发者明确声明意图 let x 5; …...

【OSG学习笔记】Day 18: 碰撞检测与物理交互

物理引擎&#xff08;Physics Engine&#xff09; 物理引擎 是一种通过计算机模拟物理规律&#xff08;如力学、碰撞、重力、流体动力学等&#xff09;的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互&#xff0c;广泛应用于 游戏开发、动画制作、虚…...

从WWDC看苹果产品发展的规律

WWDC 是苹果公司一年一度面向全球开发者的盛会&#xff0c;其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具&#xff0c;对过去十年 WWDC 主题演讲内容进行了系统化分析&#xff0c;形成了这份…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八

现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet&#xff0c;点击确认后如下提示 最终上报fail 解决方法 内核升级导致&#xff0c;需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...

为什么需要建设工程项目管理?工程项目管理有哪些亮点功能?

在建筑行业&#xff0c;项目管理的重要性不言而喻。随着工程规模的扩大、技术复杂度的提升&#xff0c;传统的管理模式已经难以满足现代工程的需求。过去&#xff0c;许多企业依赖手工记录、口头沟通和分散的信息管理&#xff0c;导致效率低下、成本失控、风险频发。例如&#…...

【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验

系列回顾&#xff1a; 在上一篇中&#xff0c;我们成功地为应用集成了数据库&#xff0c;并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了&#xff01;但是&#xff0c;如果你仔细审视那些 API&#xff0c;会发现它们还很“粗糙”&#xff1a;有…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。

1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj&#xff0c;再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...

Angular微前端架构:Module Federation + ngx-build-plus (Webpack)

以下是一个完整的 Angular 微前端示例&#xff0c;其中使用的是 Module Federation 和 npx-build-plus 实现了主应用&#xff08;Shell&#xff09;与子应用&#xff08;Remote&#xff09;的集成。 &#x1f6e0;️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...

以光量子为例,详解量子获取方式

光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学&#xff08;silicon photonics&#xff09;的光波导&#xff08;optical waveguide&#xff09;芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中&#xff0c;光既是波又是粒子。光子本…...