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.rhkb.cn/news/24114.html

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

相关文章

deep-research 专用评测数据集

Deep Research自2025年2月初由OpenAI推出后迅速引发全球关注&#xff0c;其通过端到端强化学习技术实现多步骤研究任务自动化&#xff0c;能在数十分钟内生成分析师水平报告&#xff0c;效率远超人类&#xff08;耗时从30分钟到30天不等&#xff09;&#xff0c;被学者评价为“…

SQL之order by盲注

目录 一.order by盲注的原理 二.注入方式 a.布尔盲注 b.时间盲注 三.防御 一.order by盲注的原理 order by子句是用于按指定列排序查询结果&#xff0c;列名或列序号皆可。 order by 后面接的字段或者数字不一样&#xff0c;那么这个数据表的排序就会不同。 order by 盲…

基于javaweb的SSM+Maven疫情物业系统设计和实现(源码+文档+部署讲解)

技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论…

提升数据洞察力:五款报表软件助力企业智能决策

概述 随着数据量的激增和企业对决策支持需求的提升&#xff0c;报表软件已经成为现代企业管理中不可或缺的工具。这些软件能够帮助企业高效处理数据、生成报告&#xff0c;并将数据可视化&#xff0c;从而推动更智能的决策过程。 1. 山海鲸报表 概述&#xff1a; 山海鲸报表…

IP-------GRE和MGRE

4.GRE和MGRE 1.应用场景 现实场景 居家工作&#xff0c;公司工作&#xff0c;分公司工作----------需要传输交换数据--------NAT---在该场景中需要两次NAT&#xff08;不安全&#xff09; 为了安全有两种手段-----1.物理专线---成本高 2.VPN--虚拟专用网---隧道技术--封装技…

音乐游戏Drummania(GITADORA)模拟器

文章目录 &#xff08;一&#xff09;Drummania和GITADORA&#xff08;1.1&#xff09;基本情况&#xff08;1.2&#xff09;机体 &#xff08;二&#xff09;模拟器&#xff08;2.1&#xff09;主程序&#xff08;2.2&#xff09;模拟器主题 &#xff08;三&#xff09;曲谱文…

gotool在线工具集

1. 包含各种 sql 处理 2. 包含 json 处理 3. 包含 图片处理 4. 跨平台传输 gotool

点击修改按钮图片显示有问题

问题可能出在表单数据的初始化上。在 ave-form.vue 中&#xff0c;我们需要处理一下从后端返回的图片数据&#xff0c;因为它们可能是 JSON 字符串格式。 vue:src/views/tools/fake-strategy/components/ave-form.vue// ... existing code ...Watch(value)watchValue(v: any) …

绩效管理与业务流程

绩效管理本质就是价值管理&#xff0c;或者说是能力管理&#xff0c;也就是通过一系列的科技手段去发现、证明一个人的能力和价值&#xff0c;然后给予科学、合理的利益分配。业务流程就是把企业的每一个零部件或者说齿轮都有效组合起来形成一个有机体为市场提供自己的独特价值…

Nginx处理http的流程

文章目录 前言一、发版本后旧版本可以用项目基本情况Nginx 配置**解释每一行的作用&#xff1a;****表现和行为&#xff1a;****适用场景**&#xff1a;资源的缓存策略 在这里插入图片描述 二&#xff0c; nginx处理http的流程Nginx 的 GitHub 源码地址 **Nginx 核心源码解读&a…

QT各种版本下载安装

参考链接&#xff1a; 【Qt】超详细&#xff01;Qt4.8.6和VS2010的配置及使用 由于QT官网一般现在进不去&#xff0c;所以下载一些QT版本只能通过镜像或者以前下载存储的安装包来进行&#xff0c;现在推荐两种方法 从参考链接中搬过来&#xff1a; 方案一&#xff1a;国内镜…

【STM32H743IIT6】STM32H7的ADC时钟频率设置问题 —— 网上大多文章未注意到的要点!

