Contrairement à Android, je suis relativement nouveau dans GL/libgdx. La tâche que je dois résoudre, à savoir le rendu de l'image de prévisualisation YUV-NV21 de l'appareil photo Android) en arrière-plan de l'écran dans libgdx en temps réel, comporte plusieurs facettes. Voici les principales préoccupations:
L'image de prévisualisation de la caméra Android n'est garantie que dans l'espace YUV-NV21 (et dans l'espace YV12 similaire où les canaux U et V ne sont pas entrelacés mais groupés). En supposant que la plupart des appareils modernes fourniront une conversion RVB implicite est TRÈS faux, par exemple, la dernière version de Samsung Note 10.1 2014 ne fournit que les formats YUV. Étant donné que rien ne peut être dessiné à l'écran dans OpenGL à moins qu'il ne soit en RVB, l'espace colorimétrique doit en quelque sorte être converti.
L'exemple de la documentation libgdx ( Intégration de libgdx et de l'appareil photo de l'appareil ) utilise une vue de surface Android qui se trouve en dessous de tout pour dessiner l'image avec GLES 1.1. début mars 2014, la prise en charge d'OpenGLES 1.x est supprimée de libgdx car elle est obsolète et presque tous les appareils prennent désormais en charge GLES 2.0. Si vous essayez le même exemple avec GLES 2.0, les objets 3D que vous dessinez sur l'image seront à moitié transparents Étant donné que la surface derrière n'a rien à voir avec GL, cela ne peut pas vraiment être contrôlé. La désactivation de BLENDING/TRANSLUCENCY ne fonctionne pas. Par conséquent, le rendu de cette image doit être effectué uniquement en GL.
Cela doit être fait en temps réel, donc la conversion de l'espace colorimétrique doit être TRÈS rapide. La conversion logicielle à l'aide de Android bitmaps sera probablement trop lente.
En tant que fonctionnalité latérale, l'image de la caméra doit être accessible à partir du code Android afin de réaliser d'autres tâches que de la dessiner à l'écran, par exemple, de l'envoyer à un processeur d'image natif via JNI.
La question est de savoir comment cette tâche est effectuée correctement et le plus rapidement possible?
La réponse courte est de charger les canaux d'image de la caméra (Y, UV) dans des textures et de dessiner ces textures sur un maillage à l'aide d'un shader de fragment personnalisé qui fera la conversion de l'espace colorimétrique pour nous. Comme ce shader fonctionnera sur le GPU, il sera beaucoup plus rapide que le CPU et certainement beaucoup plus rapide que le code Java. Comme ce maillage fait partie de GL, toutes les autres formes 3D ou sprites peut être tiré en toute sécurité par dessus ou par dessous.
J'ai résolu le problème à partir de cette réponse https://stackoverflow.com/a/17615696/1525238 . J'ai compris la méthode générale en utilisant le lien suivant: Comment utiliser la vue caméra avec OpenGL ES , c'est écrit pour Bada mais les principes sont les mêmes. Les formules de conversion étaient un peu étranges, je les ai donc remplacées par celles de l'article Wikipedia Conversion YUV vers/depuis RVB .
Voici les étapes menant à la solution:
Explication YUV-NV21
Les images en direct de la Android sont des images d'aperçu. L'espace colorimétrique par défaut (et l'un des deux espaces colorimétriques garantis) est YUV-NV21 pour l'aperçu de la caméra. L'explication de ce format est très dispersée, Je vais donc l'expliquer ici brièvement:
Les données d'image sont constituées de (largeur x hauteur) x 3/2 octets. Les premiers largeur x hauteur octets sont le canal Y, 1 octet de luminosité pour chaque pixel. Les suivants (largeur/2) x (hauteur/2) x 2 = largeur x hauteur/2 octets sont le plan UV. Deux octets consécutifs sont les octets de chrominance V, U (dans cet ordre selon la spécification NV21) pour les pixels originaux 2 x 2 = 4. En d'autres termes, le plan UV est (largeur/2) x (hauteur/2) pixels et est sous-échantillonné d'un facteur 2 dans chaque dimension. De plus, les octets de chrominance U, V sont entrelacés.
Voici une très belle image qui explique que le YUV-NV12, NV21 est juste U, V octets inversés:
Comment convertir ce format en RGB?
Comme indiqué dans la question, cette conversion prendrait trop de temps à vivre si elle est effectuée à l'intérieur du code Android. Heureusement, elle peut être effectuée à l'intérieur d'un GL shader, qui fonctionne sur le GPU. Cela lui permettra de fonctionner TRÈS rapidement.
L'idée générale est de passer les canaux de notre image sous forme de textures au shader et de les rendre d'une manière qui effectue la conversion RVB. Pour cela, nous devons d'abord copier les canaux de notre image dans des tampons qui peuvent être passés aux textures:
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);
Ensuite, nous passons ces tampons aux textures réelles 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);
Ensuite, nous rendons le maillage que nous avons préparé précédemment (couvre tout l'écran). Le shader prendra en charge le rendu des textures liées sur le maillage:
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();
Enfin, le shader reprend la tâche de rendre nos textures au maillage. Le shader de fragment qui réalise la conversion réelle ressemble à ceci:
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";
Veuillez noter que nous accédons aux textures Y et UV en utilisant la même variable de coordonnées v_texCoord
, Cela est dû au fait que v_texCoord
Se situe entre 1.0 et 1.0 qui évolue d'une extrémité à l'autre de la texture par opposition aux coordonnées réelles des pixels de texture. C'est l'une des plus belles fonctionnalités des shaders.
Le code source complet
Étant donné que libgdx est multiplateforme, nous avons besoin d'un objet qui peut être étendu différemment sur différentes plates-formes qui gère la caméra et le rendu de l'appareil. Par exemple, vous voudrez peut-être contourner complètement la conversion des shaders YUV-RGB si vous pouvez obtenir le matériel pour vous fournir des images RVB. Pour cette raison, nous avons besoin d'une interface de contrôleur de caméra d'appareil qui sera implémentée par chaque plate-forme différente:
public interface PlatformDependentCameraController {
void init();
void renderBackground();
void destroy();
}
La Android de cette interface est la suivante (l'image de la caméra en direct est supposée être de 1280x720 pixels):
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();
}
}
La partie principale de l'application garantit simplement que init()
est appelée une fois au début, renderBackground()
est appelée à chaque cycle de rendu et destroy()
est appelée une fois à la fin:
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() {
}
}
La seule autre partie spécifique à Android est le principal extrêmement court suivant Android code, vous créez simplement un nouveau Android gestionnaire d'appareil photo spécifique à l'appareil et le passez au principal objet 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);
}
}
À quelle vitesse est-ce?
J'ai testé cette routine sur deux appareils. Bien que les mesures ne soient pas constantes d'une image à l'autre, un profil général peut être observé:
Samsung Galaxy Note II LTE - (GT-N7105): A ARM Mali-400 MP4 GPU.
mesh.render(shader, GL20.GL_TRIANGLES);
) prend systématiquement 0 à 1 msSamsung Galaxy Note 10.1 2014 - (SM-P600): A ARM GPU Mali-T628.
mesh.render(shader, GL20.GL_TRIANGLES);
) prend systématiquement 0 à 1 msN'hésitez pas à partager si vous pensez que ces profils peuvent être rendus plus rapides avec une autre méthode. J'espère que ce petit tutoriel vous a aidé.
Pour le moyen le plus rapide et le plus optimisé, utilisez simplement l'extension courante GL
//Fragment Shader
#extension GL_OES_EGL_image_external : require
uniform samplerExternalOES u_Texture;
Que dans Java
surfaceTexture = new SurfaceTexture(textureIDs[0]);
try {
someCamera.setPreviewTexture(surfaceTexture);
} catch (IOException t) {
Log.e(TAG, "Cannot set preview texture target!");
}
someCamera.startPreview();
private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;
Dans Java GL Thread
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureIDs[0]);
GLES20.glUniform1i(uTextureHandle, 0);
La conversion des couleurs est déjà faite pour vous. Vous pouvez faire ce que vous voulez dans le shader Fragment.
Ce n'est pas du tout une solution Libgdx car elle dépend de la plateforme. Vous pouvez initialiser le contenu dépendant de la plateforme dans le wrapper et l'envoyer à Libgdx Activity.
J'espère que cela vous fera gagner du temps dans vos recherches.