gpt4 book ai didi

android - 如何使用OpenGLES 2.0在libgdx中的背景上实时渲染Android的YUV-NV21摄像机图像?

转载 作者:IT王子 更新时间:2023-10-28 23:33:22 27 4
gpt4 key购买 nike

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

  • 仅保证Android摄像机的预览图像在YUV-NV21空间(以及在类似的YV12空间中,U和V channel 不交织而是分组)。假设大多数现代设备将提供隐式RGB转换是非常错误的,例如最新的Samsung Note 10.1 2014版本仅提供YUV格式。由于在OpenGL中,除非在RGB中,否则什么都不能绘制到屏幕上,因此必须以某种方式转换颜色空间。
  • 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将其发送到本地镜像处理器。

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

    最佳答案

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

    我从这个答案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 channel ,每个像素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着色器中完成。这将使其非常快速地运行。

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

    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毫秒
  • 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左右

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

    关于android - 如何使用OpenGLES 2.0在libgdx中的背景上实时渲染Android的YUV-NV21摄像机图像?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22456884/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com