前言 我使用的是定时器触发ADC采样。最近在想达到ADC的最高采样率的时候&#xff0c;发现一直却卡在1Msps上不去&#xff0c;直到在硬汉嵌入式的论坛里才发现了答案&#xff1a;[ADC] STM32H743/H750的Y版和V版芯片ADC的主频区别 这篇文章就详细的讲一下这个问题&#xff0c;这…

2024-2025 学年广东省职业院校技能大赛 “信息安全管理与评估”赛项 技能测试试卷(四)

2024-2025 学年广东省职业院校技能大赛 “信息安全管理与评估”赛项 技能测试试卷&#xff08;四&#xff09; 第一部分&#xff1a;网络平台搭建与设备安全防护任务书第二部分&#xff1a;网络安全事件响应、数字取证调查、应用程序安全任务书任务 1&#xff1a;应急响应&…

touchgfx的工作机制

touchgfx的工作机制 一.MVP软件架构 MVP的全称为Model-View-Presenter Model: 就是数据部分,在整个touchgfx应用中,只有一个Model类实例对象,它为所有的Screen屏幕界面服务,可以理解成是一个全局变量区,同时它还负责和后端系统通信 View: 就是UI界面部分,对应于View类,在整…

在 Ansys Mechanical 中解决干涉拟合

有意和无意的过盈配合在工程设计和有限元分析 &#xff08;FEA&#xff09; 中很常见。当两个组件重叠或接触时&#xff0c;就会发生这种情况&#xff0c;从而产生应力和变形&#xff0c;必须仔细分析以确保功能正常。有意干涉&#xff0c;例如轴和轴承之间的压配合或用于固定金…

Linux设备驱动开发-SPI驱动开发详解(包含设备树处理详细过程)

基础知识及 SPI 相关结构体介绍 引脚&#xff1a;MISO&#xff08;master 输入&#xff0c;slave 输出&#xff09;&#xff0c;MOSI&#xff08;master 输出&#xff0c;slave 输入&#xff09;&#xff0c;片选引脚&#xff0c;SCK&#xff08;时钟&#xff09; 控制寄存器&…

Visual Studio 中的 /MD 与 /MT、动态库与静态库的深入解析

文章目录 1. /MD 与 /MT 的区别1.3 调试版本1.4 注意事项 2. 动态库与静态库的联系与区别2.3 联系与区别 3. 结合你的错误分析3.1 错误原因3.2 解决方案3.3 经验教训 4. 总结 在 Visual Studio 中进行 C/C 项目开发时&#xff0c;开发者经常需要对运行时库选项&#xff08;例如…

蓝桥杯备考:贪心算法之矩阵消除游戏

这道题是牛客上的一道题&#xff0c;它呢和我们之前的排座位游戏非常之相似&#xff0c;但是&#xff0c;排座位问题选择行和列是不会改变元素的值的&#xff0c;这道题呢每每选一行都会把这行或者这列清零&#xff0c;所以我们的策略就是先用二进制把选择所有行的情况全部枚举…

绿色节能|暴雨服务器荣获液冷装置新专利授权

近日&#xff0c;暴雨信息宣布“液冷装置及电子设备”专利正式获批&#xff0c;标志着暴雨信息在电子设备散热领域取得重大技术突破&#xff0c;为数据中心及高性能电子设备的散热需求提供了创新性解决方案。 双重密封零漏液保障 传统冷板技术存在液体泄漏风险&#xff0c;严…

萌新学 Python 之闭包函数

闭包函数&#xff1a;在一个函数体内嵌套函数&#xff0c;是一个函数对象&#xff0c;允许在内部函数中修改或引用外部函数的变量 闭包函数对数据有封存功能 闭包函数需要满足以下几个条件&#xff1a; 1.函数必须有一个嵌套函数&#xff0c;在定义函数时&#xff0c;内部再…