Estabilizar imagens da câmera no SDK do Android (Kotlin/Java)

O ARCore agora oferece suporte à estabilização eletrônica de imagem (EIS, na sigla em inglês), o que ajuda a produzir uma visualização suave da câmera. O EIS alcança a estabilização observando o movimento do smartphone com o giroscópio e aplicando malha de homografia de compensação dentro dos limites da textura da câmera que contraria as pequenas trepidações. O EIS só é compatível com a orientação retrato do dispositivo. Todas as orientações serão compatíveis com a versão 1.39.0 do ARCore.

Consultar o suporte do EIS e ativar o EIS

Para ativar o EIS, configure sua sessão para usar o ImageStabilizationMode.EIS. Se o dispositivo não for compatível com o recurso EIS, uma exceção será gerada no ARCore.

Java

if (!session.isImageStabilizationModeSupported(Config.ImageStabilizationMode.EIS)) {
  return;
}
Config config = session.getConfig();
config.setImageStabilizationMode(Config.ImageStabilizationMode.EIS);
session.configure(config);

Kotlin

if (!session.isImageStabilizationModeSupported(Config.ImageStabilizationMode.EIS)) return
session.configure(
  session.config.apply { imageStabilizationMode = Config.ImageStabilizationMode.EIS }
)

Coordenadas de transformação

Quando o EIS está ativado, o renderizador precisa usar as coordenadas do dispositivo modificadas e as coordenadas de textura correspondentes que incorporem a compensação de EIS ao renderizar o plano de fundo da câmera. Para receber as coordenadas compensadas pelo EIS, use Frame.transformCoordinates3d(), usando OPENGL_NORMALIZED_DEVICE_COORDINATES como entrada e EIS_NORMALIZED_DEVICE_COORDINATES como saída, para receber as coordenadas do dispositivo 3D e EIS_TEXTURE_NORMALIZED como saída para as coordenadas de textura 3D. Por enquanto, o único tipo de coordenada de entrada compatível com Frame.transformCoordinates3d() é OPENGL_NORMALIZED_DEVICE_COORDINATES.

Java

final FloatBuffer cameraTexCoords =
    ByteBuffer.allocateDirect(COORDS_BUFFER_SIZE_3D)
        .order(ByteOrder.nativeOrder())
        .asFloatBuffer();

final FloatBuffer screenCoords =
    ByteBuffer.allocateDirect(COORDS_BUFFER_SIZE_3D)
        .order(ByteOrder.nativeOrder())
        .asFloatBuffer();

final FloatBuffer NDC_QUAD_COORDS_BUFFER =
    ByteBuffer.allocateDirect(COORDS_BUFFER_SIZE_2D)
        .order(ByteOrder.nativeOrder())
        .asFloatBuffer()
        .put(
            new float[] {
              /*0:*/ -1f, -1f, /*1:*/ +1f, -1f, /*2:*/ -1f, +1f, /*3:*/ +1f, +1f,
            });

final VertexBuffer screenCoordsVertexBuffer =
    new VertexBuffer(render, /* numberOfEntriesPerVertex= */ 3, null);
final VertexBuffer cameraTexCoordsVertexBuffer =
    new VertexBuffer(render, /* numberOfEntriesPerVertex= */ 3, null);

NDC_QUAD_COORDS_BUFFER.rewind();
frame.transformCoordinates3d(
    Coordinates2d.OPENGL_NORMALIZED_DEVICE_COORDINATES,
    NDC_QUAD_COORDS_BUFFER,
    Coordinates3d.EIS_NORMALIZED_DEVICE_COORDINATES,
    screenCoords);
screenCoordsVertexBuffer.set(screenCoords);

NDC_QUAD_COORDS_BUFFER.rewind();
frame.transformCoordinates3d(
    Coordinates2d.OPENGL_NORMALIZED_DEVICE_COORDINATES,
    NDC_QUAD_COORDS_BUFFER,
    Coordinates3d.EIS_TEXTURE_NORMALIZED,
    cameraTexCoords);
cameraTexCoordsVertexBuffer.set(cameraTexCoords);

Kotlin

val COORDS_BUFFER_SIZE_2D = 2 * 4 * Float.SIZE_BYTES
val COORDS_BUFFER_SIZE_3D = 3 * 4 * Float.SIZE_BYTES
val cameraTexCoords =
  ByteBuffer.allocateDirect(COORDS_BUFFER_SIZE_3D)
    .order(ByteOrder.nativeOrder())
    .asFloatBuffer()
val screenCoords =
  ByteBuffer.allocateDirect(COORDS_BUFFER_SIZE_3D)
    .order(ByteOrder.nativeOrder())
    .asFloatBuffer()
val cameraTexCoordsVertexBuffer = VertexBuffer(render, /* numberOfEntriesPerVertex= */ 3, null)
val screenCoordsVertexBuffer = VertexBuffer(render, /* numberOfEntriesPerVertex= */ 3, null)
val NDC_QUAD_COORDS_BUFFER =
  ByteBuffer.allocateDirect(COORDS_BUFFER_SIZE_2D)
    .order(ByteOrder.nativeOrder())
    .asFloatBuffer()
    .apply {
      put(
        floatArrayOf(
          /* 0: */
          -1f,
          -1f,
          /* 1: */
          +1f,
          -1f,
          /* 2: */
          -1f,
          +1f,
          /* 3: */
          +1f,
          +1f
        )
      )
    }
NDC_QUAD_COORDS_BUFFER.rewind()
frame.transformCoordinates3d(
  Coordinates2d.OPENGL_NORMALIZED_DEVICE_COORDINATES,
  NDC_QUAD_COORDS_BUFFER,
  Coordinates3d.EIS_NORMALIZED_DEVICE_COORDINATES,
  screenCoords
)
screenCoordsVertexBuffer.set(screenCoords)

NDC_QUAD_COORDS_BUFFER.rewind()
frame.transformCoordinates3d(
  Coordinates2d.OPENGL_NORMALIZED_DEVICE_COORDINATES,
  NDC_QUAD_COORDS_BUFFER,
  Coordinates3d.EIS_TEXTURE_NORMALIZED,
  cameraTexCoords
)
cameraTexCoordsVertexBuffer.set(cameraTexCoords)

Quando o EIS está desativado, as coordenadas 3D de saída são equivalentes às coordenadas 2D, com os valores z definidos para não produzir nenhuma alteração.

Modificar sombreadores

As coordenadas 3D calculadas precisam ser transmitidas aos sombreadores de renderização em segundo plano. Os buffers de vértice agora são 3D com EIS:

layout(location = 0) in vec4 a_Position;
layout(location = 1) in vec3 a_CameraTexCoord;
out vec3 v_CameraTexCoord;
void main() {
  gl_Position = a_Position;
  v_CameraTexCoord = a_CameraTexCoord;
}

Além disso, o sombreador de fragmento precisa aplicar a correção de perspectiva:

precision mediump float;
uniform samplerExternalOES u_CameraColorTexture;
in vec3 v_CameraTexCoord;
layout(location = 0) out vec4 o_FragColor;
void main() {
  vec3 tc = (v_CameraTexCoord / v_CameraTexCoord.z);
  o_FragColor = texture(u_CameraColorTexture, tc.xy);
}

Consulte o app de exemplo hello_eis_kotlin para saber mais.