Android Code Snippet: Augmented Reality y Sistemas de coordenadas


La realidad aumentada es un concepto que incluye a muchos tipos diferentes de aplicaciones.

Algunas descodifican códigos QR dibujando "Pokemons" (o coches, dinosaurios, personajes animados...) 3D sobre ellas, otras detectan caras y rasgos faciales, reconocen objetos... 

Y luego hay otro gran grupo que utiliza los sensores del teléfono para "localizar" objetos o información a nuestro alrededor. 

Para este último grupo necesitamos usar los sensores del dispositivo para localizarnos con respecto al entorno que nos rodea. Hay varios tipos de sensores (aceleración, geomagnéticos, gravedad...) que junto a la información de geoposicionamiento podemos utilizar para nuestra aplicación.

Lo primero que hay que saber, es que la información que se obtiene de estos sensores se refiere al sistema de coordenadas del dispositivo y éste no cambia con la orientación ("portrait"/"landscape"), ni con el movimiento del dispositivo.



En segundo lugar, debéis decidir cual es el sistema de referencia que vuestra aplicación necesita. En mi opinión a varios tipos que dependen del diseño de vuestra aplicación y sobre todo de como se sujete el móvil:
  1. Aplicación en el que el móvil se sujeta verticalmente: la más común, por ejemplo la típica aplicación en la que se utiliza la cámara y que situáis información de objetos/edificios/monumentos sobre la pantalla. 
  2. Aplicación en el que el móvil se sujeta horizontalmente (paralelo al suelo): por ejemplo una brújula, compás, mapas...
  3. Otras: por ejemplo las de posición de estrellas/fases lunares... en el firmamento o juegos como ¿Dónde está la mosca? aquí que cada uno aguante su vela y elija su propio sistema (aunque lo más probable es que el más conveniente sea el del primer tipo).
Siendo el tipo 1 el más común es el que vamos a explicar ya que adaptarlo para los otros tipos no es muy difícil. En este caso se define el eje Y hacia dentro (hacia adonde apunta la cámara). Lo explican aquí.

  1. Implementáis el interfaz SensorEventListener en vuestra clase.
  2. Obtenéis una instancia de SensorManager (p.ej. desde una Activity (SensorManager)getSystemService(Context.SENSOR_SERVICE); ).
  3. Registráis los sensores:
    private void registerSensors() {
     Sensor accelerometer = mSensorManager
       .getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
     boolean success = mSensorManager.registerListener(this, accelerometer,
       SensorManager.SENSOR_DELAY_GAME);
     Sensor magneticField = mSensorManager
       .getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
     success &= mSensorManager.registerListener(this, magneticField,
       SensorManager.SENSOR_DELAY_GAME);
     
     if(!success) {
      Log.e(TAG, "SensorManager not supported");
      new java.lang.Exception("SensorManager not supported");
     }
    }
  4. En el método del interfaz implementado: 
  5. @Override
    public void onSensorChanged(SensorEvent event) {
    
        float[] gravity = new float[3];
        float[] linearAcceleration = new float[3];
        float[] magnetic = new float[3];
        float[] matrixInR = new float[16];
        float[] matrixOutR = new float[16];
        float[] matrixI = new float[16];
        float[] orientation = new float[3];
      
        switch (event.sensor.getType()) {
        case Sensor.TYPE_ACCELEROMETER:
        gravity = event.values.clone();
        break;
        case Sensor.TYPE_MAGNETIC_FIELD:
        magnetic = event.values.clone();
        break;
        default:
        break;
        }
    
      // If gravity and geomag have values then find rotation matrix
      if (gravity != null && magnetic != null) {
       
      // checks that the rotation matrix is found
      boolean success = SensorManager.getRotationMatrix(matrixInR,
         matrixI, gravity, magnetic);
      if (success) {
        // Re-map coordinates so y-axis comes out of camera
        SensorManager.remapCoordinateSystem(matrixInR,
        SensorManager.AXIS_X, SensorManager.AXIS_Z,
        matrixOutR);
    
        // Finds the Azimuth and Pitch angles of the y-axis with
        // magnetic north and the horizon respectively
        SensorManager.getOrientation(matrixOutR, orientation);
        float azimuth = (float) Math.toDegrees(orientation[0]);
        float pitch = (float) Math.toDegrees(orientation[1]);
        float roll = (float) Math.toDegrees(orientation[2]);
       }
     }
    }
    


¡Y ya está! :P Bueno aún queda un poco. Como podéis comprobar el resultado son 3 parámetros (azimuth, pitch y roll) que si tenéis algo de conocimientos de aeronáutica o astronomía os sonarán mucho. Son los ángulos de rotación de vuestro dispositivo con respecto al sistema de referencia por el norte magnético, el eje de gravedad y el eje resultante del producto vectorial de ambos:


Donde los ángulos se definen como (documentación):

  • azimuth: rotación sobre el eje Z
  • pitch: rotación sobre el eje X
  • roll: rotación sobre el eje Y
Dependiendo de la aplicación y el tipo de cálculos matemáticos que requiera, podéis utilizar los ángulos de rotación, la propia matriz de rotación (la que he definido en el ejemplo como matrixOutR, es la típica matriz 4x4 de rotación tal y como se define en Wikipedia)  e incluso existen métodos para trabajar con Cuaternios. A partir de aquí ya cada uno tendrá que seguir su camino.

Y por último hay que tener en cuenta el ruido en los datos proporcionados por los sensores. Puede que sea necesario, por ejemplo, aplicar un filtro paso baja a los valores obtenidos antes de hacer los cálculos. Aunque en mi experiencia es casi más importante si no ya imprescindible filtrar los valores obtenidos para pintar una vez ya calculados (es decir las posiciones x,y donde pintas en el display), porque puede resultar muy molesto al usuario.