Previsualización de la cámara de Android en tiempo real


Si queréis hacer cosas más complejas con la cámara, deberéis de desarrollar la vuestra. En concreto, en esta entrada voy a describir como mostrar lo que "se vé" por la cámara en la pantalla del móvil en tiempo real. La parte divertida es que tendréis acceso a los "frames" antes de que se pinten en pantalla.
En unos sencillos pasos:
  1. En primer lugar necesitaréis un "SurfaceView" donde pintar las imágenes. Podéis crearlo o añadirlo al "layout" directamente.
  2. A continuación, hay que pedir permiso en el "manifest":
    <uses-permission android:name="android.permission.CAMERA"/>
  3. Necesitáis un variable del tipo Camera y otra para el "SurfaceView" del primer punto.
    private Camera mCamera = null;
    private SurfaceView mSurfaceViewCameraPreview;
  4. Vuestra clase debe implementar los interfaces android.view.SurfaceHolder.Callback y android.hardware.Camera.PreviewCallback e incluir el siguiente código (por ejemplo en el método "onCreate" y no olvidar que la variable del "SurfaceView" debe estar ya inicializada):
    SurfaceHolder surfaceHolder = mSurfaceViewCameraPreview.getHolder();
    surfaceHolder.addCallback(this);
  5. Incluir las siguientes dos funciones para comenzar/parar la captura desde la camara (utilizo la llamada con el buffer porque es más rápido y el formato no lo cambio y dejo el que hay por defecto "NV21"):
    private void startCamera()
    {
      if(mCamera == null)
      {
        mCamera = Camera.open();
        mCamera.startPreview();
        Camera.Size size = mCamera.getParameters().getPreviewSize();
        int format = mCamera.getParameters().getPreviewFormat();
        byte[] buffer = new byte[size.width*size.height*ImageFormat.getBitsPerPixel(format)/8];
        mCamera.addCallbackBuffer(buffer);
        mCamera.setPreviewCallbackWithBuffer(this);
      }
    }
    
    private void stopCamera()
    {
      if(mCamera != null)
      {
        mCamera.stopPreview();
        mCamera.setPreviewCallback(null);
        mCamera.release();
        mCamera = null;
      }
    }
  6. ¿Cuándo abrir la cámara? Depende de vuestra aplicación y vuestro código, pero siempre esperad a que el "SurfaceView" esté correctamente creado e inicializado. Por ejemplo, lo podéis implementar en el siguiente método del "Callback":
    @Override
    public void surfaceCreated(SurfaceHolder holder) 
    {
      this.startCamera();
    }
  7. No hay que olvidar cerrar la cámara cuando no se vaya a usar, la aplicación se cierre, en pausa, ... llamando al método "stopCamera" del punto 5 en sus respectivos métodos ("onStop", "onDestroy", "surfaceDestroyed"...).
  8. Y por último, leer los "frames" y pintarlos en el "SurfaceView":
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
      
      Camera.Size size = camera.getParameters().getPreviewSize();
      Bitmap bitmap = this.getBitmapFromNV21(data, size.width, size.height, true);
    
      SurfaceHolder holder = mSurfaceViewCameraPreview.getHolder();
      Canvas canvas = holder.lockCanvas();
      canvas.drawColor(Color.BLACK);
      canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), holder.getSurfaceFrame(), null);
      holder.unlockCanvasAndPost(canvas);
    
      mCamera.addCallbackBuffer(data);
    }
  9. Si os habéis fijado en el código anterior, se llama a la función "getBitmapFromNV21" que incluyo a continuación. Es necesaria porque el formato de la imagen del array "data" que llega es Yuv (en concreto NV21 semiplanar). Además el último parámetro es un flag, para que al realizar la conversión también se rote la imagen, porque el array "data" está rotado respecto al "canvas". Estos son dos pequeños "hacks" que incluyo, porque hay otras formas de hacerlo, pero por compatibilidad y velocidad a mi me valen (lo explico más a fondo en ésta otra entrada).
    /**
     * Devuelve un Bitmap desde un array de datos en formato NV21
     * 
     * @param data array en formato NV21
     * @param width
     * @param height
     * @param rotated flag de rotación
     * @return
     */
    public Bitmap getBitmapFromNV21(byte[] data, int width, int height, boolean rotated) {
    
      Bitmap bitmap = null;
      int[] pixels = new int[width * height];
    
      // Convertimos el array 
      this.yuv2rgb(mBitmapPixels, data, width, height, rotated);
    
      // Si está rotado se crea el Bitmap con "width" y "height" intercambiados
      if(rotated)
      {
        bitmap = Bitmap.createBitmap(pixels, height, width, Bitmap.Config.ARGB_8888);
      }
      else
      {
        bitmap = Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888);
      }
    
      return bitmap;
    }
  10. Por último, la función "yuv2rgb" la implemento en esta otra entrada:  Convertir de Yuv a Rgb en Android, pero podéis implementarla vosotros mismos.
