欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

在Android-OpenGLES3绘图中实现YUV420到RGB的转换

最编程 2024-08-15 15:28:23
...

之前写过一篇 Android YUV图像转换算法和检测工具,里面实现了YUV420的四种格式的相互转换,和与RGB之间的转换。因为是直接用CPU计算的,所以对CPU有一定的消耗和占用。这里我们用OpenGL实现GPU转换。

我们用相机作为YUV420图像输入,上一篇 Android OpenGLES3绘图 - 使用CameraX 中实现了用OpenGL的OES纹理直接显示相机的SurfaceTexture。如果要进行格式转换,就不能这样用一个纹理直接显示了,显示YUV420数据的方法是:创建三个纹理,分别接受Y、U、V数据,在片段着色器中取出当前位置的Y、U、V数据,用公式转换成rgb格式显示。

如图,中间是相机预览画面,左下角是相机的YUV420数据通过OpenGL转换成RGB显示到GLSurfaceView上。

1 创建YUVShader

跟绘制普通的纹理类似,创建vao、vbo、ebo,绘制一个矩形。不同的是,需要用for循环创建3个纹理tex0、tex1和tex2,传给着色器。

class YUVShader {

    var vertices = floatArrayOf( //     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -
        -1f, -1f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,  // 左下
        1f, -1f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,  // 右下
        -1f, 1f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, // 左上
        1f, 1f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f  // 右上
    )

    val indices = intArrayOf( // 注意索引从0开始!
        0, 1, 2,  // 第一个三角形
        1, 2, 3 // 第二个三角形
    )

    var program = 0
    var vertexBuffer: FloatBuffer? = null
    var intBuffer: IntBuffer? = null
    var vao: IntArray = IntArray(1)

    var tex: IntArray = IntArray(3) // yuv

    var transform = FloatArray(16)

    fun init() {
        program = ShaderUtils.loadProgramYUV()
        //分配内存空间,每个浮点型占4字节空间
        vertexBuffer = ByteBuffer.allocateDirect(vertices.size * 4)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
        //传入指定的坐标数据
        vertexBuffer!!.put(vertices)
        vertexBuffer!!.position(0)
        vao = IntArray(1)
        glGenVertexArrays(1, vao, 0)
        glBindVertexArray(vao[0])
        val vbo = IntArray(1)
        glGenBuffers(1, vbo, 0)
        glBindBuffer(GL_ARRAY_BUFFER, vbo[0])
        glBufferData(GL_ARRAY_BUFFER, vertices.size * 4, vertexBuffer, GL_STATIC_DRAW)

        intBuffer = IntBuffer.allocate(indices.size * 4)
        intBuffer!!.put(indices)
        intBuffer!!.position(0)
        val ebo = IntArray(1)
        glGenBuffers(1, ebo, 0)
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[0])
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size * 4, intBuffer, GL_STATIC_DRAW)

        glUseProgram(program)
        glGenTextures(3, tex, 0)
        for (i in 0..2) {
            glActiveTexture(GL_TEXTURE0 + i)
            glBindTexture(GL_TEXTURE_2D, tex[i])
            // 为当前绑定的纹理对象设置环绕、过滤方式
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
//            val bitmap: Bitmap = ShaderUtils.loadImageAssets("face.png")
//            GLUtils.texImage2D(GL_TEXTURE_2D, 0, bitmap, 0)
//            glGenerateMipmap(GL_TEXTURE_2D)

            val loc0 = glGetUniformLocation(program, "tex$i")
            glUniform1i(loc0, i)
        }

        // Load the vertex data
        glVertexAttribPointer(0, 3, GL_FLOAT, false, 8 * 4, 0)
        glEnableVertexAttribArray(0)
        glVertexAttribPointer(1, 3, GL_FLOAT, false, 8 * 4, 3 * 4)
        glEnableVertexAttribArray(1)
        glVertexAttribPointer(2, 2, GL_FLOAT, false, 8 * 4, 6 * 4)
        glEnableVertexAttribArray(2)

        glBindBuffer(GL_ARRAY_BUFFER, 0)
        glBindVertexArray(0)

        Matrix.setIdentityM(transform, 0)
    }

