与Android不同,我对GL / libgdx比较陌生。我需要解决的任务是多方面的,即将Android摄像机的YUV-NV21预览图像实时渲染到libgdx内的屏幕背景。主要问题如下:

  • 仅保证Android摄像机的预览图像在YUV-NV21空间(以及在类似的YV12空间中,U和V通道不交织而是分组)。假设大多数现代设备将提供隐式RGB转换是非常错误的,例如最新的Samsung Note 10.1 2014版本仅提供YUV格式。由于除非在RGB中,否则在OpenGL中什么都不能绘制到屏幕上,因此必须以某种方式转换颜色空间。
  • libgdx文档(Integrating libgdx and the device camera)中的示例使用一个Android曲面 View ,该 View 位于所有内容的下方,以GLES 1.1绘制图像。自2014年3月以来,由于已过时,OpenGL ES 1.x支持已从libgdx中删除,几乎所有设备现在都支持GLES 2.0。如果使用GLES 2.0尝试相同的样本,则在图像上绘制的3D对象将是半透明的。由于后面的表面与GL无关,因此无法真正控制它。禁用混合/透明功能不起作用。因此,渲染该图像必须完全在GL中完成。
  • 这必须实时进行,因此颜色空间转换必须非常快。使用Android位图的软件转换可能会太慢。
  • 作为附带功能,必须从Android代码访问摄像头图像,以执行除在屏幕上绘制图像外的其他任务,例如,通过JNI将其发送到本地镜像处理器。

  • 问题是,如何正确,尽快地完成此任务?

    最佳答案

    简短的答案是将相机图像通道(Y,UV)加载到纹理中,然后使用自定义片段着色器将这些纹理绘制到网格上,该着色器将为我们完成颜色空间转换。由于此着色器将在GPU上运行,因此它将比CPU快得多,并且当然比Java代码快得多。由于此网格是GL的一部分,因此可以在其上方或下方安全地绘制任何其他3D形状或精灵。

    我从这个答案https://stackoverflow.com/a/17615696/1525238开始解决了这个问题。我通过以下链接了解了通用方法:How to use camera view with OpenGL ES,它是为Bada编写的,但原理是相同的。转换公式有点奇怪,所以我用Wikipedia文章YUV Conversion to/from RGB中的转换公式替换了它们。

    以下是导致解决方案的步骤:

    YUV-NV21说明

    来自Android相机的实时图像是预览图像。相机预览的默认色彩空间(以及两个保证的色彩空间之一)是YUV-NV21。这种格式的说明非常分散,因此在这里我将简要说明一下:

    图像数据由(宽x高)x 3/2字节组成。第一个宽度x高度字节是Y通道,每个像素1个亮度字节。以下(宽度/ 2)x(高度/ 2)x 2 =宽度x高度/ 2个字节是UV平面。每两个连续字节是2 x 2 = 4个原始像素的V,U(按照NV21规范的顺序)色度字节。换句话说,UV平面的大小为(宽度/ 2)x(高度/ 2)像素,并且在每个维度中均以2的因数向下采样。此外,U,V色度字节是交错的。

    这是一张非常漂亮的图像,说明YUV-NV12,NV21只是翻转了U,V字节:

    如何将这种格式转换为RGB?

    如问题中所述,如果在Android代码中完成转换,则转换将花费太多时间。幸运的是,它可以在运行于GPU的GL着色器内完成。这将使其非常快速地运行。

    一般的想法是将图像的通道作为纹理传递给着色器,并以进行RGB转换的方式进行渲染。为此,我们必须首先将图像中的通道复制到可以传递给纹理的缓冲区中:

    byte[] image;
    ByteBuffer yBuffer, uvBuffer;
    
    ...
    
    yBuffer.put(image, 0, width*height);
    yBuffer.position(0);
    
    uvBuffer.put(image, width*height, width*height/2);
    uvBuffer.position(0);
    

    然后,我们将这些缓冲区传递给实际的GL纹理:
    /*
     * Prepare the Y channel texture
     */
    
    //Set texture slot 0 as active and bind our texture object to it
    Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0);
    yTexture.bind();
    
    //Y texture is (width*height) in size and each pixel is one byte;
    //by setting GL_LUMINANCE, OpenGL puts this byte into R,G and B
    //components of the texture
    Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_LUMINANCE,
        width, height, 0, GL20.GL_LUMINANCE, GL20.GL_UNSIGNED_BYTE, yBuffer);
    
    //Use linear interpolation when magnifying/minifying the texture to
    //areas larger/smaller than the texture size
    Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,
        GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_LINEAR);
    Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,
        GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_LINEAR);
    Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,
        GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_EDGE);
    Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,
        GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_EDGE);
    
    /*
     * Prepare the UV channel texture
     */
    
    //Set texture slot 1 as active and bind our texture object to it
    Gdx.gl.glActiveTexture(GL20.GL_TEXTURE1);
    uvTexture.bind();
    
    //UV texture is (width/2*height/2) in size (downsampled by 2 in
    //both dimensions, each pixel corresponds to 4 pixels of the Y channel)
    //and each pixel is two bytes. By setting GL_LUMINANCE_ALPHA, OpenGL
    //puts first byte (V) into R,G and B components and of the texture
    //and the second byte (U) into the A component of the texture. That's
    //why we find U and V at A and R respectively in the fragment shader code.
    //Note that we could have also found V at G or B as well.
    Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_LUMINANCE_ALPHA,
        width/2, height/2, 0, GL20.GL_LUMINANCE_ALPHA, GL20.GL_UNSIGNED_BYTE,
        uvBuffer);
    
    //Use linear interpolation when magnifying/minifying the texture to
    //areas larger/smaller than the texture size
    Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,
        GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_LINEAR);
    Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,
        GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_LINEAR);
    Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,
        GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_EDGE);
    Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,
        GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_EDGE);
    

    接下来,我们渲染之前准备的网格(覆盖整个屏幕)。着色器将负责在网格上渲染绑定(bind)的纹理:
    shader.begin();
    
    //Set the uniform y_texture object to the texture at slot 0
    shader.setUniformi("y_texture", 0);
    
    //Set the uniform uv_texture object to the texture at slot 1
    shader.setUniformi("uv_texture", 1);
    
    mesh.render(shader, GL20.GL_TRIANGLES);
    shader.end();
    

    最后,着色器接管了将纹理渲染到网格的任务。实现实际转换的片段着色器如下所示:
    String fragmentShader =
        "#ifdef GL_ES\n" +
        "precision highp float;\n" +
        "#endif\n" +
    
        "varying vec2 v_texCoord;\n" +
        "uniform sampler2D y_texture;\n" +
        "uniform sampler2D uv_texture;\n" +
    
        "void main (void){\n" +
        "   float r, g, b, y, u, v;\n" +
    
        //We had put the Y values of each pixel to the R,G,B components by
        //GL_LUMINANCE, that's why we're pulling it from the R component,
        //we could also use G or B
        "   y = texture2D(y_texture, v_texCoord).r;\n" +
    
        //We had put the U and V values of each pixel to the A and R,G,B
        //components of the texture respectively using GL_LUMINANCE_ALPHA.
        //Since U,V bytes are interspread in the texture, this is probably
        //the fastest way to use them in the shader
        "   u = texture2D(uv_texture, v_texCoord).a - 0.5;\n" +
        "   v = texture2D(uv_texture, v_texCoord).r - 0.5;\n" +
    
        //The numbers are just YUV to RGB conversion constants
        "   r = y + 1.13983*v;\n" +
        "   g = y - 0.39465*u - 0.58060*v;\n" +
        "   b = y + 2.03211*u;\n" +
    
        //We finally set the RGB color of our pixel
        "   gl_FragColor = vec4(r, g, b, 1.0);\n" +
        "}\n";
    

    请注意,我们使用相同的坐标变量v_texCoord访问Y和UV纹理,这是由于v_texCoord在-1.0和1.0之间,它从纹理的一端缩放到另一端,而不是实际的纹理像素坐标。这是着色器的最佳功能之一。

    完整源代码

    由于libgdx是跨平台的,因此我们需要一个可以在不同平台上以不同方式扩展的对象,以处理设备摄像头和渲染。例如,如果您可以获取硬件为您提供RGB图像,则可能希望完全绕过YUV-RGB着色器转换。因此,我们需要一个设备相机 Controller 接口(interface),该接口(interface)将由每个不同的平台实现:
    public interface PlatformDependentCameraController {
    
        void init();
    
        void renderBackground();
    
        void destroy();
    }
    

    该界面的Android版本如下(实时摄像机图像假定为1280x720像素):
    public class AndroidDependentCameraController implements PlatformDependentCameraController, Camera.PreviewCallback {
    
        private static byte[] image; //The image buffer that will hold the camera image when preview callback arrives
    
        private Camera camera; //The camera object
    
        //The Y and UV buffers that will pass our image channel data to the textures
        private ByteBuffer yBuffer;
        private ByteBuffer uvBuffer;
    
        ShaderProgram shader; //Our shader
        Texture yTexture; //Our Y texture
        Texture uvTexture; //Our UV texture
        Mesh mesh; //Our mesh that we will draw the texture on
    
        public AndroidDependentCameraController(){
    
            //Our YUV image is 12 bits per pixel
            image = new byte[1280*720/8*12];
        }
    
        @Override
        public void init(){
    
            /*
             * Initialize the OpenGL/libgdx stuff
             */
    
            //Do not enforce power of two texture sizes
            Texture.setEnforcePotImages(false);
    
            //Allocate textures
            yTexture = new Texture(1280,720,Format.Intensity); //A 8-bit per pixel format
            uvTexture = new Texture(1280/2,720/2,Format.LuminanceAlpha); //A 16-bit per pixel format
    
            //Allocate buffers on the native memory space, not inside the JVM heap
            yBuffer = ByteBuffer.allocateDirect(1280*720);
            uvBuffer = ByteBuffer.allocateDirect(1280*720/2); //We have (width/2*height/2) pixels, each pixel is 2 bytes
            yBuffer.order(ByteOrder.nativeOrder());
            uvBuffer.order(ByteOrder.nativeOrder());
    
            //Our vertex shader code; nothing special
            String vertexShader =
                    "attribute vec4 a_position;                         \n" +
                    "attribute vec2 a_texCoord;                         \n" +
                    "varying vec2 v_texCoord;                           \n" +
    
                    "void main(){                                       \n" +
                    "   gl_Position = a_position;                       \n" +
                    "   v_texCoord = a_texCoord;                        \n" +
                    "}                                                  \n";
    
            //Our fragment shader code; takes Y,U,V values for each pixel and calculates R,G,B colors,
            //Effectively making YUV to RGB conversion
            String fragmentShader =
                    "#ifdef GL_ES                                       \n" +
                    "precision highp float;                             \n" +
                    "#endif                                             \n" +
    
                    "varying vec2 v_texCoord;                           \n" +
                    "uniform sampler2D y_texture;                       \n" +
                    "uniform sampler2D uv_texture;                      \n" +
    
                    "void main (void){                                  \n" +
                    "   float r, g, b, y, u, v;                         \n" +
    
                    //We had put the Y values of each pixel to the R,G,B components by GL_LUMINANCE,
                    //that's why we're pulling it from the R component, we could also use G or B
                    "   y = texture2D(y_texture, v_texCoord).r;         \n" +
    
                    //We had put the U and V values of each pixel to the A and R,G,B components of the
                    //texture respectively using GL_LUMINANCE_ALPHA. Since U,V bytes are interspread
                    //in the texture, this is probably the fastest way to use them in the shader
                    "   u = texture2D(uv_texture, v_texCoord).a - 0.5;  \n" +
                    "   v = texture2D(uv_texture, v_texCoord).r - 0.5;  \n" +
    
    
                    //The numbers are just YUV to RGB conversion constants
                    "   r = y + 1.13983*v;                              \n" +
                    "   g = y - 0.39465*u - 0.58060*v;                  \n" +
                    "   b = y + 2.03211*u;                              \n" +
    
                    //We finally set the RGB color of our pixel
                    "   gl_FragColor = vec4(r, g, b, 1.0);              \n" +
                    "}                                                  \n";
    
            //Create and compile our shader
            shader = new ShaderProgram(vertexShader, fragmentShader);
    
            //Create our mesh that we will draw on, it has 4 vertices corresponding to the 4 corners of the screen
            mesh = new Mesh(true, 4, 6,
                    new VertexAttribute(Usage.Position, 2, "a_position"),
                    new VertexAttribute(Usage.TextureCoordinates, 2, "a_texCoord"));
    
            //The vertices include the screen coordinates (between -1.0 and 1.0) and texture coordinates (between 0.0 and 1.0)
            float[] vertices = {
                    -1.0f,  1.0f,   // Position 0
                    0.0f,   0.0f,   // TexCoord 0
                    -1.0f,  -1.0f,  // Position 1
                    0.0f,   1.0f,   // TexCoord 1
                    1.0f,   -1.0f,  // Position 2
                    1.0f,   1.0f,   // TexCoord 2
                    1.0f,   1.0f,   // Position 3
                    1.0f,   0.0f    // TexCoord 3
            };
    
            //The indices come in trios of vertex indices that describe the triangles of our mesh
            short[] indices = {0, 1, 2, 0, 2, 3};
    
            //Set vertices and indices to our mesh
            mesh.setVertices(vertices);
            mesh.setIndices(indices);
    
            /*
             * Initialize the Android camera
             */
            camera = Camera.open(0);
    
            //We set the buffer ourselves that will be used to hold the preview image
            camera.setPreviewCallbackWithBuffer(this);
    
            //Set the camera parameters
            Camera.Parameters params = camera.getParameters();
            params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
            params.setPreviewSize(1280,720);
            camera.setParameters(params);
    
            //Start the preview
            camera.startPreview();
    
            //Set the first buffer, the preview doesn't start unless we set the buffers
            camera.addCallbackBuffer(image);
        }
    
        @Override
        public void onPreviewFrame(byte[] data, Camera camera) {
    
            //Send the buffer reference to the next preview so that a new buffer is not allocated and we use the same space
            camera.addCallbackBuffer(image);
        }
    
        @Override
        public void renderBackground() {
    
            /*
             * Because of Java's limitations, we can't reference the middle of an array and
             * we must copy the channels in our byte array into buffers before setting them to textures
             */
    
            //Copy the Y channel of the image into its buffer, the first (width*height) bytes are the Y channel
            yBuffer.put(image, 0, 1280*720);
            yBuffer.position(0);
    
            //Copy the UV channels of the image into their buffer, the following (width*height/2) bytes are the UV channel; the U and V bytes are interspread
            uvBuffer.put(image, 1280*720, 1280*720/2);
            uvBuffer.position(0);
    
            /*
             * Prepare the Y channel texture
             */
    
            //Set texture slot 0 as active and bind our texture object to it
            Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0);
            yTexture.bind();
    
            //Y texture is (width*height) in size and each pixel is one byte; by setting GL_LUMINANCE, OpenGL puts this byte into R,G and B components of the texture
            Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_LUMINANCE, 1280, 720, 0, GL20.GL_LUMINANCE, GL20.GL_UNSIGNED_BYTE, yBuffer);
    
            //Use linear interpolation when magnifying/minifying the texture to areas larger/smaller than the texture size
            Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_LINEAR);
            Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_LINEAR);
            Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_EDGE);
            Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_EDGE);
    
    
            /*
             * Prepare the UV channel texture
             */
    
            //Set texture slot 1 as active and bind our texture object to it
            Gdx.gl.glActiveTexture(GL20.GL_TEXTURE1);
            uvTexture.bind();
    
            //UV texture is (width/2*height/2) in size (downsampled by 2 in both dimensions, each pixel corresponds to 4 pixels of the Y channel)
            //and each pixel is two bytes. By setting GL_LUMINANCE_ALPHA, OpenGL puts first byte (V) into R,G and B components and of the texture
            //and the second byte (U) into the A component of the texture. That's why we find U and V at A and R respectively in the fragment shader code.
            //Note that we could have also found V at G or B as well.
            Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_LUMINANCE_ALPHA, 1280/2, 720/2, 0, GL20.GL_LUMINANCE_ALPHA, GL20.GL_UNSIGNED_BYTE, uvBuffer);
    
            //Use linear interpolation when magnifying/minifying the texture to areas larger/smaller than the texture size
            Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_LINEAR);
            Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_LINEAR);
            Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_EDGE);
            Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_EDGE);
    
            /*
             * Draw the textures onto a mesh using our shader
             */
    
            shader.begin();
    
            //Set the uniform y_texture object to the texture at slot 0
            shader.setUniformi("y_texture", 0);
    
            //Set the uniform uv_texture object to the texture at slot 1
            shader.setUniformi("uv_texture", 1);
    
            //Render our mesh using the shader, which in turn will use our textures to render their content on the mesh
            mesh.render(shader, GL20.GL_TRIANGLES);
            shader.end();
        }
    
        @Override
        public void destroy() {
            camera.stopPreview();
            camera.setPreviewCallbackWithBuffer(null);
            camera.release();
        }
    }
    

    主要应用程序部分仅确保init()在开始时被调用一次,renderBackground()在每个渲染周期中被调用,destroy()在结束时被调用一次:
    public class YourApplication implements ApplicationListener {
    
        private final PlatformDependentCameraController deviceCameraControl;
    
        public YourApplication(PlatformDependentCameraController cameraControl) {
            this.deviceCameraControl = cameraControl;
        }
    
        @Override
        public void create() {
            deviceCameraControl.init();
        }
    
        @Override
        public void render() {
            Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
            Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
    
            //Render the background that is the live camera image
            deviceCameraControl.renderBackground();
    
            /*
             * Render anything here (sprites/models etc.) that you want to go on top of the camera image
             */
        }
    
        @Override
        public void dispose() {
            deviceCameraControl.destroy();
        }
    
        @Override
        public void resize(int width, int height) {
        }
    
        @Override
        public void pause() {
        }
    
        @Override
        public void resume() {
        }
    }
    

    唯一的其他特定于Android的部分是以下极其简短的主要Android代码,您只需创建一个新的特定于Android的设备相机处理程序并将其传递给主要的libgdx对象即可:
    public class MainActivity extends AndroidApplication {
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration();
            cfg.useGL20 = true; //This line is obsolete in the newest libgdx version
            cfg.a = 8;
            cfg.b = 8;
            cfg.g = 8;
            cfg.r = 8;
    
            PlatformDependentCameraController cameraControl = new AndroidDependentCameraController();
            initialize(new YourApplication(cameraControl), cfg);
    
            graphics.getView().setKeepScreenOn(true);
        }
    }
    

    多快?

    我在两个设备上测试了此例程。虽然跨帧的测量值不是恒定的,但可以观察到以下一般情况:
  • 三星Galaxy Note II LTE-(GT-N7105):具有ARM Mali-400 MP4 GPU。
  • 渲染一帧大约需要5-6毫秒,偶尔每两秒钟跳转到15毫秒左右
  • 实际渲染线(mesh.render(shader, GL20.GL_TRIANGLES);)始终需要0-1毫秒
  • 两种纹理的创建和绑定(bind)总共需要1-3 ms的
  • ByteBuffer副本通常总共需要1-3毫秒,但偶尔会跳到7毫秒左右,这可能是由于图像缓冲区在JVM堆
  • 中移动
  • 三星Galaxy Note 10.1 2014-(SM-P600):具有ARM Mali-T628 GPU。
  • 渲染一帧大约需要2-4毫秒,很少会跳到6-10毫秒左右
  • 实际渲染线(mesh.render(shader, GL20.GL_TRIANGLES);)始终需要0-1毫秒
  • 两种纹理的创建和绑定(bind)总共花费1-3毫秒,但每两秒钟跳到6-9毫秒左右
  • ByteBuffer副本通常总共花费0-2毫秒,但很少跳到6ms左右

  • 如果您认为使用其他方法可以更快地创建这些配置文件,请随时分享。希望这个小教程对您有所帮助。

    07-27 14:04