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).