2 数据传给纹理

这是最关键的一步

每一帧画面需要更新YUV数据,如何将数据传给纹理呢?就需要用到glTexImage2D这个方法,方法有很多参数,主要是设置纹理格式、宽高等。方法的第3个和第8个参数是相同的,Y平面这个参数传GL_LUMINANCE,至于U和V平面:I420和YV12格式下,传GL_LUMINANCE参数;NV12和NV21格式下,传GL_LUMINANCE_ALPHA。这里如果弄错了,画面必错乱,我也是调了很久,搜索了不少地方才总结下来的。

至于宽高参数,Y平面就是图像宽高,UV平面的宽高都是一半。最后一个参数是真正的数据,传入ByteBuffer格式的。

其实NV12和NV21格式,UV平面的数据放在一起了,因此使用前两个纹理就够了。后面在着色器里使用时可以看出来。

    fun draw(ib: ImageBytes) {
        val w0 = ib.width
        val h0 = ib.height
        val w1 = w0 / 2
        val h1 = h0 / 2
        glUseProgram(program)

        glActiveTexture(tex[0])
        glBindTexture(GL_TEXTURE_2D, tex[0])
        glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, w0, h0, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, ib.bufY)

        glActiveTexture(tex[1])
        glBindTexture(GL_TEXTURE_2D, tex[1])
        glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, w1, h1, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, ib.bufU)
//        glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, w1, h1, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, ib.bufU)

        glActiveTexture(tex[2])
        glBindTexture(GL_TEXTURE_2D, tex[2])
        glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, w1, h1, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, ib.bufV)
//        glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, w1, h1, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, ib.bufV)

        val loc = glGetUniformLocation(program, "transform")
        glUniformMatrix4fv(loc, 1, false, transform, 0)

        glBindVertexArray(vao[0])
        glDrawElements(GL_TRIANGLES, vertices.size, GL_UNSIGNED_INT, 0)
    }

3 数据获取和封装

Camra1与Camera2/X获取数据的方法不同,格式也不一样。

Camera1是在onPreviewFrame回调方法中获取byte[]格式的数据

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        if(camera == null) return;
        Camera.Size size = camera.getParameters().getPreviewSize(); //获取预览大小
        final int w = size.width;
        final int h = size.height;
        ydv.inputAsync(data, w, h);
    }

Camera2/X是在ImageAnalysis.Analyzer的analyze回调方法中获取Image格式的数据

    private inner class LuminosityAnalyzer(private val listener : LumaListener) : ImageAnalysis.Analyzer {

        private fun ByteBuffer.toByteArray() : ByteArray {
            rewind()
            val data = ByteArray(remaining())
            get(data)
            return data
        }

        @SuppressLint("UnsafeExperimentalUsageError")
        override fun analyze(image: ImageProxy) {

            // 暂时用SurfaceTexture中获取的变换矩阵
            this@MainActivity.yuvDetectView.yuvRender.yuvShader.transform = this@MainActivity.cameraRender.transform

            this@MainActivity.yuvDetectView.input(image.image!!)
            image.close()
        }

    }

为了方便处理,自己封装一个ImageBytes类,将byte[]或Image格式的数据转换成对应YUV的三个ByteBuffer。

Image格式的直接取三个Plane包装成ByteBuffer就行;byte[]格式的需要根据宽高确定YUV的长度,将对应长度的子数组包装成ByteBuffer。(注意:Camera1的NV21格式,需要所有的UV数据都放到第二个ByteBuffer里面,否则画面只有一半显示正常,这应该是相机扫描方向跟数据读取方向不同导致的)。

public class ImageBytes {
    public byte[] bytes;
    public int width;
    public int height;

    public ByteBuffer bufY;
    public ByteBuffer bufU;
    public ByteBuffer bufV;