El código lo podéis usar libremente, pero está pensado para aprender y basarse en él y no para soluciones comerciales. Sin embargo, probándolo con la función "yuv2rgb" que describo en la otra entrada, se consiguen unos 20fps en un Samsung Galaxy i9000, suficiente para que la imagen sea más o menos fluida. Y estoy seguro que admite muchas optimizaciones (por ejemplo código nativo).

8 comentarios:

  1. Hola!

    Estoy con un TFC de Realiada aumentada y ya controlo la parte del GPS la de la base de datos pero me falta lo mas importante que es que sobre la preview de la Camara pueda dibujar o sobreponer imagenes... Como dibujar era con openGL obte por sobreponer imagenes y explico lo hecho a ver si me podeis ayudar... Hice una clase preview que extiende de SurfaceView e implementa de SurfaceHolder.Callback y ahi muestro al saco la imagen de la camara y me gustaria que en el metodo onDraw mirara cuantos puentos tiene que pintar y los pintara... Pero si lo hago como lo he hecho en otro ejemplo sin preview el programa peta y no me muestra nada... Alguna idea??? No se cuantos puntos tengo que pintar por lo que dependiendo de cada momento tengo que pintar 0 o N... Gracias.

    ResponderEliminar
  2. Usa dos SurfaceView, uno para el preview de la cámara y el otro para pintar. Los puedes apilar en un FrameLayout para que uno quede sobre el otro y el de arriba le pintas con el fondo transparente para que no oculte el preview de la cámara.

    Para pintar el fondo transparente: surfaceView.setFormat(PixelFormat.TRANSPARENT);

    Y lo del layout algo así:







    ResponderEliminar
  3. El layout:

    <FrameLayout
    android:width="match_parent"
    android:height="match_parent">

    <SurfaceView
    android:id="@+id/preview_surface"
    android:width="match_parent"
    android:height="match_parent">
    </SurfaceView>

    <SurfaceView
    android:id="@+id/draw_surface"
    android:width="match_parent"
    android:height="match_parent">
    </SurfaceView>

    </FrameLayout>

    ResponderEliminar
  4. Hola!

    hes estado intentando hacer eso que me dices y no me sale bien y no se si tengo algun error... He probado de mil manera pero cuando voy a utilizar el frame siempre me da error te pongo un poco de codigo a ver que te parece...

    Todo va en el onCreate de la Activity

    frame = (FrameLayout) findViewById(R.id.frame);
    mCamara = new Preview (this);
    frame.addView(mCamara);

    pintar = new MySurface(this);
    frame.addView(pintar);

    setContentView(R.layout.activity_main);

    he probado de mil maneras pero da error cuando llega al frame.addView();

    Si pongo SetContentView(mCamara) o con SetContentView(pintar) me mostraria bien ese surfaceView en concreto...

    Gracias de antemano.

    ResponderEliminar
  5. Hola Ignacio,

    setContentView es a lo primero que tienes que llamar en el onCreate para cargar el layout. Probablemente el error que te de sea un NullPointerException debido a que llamas a findViewById antes de cargar el layout y como todavía no existe devuelve null.

    De todas formas es más fácil que eso:

    1. Digamos que el nombre del paquete de tu app es com.appdeignacio

    2. El layout de tu actividad lo pones así:

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000" >

    <SurfaceView
    android:id="@+id/cameraLayer"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    <com.appdeignacio.MySurface
    android:id="@+id/drawLayer"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    </FrameLayout>

    3. Y el onCreate de tu actividad:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Para poner la app en pantalla completa, probablemente lo necesites así y si no lo puedes quitar
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

    setContentView(R.layout.activity_main);

    // Aquí está el surface de la cámara
    SurfaceView cameraSurface = (SurfaceView)findViewById(R.id.cameraLayer);

    // Aquí tienes tu surface para pintar
    MySurface drawSurface = (MySurface)findViewById(R.id.drawLayer);
    }
    }

    ResponderEliminar
  6. Perfecto!!!

    Poniendo el SetContetView arriba el frame ya no da problemas y funciona! Muchas Gracias! Ahora a situar los puntos en el mapa segun coordenadas pero creo que eso ya se hacerlo. Eres muy grande! Me has dado la alegria del dia!

    ResponderEliminar
  7. exelente blog!! (y)

    tengo una duda si quisiera poner un layout transparente encima del sufaceView eso como seria.
    stoy tratando de hacer un juego AR pero me quede en este punto!!
    Si alguien me puede ayudar le agradeceria mucho!!
    Saludos..

    ResponderEliminar
    Respuestas
    1. Puedes utilizar un FrameLayout y dentro declaras primero el SurfaceView (donde supongo mostrarás la cámara) y luego puedes poner otro SurfaceView para pintar e incluso Layouts adicionales para poner por ejemplo botones y puntuaciones.

      El juego ¿Dónde está la mosca? (Google Play: https://play.google.com/store/apps/details?id=com.itiox.wtf) es un ejemplo de esta técnica. Lo explico un poco más en la entrada: http://blog.itiox.com/2013/06/donde-esta-la-mosca.html

      ¡Y si te gusta el juego no olvides puntuarlo! :P

      Eliminar