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:
YV12:
YUV420SP
该格式,UV交错分布,根据UV的先后顺序分为NV12和NV21(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标准,实际上有多种标准,每种标准系数不同
- ITU-R BT.601(SDTV 标准,适用于 Android NV21)
- ITU-R BT.709(HDTV 标准,适用于 1080p 及以上视频)
- 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_LUMINANCE 和 GL_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个变量samplerY、samplerU、samplerV、samplerUV
- 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
人生如逆旅,我亦是行人 Android OpenGLES开发:EGL环境搭建Android OpenGLES2.0开发(一):艰难的开始Android OpenGLES2.0开发(二):环境搭建Android OpenGLES2.0开发(三&am…...
7种内外网数据交换方案全解析 哪种安全、高效、合规?
内外网数据交换方案主要解决了企业跨网络数据传输中的安全、效率与合规性问题。通过采用先进的加密技术、高效的数据传输协议以及严格的审批和审计机制,该方案确保了数据在内外网之间的安全交换,同时提高了传输效率,并满足了企业对数据合规性…...
详解:事务注解 @Transactional
创作内容丰富的干货文章很费心力,感谢点过此文章的读者,点一个关注鼓励一下作者,激励他分享更多的精彩好文,谢谢大家! Transactional 是 Spring Framework 中常用的注解之一,它可以被用于管理事务。通过使…...
2025最新Flask学习笔记(对照Django做解析)
前言:如果还没学Django的同学,可以看Django 教程 | 菜鸟教程,也可以忽略下文所提及的Django内容;另外,由于我们接手的项目大多都是前后端分离的项目,所以本文会跳过对模板的介绍,感兴趣的朋友可…...
大模型面试问题准备
1. BERT的多头注意力为什么需要多头? 为了捕捉不同子空间的语义信息,每个头关注不同的方面,增强模型的表达能力 2. 什么是softmax上下溢出问题? 问题描述: 上溢出:ye^x中,如果x取非常大的正数…...
FFmpeg 命令行全解析:高效音视频处理从入门到精通
FFmpeg FFmpeg 是一款开源的多媒体处理工具集,支持音视频编解码、格式转换、流媒体处理等全链路操作。核心功能与工具: 多媒体全链路支持 支持 1000+ 音视频编解码格式(如 H.264、HEVC、AV1)和协议(RTMP、RTSP、HLS),覆盖录制、转码、流化等全流程。提供三大核心工具: …...
在使用LomBok时编译器弹出java: 错误: 不支持发行版本 5该怎么解决的四种方案
你遇到的错误 java: 错误: 不支持发行版本 5 表明你的代码正在尝试使用 Java 5 或更早版本的编译器,而这些版本已经不再受支持,并且可能与你当前使用的 JDK 版本不兼容。以下是解决此问题的步骤: 1. 检查项目语言级别 确保你的项目配置为使…...
【数据结构】(12) 反射、枚举、lambda 表达式
一、反射 1、反射机制定义及作用 反射是允许程序在运行时检查和操作类、方法、属性等的机制,能够动态地获取信息、调用方法等。换句话说,在编写程序时,不需要知道要操作的类的具体信息,而是在程序运行时获取和使用。 2、反射机制…...
在VSCode中安装jupyter跑.ipynb格式文件
个人用vs用的较多,不习惯在浏览器单独打开jupyter,看着不舒服,直接上教程。 1、在你的环境中pip install ipykernel 2、在vscode的插件中安装jupyter扩展 3、安装扩展后,打开一个ipynb文件,并且在页面右上角配置内核 …...
WordPress网站502错误全面排查与解决指南
502 Bad Gateway错误是WordPress站长最常遇到的服务器问题之一,它意味着服务器作为网关或代理时,未能从上游服务器获取有效响应。针对WP可能出现的502问题,本文提供一些基础到进阶的解决方案供大家参考:) 一、502错误的本质和核心诱因 502错误属于HTTP状态码中的5xx系列,…...
锂电池保护板测试仪:电池安全的守护者与创新驱动力
在新能源产业蓬勃发展的今天,锂电池以其高能量密度、长循环寿命和环保特性,成为电动汽车、无人机、便携式电子设备等领域不可或缺的能量来源。然而,锂电池的安全性和稳定性一直是行业关注的焦点。为了确保锂电池在各种应用场景下的可靠运行&a…...
flowable-ui 的会签功能实现
场景:在进行智慧保时通开发时,有个协作合同入围功能,这个功能的流程图里有个评审小组,这个评审小组就需要进行会签操作,会签完成后,需要依据是否有不通过的情况选择下一步走的流程 思考步骤: 首…...
Python学习第十七天之PyTorch保姆级安装
PyTorch安装与部署 一、准备工作二、pytorch介绍三、CPU版本pytorch安装1. 创建虚拟环境2. 删除虚拟环境1. 通过环境名称删除2. 通过环境路径删除 3. 配置镜像源4. 安装pytorch1. 首先激活环境变量2. 进入pytorch官网,找到安装指令 5. 验证pytorch是否安装成功 四、…...
Kibana:Spotify Wrapped 第二部分:深入挖掘数据
作者:来自 Elastic Philipp Kahr 我们将比以往更深入地探究你的 Spotify 数据并探索你甚至不知道存在的联系。 在由 Iulia Feroli 撰写的本系列的第一部分中,我们讨论了如何获取 Spotify Wrapped 数据并在 Kibana 中对其进行可视化。在第 2 部分中&#…...
半导体晶圆精控:ethercat转profient网关数据提升制造精度
数据采集系统通过网关连接离子注入机,精细控制半导体晶圆制造过程中的关键参数。 在半导体制造中,晶圆制造设备的精密控制是决定产品性能的关键因素。某半导体工厂采用耐达讯Profinet转EtherCAT协议网关NY-PN-ECATM,将其数据采集系统与离子注…...
CMake小结2(PICO为例)
1 前言 之前写过一篇cmake,不过很简单:CMake小结_cmake ${sources}-CSDN博客 构建系统现在真的太多了,完全学不过来的感觉,meson,gardle,buildroot, Maven。。。我是真的有点放弃治疗了。之前…...
5. Go 方法(结构体的方法成员)
Go语言没有传统的 class ,为了让函数和结构体能够关联,Go引入了“方法”的概念。 当普通函数添加了接收者(receiver)后,就变成了方法。 一、函数和方法示例 // 普通函数 func Check(s string) string {return s }//…...
Linux查看和处理文件内容
1.文本文件 有字符集编码的文件 如:ASCII、UTF-8、Unicode、ANSI等 常见的文本文件 txt、xml、conf、properties、yml等配置文件、日志文 件、源代码 2.二进制文件 除文本文件外的文件 如:可执行程序、图片、音频、视频 3.cat 格式:…...
关于网络端口探测:TCP端口和UDP端口探测区别
网络端口探测是网络安全领域中的一项基础技术,它用于识别目标主机上开放的端口以及运行在这些端口上的服务。这项技术对于网络管理和安全评估至关重要。在网络端口探测中,最常用的两种协议是TCP(传输控制协议)和UDP(用…...
Spring IoC和DI
Spring IoC和DI 1 IOC1.1 什么是IoC?1.2 IoC 介绍1.2.1 传统程序开发1.2.2 问题分析1.2.3 IoC程序开发1.2.3 IoC 优势 1.3 DI 介绍 2. IoC & DI 使⽤IoC 详解Bean的存储1.1 Controller(控制器存储) 1.2 Service(服务存储&…...
centos9之ESXi环境下安装
一、centos9简介 CentOS Stream 9是一个基于RHEL(Red Hat Enterprise Linux)的开源操作系统。它是CentOS Stream系列的最新版本。CentOS Stream是一个中间发行版,位于RHEL和Fedora之间,旨在提供更及时的软件更新和新功能。CentOS …...
【论文学习】基于规模化Transformer模型的低比特率高质量语音编码
以下文章基于所提供的文档内容撰写,旨在对该论文“Scaling Transformers for Low-Bitrate High-Quality Speech Coding”进行较为系统和深入的分析与总结。 论文地址:https://arxiv.org/pdf/2411.19842 一、研究背景与动机 自20世纪70年代以来ÿ…...
Docker 2025/2/24
用来快速构建、运行和管理应用的工具。帮助部署。 快速入门 代码略 解释 docker run :创建并运行一个容器,-d是让容器在后台运行 --name mysql :给容器起个名字,必须唯一 -p 3306:3306 :设置端口映射 -e KEYVALUE :是设置环境变量 mysql :指定运行的…...
Rust语言基础知识详解【一】
1.在windows上安装Rust Windows 上安装 Rust 需要有 C 环境,以下为安装的两种方式: 1. x86_64-pc-windows-msvc(官方推荐) 先安装 Microsoft C Build Tools,勾选安装 C 环境即可。安装时可自行修改缓存路径与安装路…...
Kronecker分解(K-FAC):让自然梯度在深度学习中飞起来
Kronecker分解(K-FAC):让自然梯度在深度学习中飞起来 在深度学习的优化中,自然梯度下降(Natural Gradient Descent)是一个强大的工具,它利用Fisher信息矩阵(FIM)调整梯度…...
Ubutu部署WordPress
前言 什么是word press WordPress是一种使用PHP语言开发的建站系统,用户可以在支持PHP和MySQL数据库的服务器上架设WordPress。它是一个开源的内容管理系统(CMS),允许用户构建动态网站和博客。现在的WordPress已经强大到几乎可以…...
请解释 React 中的 Hooks,何时使用 Hooks 更合适?
一、Hooks 核心理解 1. 什么是 Hooks? Hooks 是 React 16.8 引入的函数式编程范式,允许在函数组件中使用状态管理和生命周期能力。就像给函数组件装上了"智能芯片",让原本只能做简单展示的组件具备了处理复杂逻辑的能力。 2. 类…...
在Linux桌面上创建Idea启动快捷方式
1、在桌面新建idea.desktop vim idea.desktop [Desktop Entry] EncodingUTF-8 NameIntelliJ IDEA CommentIntelliJ IDEA Exec/home/software/idea-2021/bin/idea.sh Icon/home/software/idea-2021/bin/idea.svg Terminalfalse TypeApplication CategoriesApplication;Developm…...
如何在netlify一键部署静态网站
1. 准备你的项目 确保你的静态网站文件(如 HTML、CSS、JavaScript、图片等)都在一个文件夹中。通常,项目结构如下: my-static-site/ ├── index.html ├── styles/ │ └── styles.css └── scripts/└── script.js…...
网页制作09-html,css,javascript初认识のhtml如何使用表单
表单主要用来收集客户端提供的相关信息。,使网页具有交互作用。在网页制作的过程中,常常需要使用表单,如进行会员注册,网上调查和搜索等 访问者可以使用如文本域列表框,复选框以及单选按钮之类的表单对象输入信息,然后…...
Linux 命令大全完整版(03)
1. 系统管理命令 screen 功能说明:多重视窗管理程序。语 法:screen [-AmRvx -ls -wipe][-d <作业名称>][-h <行数>][-r <作业名称>][-s <shell>][-S <作业名称>]补充说明:screen 为多重视窗管理程序。此处…...
【新人系列】Python 入门专栏合集
✍ 个人博客:https://blog.csdn.net/Newin2020?typeblog 📝 专栏地址:https://blog.csdn.net/newin2020/category_12801353.html 📣 专栏定位:为 0 基础刚入门 Python 的小伙伴提供详细的讲解,也欢迎大佬们…...
嵌入式软件数据结构(一)链表知识点专栏 附源码 附原理
嵌入式软件数据结构(一)链表知识点专栏 附源码 附原理 前言: 首先我们要知道什么是链表? 什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指…...
order by布尔盲注、时间盲注
pdo防御下,order by、limit不能参数绑定,可以进行sql注入 案例:靶场的less-46 布尔盲注: import requests from lxml import htmldef get_id_one(URL, paload):res requests.get(urlURL, paramspaload)tree html.fromstring(…...
数据结构与算法-图论-最短路-拓展运用
选择最佳路线 分析: 这是一道图论中的最短路径问题,目标是在给定的公交网络中,找到从琪琪家附近的车站出发,到她朋友家附近车站(编号为 s )的最短时间。以下是对该问题的详细分析: 问题关键信息…...
数据开发的简历及面试
简历 个人信息: 邮箱别写QQ邮箱, 写126邮箱/189邮箱等 学历>>本科及以上写,大专及以下不写 专业>>非计算机专业不写 政治面貌>>党员写, 群众不用写 掌握的技能: 精通 > 熟悉 > 了解 专业工具: 大数据相关的 公司: 如果没有可以写的>>金融服…...
android s下make otapackage编译失败
[DESCRIPTION] android s上,我司推荐使用split build的方式进行编译,但是部分客户依旧会采用AOSP full build的方式进行编译。而我司在这块release的时候,并未进行验证。因此执行make otapackage的时候,会出现如下报错。 [0312/…...
【算法通关村 Day12】字符串
字符串青铜挑战 字符串转换 转换为小写字母 给你一个字符串 s ,将该字符串中的大写字母转换成相同的小写字母,返回新的字符串。leetcode791 public class ToLowerCase {/*** 将字符串中的大写字母转换为小写字母。** param s 输入字符串* return 转换…...
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 …...
SQLite 安装教程以及可视化工具介绍
目录 简述 1. Windows 系统安装 1.1 下载预编译的二进制文件 1.2 解压文件 1.3 配置环境变量 1.4 验证安装 2. GUI 可视化工具 2.1 免费工具 2.1.1 DB Browser for SQLite 2.1.2 SQLiteStudio 2.1.3 SQLite Expert 2.1.4 SQLiteGUI 2.1.5 Antares SQL 2.1.6 DbGa…...
谷云科技iPaaS×DeepSeek:构建企业智能集成的核心底座
2025年,DeepSeek大模型的爆发式普及,正引领软件行业实现 “智能跃迁”。从代码生成到系统集成,从企业级应用到消费级产品,自然语言交互能力已成为新一代软件的核心竞争力。据行业分析,超60%的软件企业已启动大模型适配…...
mac 下 java 调用 gurobi 不能加载 jar
在 mac 电脑中的 java 始终不能加载 gurobi 的 jar 包,java 的开发软件 eclipse,idea 总是显示找不到 gurobi 的 jar 包,但是 jar 包明明就在那里。 摸索了三个小时,最后发现原因竟然是: jar 包太新,替换…...
京准电钟:NTP精密时钟服务器在自动化系统中的作用
京准电钟:NTP精密时钟服务器在自动化系统中的作用 京准电钟:NTP精密时钟服务器在自动化系统中的作用 NTP精密时钟服务器在自动化系统中的作用非常重要,特别是在需要高精度时间同步的场景中。NTP能够提供毫秒级的时间同步精度,这…...
京东-零售-数据研发面经【附答案】
近期,有参加春招的同学和我交流了他的面试历程,我针对这些内容进行了细致的总结与梳理,并在此分享出来,希望能助力大家学习与借鉴。 1.八股文 1)HashMap的底层原理是什么【见V6.0面试笔记 Java基础部分第19题】 2&am…...
面试之《react hooks在源码中是怎么实现的?》
要深入理解 React Hooks 在源码中的实现,可以从以下几个关键方面来剖析: 核心数据结构 在 React 内部,使用链表来管理每个函数组件的 Hooks。每个 Hook 对应一个节点,这些节点通过 next 指针相连。以下是简化后的 Hook 节点结构…...
泛型的约束有哪几种?(C#)
目录 1 值类型约束(where T : struct) 2 引用类型约束(where T : class) 3 无参构造函数约束(where T : new ()) 4 基类约束(where T : <基类名>) 5 接口约束(…...
Harmony开发笔记(未完成)
一、感想 作为一名拥有11年经验的Android开发者,我亲历了Android从高速发展到如今面临“僧多粥少”的过程。技术的世界瞬息万变,没有一种技术能够让人依赖一辈子。去年初,我自学了鸿蒙系统,并顺利通过了鸿蒙官方的初级和高级认。…...
DevSecOps普及:安全与开发运维的深度融合
一、引言 随着软件开发模式的演进,DevOps已成为现代软件工程的主流实践。然而,在传统的DevOps流程中,安全往往被视为开发和运维之外的额外环节,导致安全漏洞在产品交付后才被发现,增加了修复成本和风险。为了解决这一…...
JavaScript 系列之:Ajax、Promise、Axios
前言 同步:会阻塞。同步代码按照编写的顺序逐行依次执行,只有当前的任务完成后,才会执行下一个任务。 异步:异步代码不会阻塞后续代码的执行。当遇到异步操作时,JavaScript 会将该操作放入任务队列中,继续…...
為什麼使用不限量動態住宅IP採集數據?
在瞭解“不限量動態住宅IP數據採集”之前,我們需要先搞清楚什麼是“動態住宅IP”。簡單來說,動態IP是一種會定期變化的IP地址,通常由互聯網服務提供商(ISP)分配給家庭用戶。與固定IP(靜態IP)不同…...