    public ImageBytes(byte[] bytes, int width, int height) {
        this.bytes = bytes;
        this.width = width;
        this.height = height;

        int r0 = width * height;
        int u0 = r0 / 4;
        int v0 = u0;

        bufY = ByteBuffer.allocate(r0).put(bytes, 0, r0);
        // bufU = ByteBuffer.allocate(u0).put(bytes, r0, u0);
        // camera1的nv21格式,需要把uv全部放到bufU中,否则画面只有一半正常
        // 实际上bufU只使用了一半,这跟相机画面扫描的方向有关
        bufU = ByteBuffer.allocate(u0 + v0).put(bytes, r0, u0 + v0);
        bufV = ByteBuffer.allocate(v0).put(bytes, r0 + u0, v0);

        bufY.position(0);
        bufU.position(0);
        bufV.position(0);
    }

    public ImageBytes(Image image) {
        final Image.Plane[] planes = image.getPlanes();

        Image.Plane p0 = planes[0];
        Image.Plane p1 = planes[1];
        Image.Plane p2 = planes[2];

        ByteBuffer b0 = p0.getBuffer();
        ByteBuffer b1 = p1.getBuffer();
        ByteBuffer b2 = p2.getBuffer();

        int r0 = b0.remaining();

        int w0 = p0.getRowStride();
        int h0 = r0 / w0;
        if(r0 % w0 != 0) h0++;

        this.width = w0;
        this.height = h0;
        this.bufY = b0;
        this.bufU = b1;
        this.bufV = b2;
    }
}

4 格式转换

在片段着色器中进行转换,这一步也很重要。

首先将yuv分量从三个tex纹理中取出来,y取tex0里面的r分量,u取tex1里面的r分量减去0.5,而v取tex1里面的a分量减去0.5;再根据固定的公式将yuv转换成rgb,用一个矩阵计算表示,这个公式我在网上发现了各种各样的数值,找到两个应该是比较标准的,它们差别不大,至少我看不出来。

注意这是NV12格式的取法,如果是NV21格式,将u和v交换就行了。

#version 300 es

precision mediump float;
in vec3 aColor;
in vec2 aTexCoord;
out vec4 fragColor;

uniform sampler2D tex0;
uniform sampler2D tex1;
uniform sampler2D tex2;

void main() {

     float y = texture(tex0, aTexCoord).r;
     float u = texture(tex1, aTexCoord).r - 0.5;
     float v = texture(tex1, aTexCoord).a - 0.5;
     vec3 yuv = vec3(y, u, v);

    // 下面两种视觉上差异不大

//     // BT.601, which is the standard for SDTV is provided as a reference
//      vec3 rgb = mat3(      1,       1,       1,
//      0, -.39465, 2.03211,
//      1.13983, -.58060,       0) * yuv;

     // Using BT.709 which is the standard for HDTV
     vec3 rgb = mat3(      1,       1,       1,
     0, -.21482, 2.12798,
     1.28033, -.38059,       0) * vec3(y,u,v);


     fragColor = vec4(rgb, 1.0);

}

下面是YV12格式的取法,y取tex0里面的r分量,u取tex2里面的r分量减去0.5,而v取tex1里面的r分量减去0.5;如果是I420格式,将u和v交换就行了。

     float y = texture(tex0, aTexCoord).r;
     float u = texture(tex2, aTexCoord).r - 0.5;
     float v = texture(tex1, aTexCoord).r - 0.5;

5 小结

结合从tex中取yuv的规律,我们再来总结一下前面glTexImage2D方法里的GL_LUMINANCE和GL_LUMINANCE_ALPHA参数。

GL_LUMINANCE是将单个u或v打包到一个纹理的vec4里面,放到第一位:vec4(u,0,0,0)
GL_LUMINANCE_ALPHA是将相邻的两个uv打包到一个纹理的vec4里面,放到第一和最后一位:vec4(u,0,0,v)

有这两种参数就能直接创建不同YUV格式的纹理,避免了需要循环从数组里面取数据,比CPU运算方便多了,而着色器大规模并行转换数据也比CPU快多了。

当然这里的数据是片段着色器转换完直接显示的,不能输出到其他地方如编码器等,如果需要输出,可以用计算着色器或者更高级的GPU缓存机制。

原文地址:https://www.cnblogs.com/rome753/p/16491512.html