Decíamos en el anterior artículo que la realidad aumentada combina datos generados por ordenador con imágenes del entorno obtenidas en tiempo real. La parte complicada es la superposición de los datos sobre la imagen en la posición correcta. Así que vamos a comenzar con una aplicación de realidad levemente aumentada: una brújula virtual.

AR CompassMi pantalla apunta al noroeste

AR Compass (brújula de realidad aumentada, en inglés queda más cool) es una aplicación para Android que muestra una brújula superpuesta a la imagen capturada por la cámara. La brújula consiste en una serie de líneas verticales con el norte, el sur, el este y el oeste marcados.

La aplicación usa OpenGL para mostrar los puntos cardinales sobre la imagen de la cámara. A partir de los datos del acelerómetro y del magnetómetro obtenemos la matriz de rotación que nos permite mostrar la dirección correcta en la pantalla.

La imagen de la cámara

Es código para mostrar la imagen de la cámara es bastante simple, y se puede sacar del ejemplo CameraPreview:

   1: public class CameraPreview extends Activity {
   2:     private Preview mPreview;
   3:
   4:     @Override
   5:     protected void onCreate(Bundle savedInstanceState) {
   6:         super.onCreate(savedInstanceState);
   7:
   8:         // Hide the window title.
   9:         requestWindowFeature(Window.FEATURE_NO_TITLE);
  10:
  11:         // Create our Preview view and set it as the content of our activity.
  12:         mPreview = new Preview(this);
  13:         setContentView(mPreview);
  14:     }
  15:
  16: }

En la línea 9 indicamos que no queremos que la ventana tenga titulo. Después simplemente instanciamos la vista y se la asignamos a la actividad. La vista es la clase Preview, que hereda de SurfaceView. SurfaceView es un tipo de vista que se caracteriza por contener una superficie (un objeto Surface) sobre la que dibujar.

   1: class Preview extends SurfaceView implements SurfaceHolder.Callback {
   2:     SurfaceHolder mHolder;
   3:     Camera mCamera;
   4:
   5:     Preview(Context context) {
   6:         super(context);
   7:
   8:         // Install a SurfaceHolder.Callback so we get notified when the
   9:         // underlying surface is created and destroyed.
  10:         mHolder = getHolder();
  11:         mHolder.addCallback(this);
  12:         mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
  13:     }
  14: ...
  15: }

Además de heredar de SurfaceView, la clase Preview implementa la interfaz SurfaceHolder.Callback. La clase SurfaceHolder es el contendor que nos da acceso a la superficie, la cual no se suele usar directamente. A través de los métodos de esta interfaz nos enteraremos de cuando se crea, destruye o cambia de tamaño la superficie.

Las líneas 10 y 11 sirven para indicarle al SurfaceHolder que notifique los cambios de la superficie a nuestra instancia. La línea 12 especifica el tipo de superficie a usar, en este caso una que no posee sus propios buffers. No tengo muy claro qué significa esto, pero imagino que el resto de los tipos de superficie tendrán uno o dos buffers para renderizar la imagen, mientras que este tipo necesita que le propercionen los buffers con la imagen a buscar. En cualquier caso es el único tipo con el que funciona la cámara.

   1: public void surfaceCreated(SurfaceHolder holder) {
   2:     // The Surface has been created, acquire the camera and tell it where
   3:     // to draw.
   4:     mCamera = Camera.open();
   5:     try {
   6:        mCamera.setPreviewDisplay(holder);
   7:     } catch (IOException exception) {
   8:         mCamera.release();
   9:         mCamera = null;
  10:         // TODO: add more exception handling logic here
  11:     }
  12: }
  13:
  14: public void surfaceDestroyed(SurfaceHolder holder) {
  15:     // Surface will be destroyed when we return, so stop the preview.
  16:     // Because the CameraDevice object is not a shared resource, it's very
  17:     // important to release it when the activity is paused.
  18:     mCamera.stopPreview();
  19:     mCamera.release();
  20:     mCamera = null;
  21: }
  22:
  23: public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
  24:     // Now that the size is known, set up the camera parameters and begin
  25:     // the preview.
  26:     Camera.Parameters parameters = mCamera.getParameters();
  27:     parameters.setPreviewSize(w, h);
  28:     mCamera.setParameters(parameters);
  29:     mCamera.startPreview();
  30: }

En estos tres métodos se notifican los eventos relacionados con la superficie donde mostramos la imagen de la cámara. El primero de ellos es surfaceCreated. El bucle de nuestra aplicación recibe un mensaje indicándole que la superficie ha sido creada, e invoca este método pasándole el SurfaceHolder de la misma. En la línea 4 “abrimos” la cámara  y en la 6 le decimos que muestre la imagen de previsualización usando la superficie asociada al holder. Si hay algún problema liberamos la cámara.

Cuando la superficie se destruye (por ejemplo, porque se va a iniciar otra actividad) detenemos la previsualización y liberamos la cámara. El ejemplo de la SDK no incluye la llamada a release(), lo que provoca excepciones Out of memory cuando la ventana se destruye y posteriormente se quiere volver a usar la cámara.

El método surfaceChanged se invoca si la superficie cambia de tamaño (si la pantalla gira, por ejemplo). En nuestro ejemplo no se usará, porque vamos a fijar la ventana de la actividad a formato landscape.

Para usar la cámara necesitaremos los permisos adecuados, en este caso <uses-permission android:name=”android.permission.CAMERA” />. Precisamente la última revisión de la SDK corrige un error por el cual no se forzaba correctamente este permiso.

En la próxima entrada comenzaremos a usar OpenGL para añadir la brújula sobre la cámara.