Как визуализировать изображение камеры YUV-NV21 на фоне в libgdx с OpenGLES 2.0 в режиме реального времени?
В отличие от Android, я относительно новичок в GL / libgdx. Задача, которую мне нужно решить, а именно предоставление изображения предварительного просмотра камеры YUV-NV21 камеры Android на фоне фона в libgdx в режиме реального времени многогранно. Вот основные проблемы:
-
Изображение предварительного просмотра камеры Android гарантировано только в пространстве YUV-NV21 (и в аналогичном пространстве YV12, где каналы U и V не чередуются, а группируются). Предполагая, что большинство современных устройств обеспечит неявное преобразование RGB, ОЧЕНЬ ошибочно, например, самая новая версия Samsung Note 10.1 2014 предоставляет только форматы YUV. Поскольку на экране OpenGL ничего нельзя отобразить, если оно не находится в RGB, цветовое пространство должно каким-то образом быть преобразовано.
-
Пример в документации libgdx ( Интеграция libgdx и камеры устройства ) использует вид поверхности Android, который находится ниже всего, чтобы нарисовать изображение с помощью GLES 1.1. С начала марта 2014 года поддержка OpenGLES 1.x удаляется из libgdx из-за устаревания и почти всех устройств, которые теперь поддерживают GLES 2.0. Если вы попробуете тот же образец с GLES 2.0, 3D-объекты, которые вы рисуете на изображении, будут полупрозрачными. Поскольку поверхность позади не имеет ничего общего с GL, это не может быть действительно контролируемо. Отключение BLENDING / TRANSLUCENCY не работает. Поэтому рендеринг этого изображения должен выполняться исключительно в GL.
-
Это необходимо сделать в режиме реального времени, поэтому преобразование цветового пространства должно быть очень быстрым. Преобразование программного обеспечения с использованием растровых изображений Android, вероятно, будет слишком медленным.
-
В качестве боковой функции изображение камеры должно быть доступно из кода Android, чтобы выполнять другие задачи, чем рисовать его на экране, например, отправлять его на собственный процессор изображения через JNI.
Вопрос в том, как эта задача выполняется должным образом и как можно быстрее?
- Как автоматически автофокусировать камеру Android?
- Использование активности камеры в Android
- Как сохранить фотографию в библиотеке фотографий iPhone?
- Как включить фронтальную вспышку программно в Android?
- Как открыть «переднюю камеру» на платформе Android?
- Камера Android вращается
- Захваченная ориентация фото меняется в андроиде
- Есть ли хороший учебник для внедрения приложения iPhone дополненной реальности?
Короткий ответ – загрузить каналы изображения камеры (Y, UV) в текстуры и нарисовать эти текстуры на Mesh, используя специальный шейдер fragmentа, который будет делать преобразование цветового пространства для нас. Поскольку этот шейдер будет работать на графическом процессоре, он будет намного быстрее, чем процессор, и, конечно, намного быстрее, чем Java-код. Поскольку эта grid является частью GL, любые другие трехмерные фигуры или спрайты можно безопасно нарисовать поверх или под ней.
Я решил проблему, начиная с этого ответа https://stackoverflow.com/a/17615696/1525238 . Я понял общий метод, используя следующую ссылку: Как использовать просмотр камеры с OpenGL ES , это написано для Bada, но принципы одинаковы. Формулы преобразования там были немного странными, поэтому я заменил их на те, которые содержатся в статье Википедии YUV Conversion в / из RGB .
Ниже приведены шаги, ведущие к решению:
Объяснение YUV-NV21
Живые изображения с камеры Android – это изображения предварительного просмотра. Цветовое пространство по умолчанию (и одно из двух гарантированных цветовых пространств) – это YUV-NV21 для предварительного просмотра камеры. Объяснение этого формата очень разбросано, поэтому я кратко объясню его здесь:
Данные изображения состоят из (ширина x высота) x 3/2 байта. Первый байт ширины x высоты – это Y-канал, 1 байт яркости для каждого пикселя. Следующие (ширина / 2) x (высота / 2) x 2 = ширина x высота / 2 байта – это УФ-плоскость. Каждый два последовательных байта представляют собой V, U (в указанном порядке в соответствии с спецификацией NV21), цветные байты для 2 x 2 = 4 исходных пикселей. Другими словами, УФ-плоскость имеет размер (ширина / 2) х (высота / 2) в пикселях и уменьшается в два раза в каждом измерении. Кроме того, байты цветности U, V чередуются.
Вот очень приятное изображение, которое объясняет YUV-NV12, NV21 – это просто U, V байт перевернуто:
Как преобразовать этот формат в RGB?
Как указано в вопросе, для этого преобразования потребуется слишком много времени, чтобы быть живым, если это сделано внутри кода Android. К счастью, это можно сделать внутри 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);
Затем мы создаем сетку, которую мы подготовили ранее (охватывает весь экран). Шейдер позаботится об оказании связанных текстур в сетке:
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";
Обратите внимание, что мы обращаемся к текстурам Y и UV, используя ту же переменную координат v_texCoord
, что связано с v_texCoord
что v_texCoord
находится между -1,0 и 1,0, который масштабируется от одного конца текстуры до другого, в отличие от фактических координат пикселя текстуры. Это одна из лучших особенностей шейдеров.
Полный исходный код
Поскольку libgdx является кросс-платформенным, нам нужен объект, который может расширяться по-разному на разных платформах, которые обрабатывают камеру устройства и рендеринг. Например, вы можете вообще обойти преобразование шейдеров YUV-RGB, если вы можете заставить оборудование предоставить вам изображения RGB. По этой причине нам нужен интерфейс controllerа камеры устройства, который будет реализован каждой другой платформой:
public interface PlatformDependentCameraController { void init(); void renderBackground(); void destroy(); }
Версия Android этого интерфейса выглядит следующим образом (изображение в реальном времени камеры считается 1280×720 пикселей):
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 для 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); } }
Как быстро?
Я протестировал эту процедуру на двух устройствах. Хотя измерения не постоянны во всех кадрах, можно наблюдать общий профиль:
-
Samsung Galaxy Note II LTE – (GT-N7105): имеет графический процессор ARM Mali-400 MP4.
- Рендеринг одного кадра занимает около 5-6 мс, при этом случайные прыжки составляют около 15 мсек каждые пару секунд
- Фактическая линия рендеринга (
mesh.render(shader, GL20.GL_TRIANGLES);
) последовательно принимает 0-1 мс - Создание и привязка обоих текстур последовательно составляет 1-3 мс
- Обычно копии ByteBuffer занимают 1-3 мс, но иногда возникают около 7 мс, вероятно, из-за перемещения буфера изображения в куче JVM
-
Samsung Galaxy Note 10.1 2014 – (SM-P600): имеет графический процессор ARM Mali-T628.
- Оказание одного кадра занимает около 2-4 мс, с редким прыжком примерно до 6-10 мс
- Фактическая линия рендеринга (
mesh.render(shader, GL20.GL_TRIANGLES);
) последовательно принимает 0-1 мс - Создание и привязка обоих текстур составляет всего 1-3 мс, но каждые пару секунд они скатываются примерно до 6-9 мс
- Обычно копии ByteBuffer занимают 0-2 мс, но очень редко достигают 6 мс
Пожалуйста, не стесняйтесь делиться, если вы считаете, что эти профили можно сделать быстрее с помощью другого метода. Надеюсь, этот небольшой учебник помог.
Для наиболее быстрого и оптимального способа просто используйте общее расширение GL
//Fragment Shader #extension GL_OES_EGL_image_external : require uniform samplerExternalOES u_Texture;
Чем в 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;
В Java GL Thread
GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureIDs[0]); GLES20.glUniform1i(uTextureHandle, 0);
Преобразование цвета уже сделано для вас. Вы можете делать то, что захотите прямо в шейдере fragmentов.
На самом деле это не решение Libgdx, поскольку оно зависит от платформы. Вы можете инициализировать зависимый от платформы материал в wraper и отправить его в Libgdx Activity.
Надеюсь, что это поможет вам немного времени в ваших исследованиях.