miércoles, 9 de mayo de 2012

Realidad Aumentada en Android. Acelerómetro y magnetómetro

Con este último ejemplo, termino con la serie de posts dedicados a un framework de realidad virtual en Android. Para ver un ejemplo básico de uso del acelerómetro en Android, voy a publicar el típico ejemplo de una Brújula.

Para desarrollar una aplicación que haga las veces de brújula debo implementar una vista especifica que pinte la flecha de indicación al Norte actualizándose cada vez que se obtenga una actualización del acelerómetro y magnetómetro.

La implementación de la clase View sobrescribe los métodos onDraw y onMeasure, y crea un método update que refresca el valor de la orientación (dirección) que se usa para dibujar la linea roja que apunta al norte. Cuando se extiende la clase View y no se sobreescribe el método onMesure, la aplicación falla en el momento de usar la vista. El método onDraw, dibuja un círculo con el radio devuelto por el menor valor entre el ancho y el alto de la pantalla y una linea roja en forma de radio del círculo, que indica el Norte y que toma como valor de cálculo la dirección calculada en el SensorListener de la clase BrujulaActivity. En el método update se refresca el valor de la dirección y se repinta la vista.

package com.m607;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

public class BrujulaView extends View {

 private float direction;
 private Paint paint;
 public BrujulaView(Context context) {
  super(context);
  paint = new Paint(Paint.ANTI_ALIAS_FLAG);
 }

 public BrujulaView(Context context, AttributeSet attrs) {
  super(context, attrs);
  paint = new Paint(Paint.ANTI_ALIAS_FLAG);
 }

 public BrujulaView(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  paint = new Paint(Paint.ANTI_ALIAS_FLAG);
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
    MeasureSpec.getSize(heightMeasureSpec));
 }

 @Override
 protected void onDraw(Canvas canvas) {
  
  paint.reset();
  int w = getMeasuredWidth();
  int h = getMeasuredHeight();
  int r;
  if (w > h) {
   r = h / 2;
  } else {
   r = w / 2;
  }

  
  paint.setStyle(Paint.Style.STROKE);
  paint.setStrokeWidth(5);
  paint.setColor(Color.WHITE);

  canvas.drawCircle(w / 2, h / 2, r, paint);

  paint.setColor(Color.RED);
  canvas.drawLine(w / 2, h / 2,
    (float) (w / 2 + r * Math.sin(-direction)), 
    (float) (h / 2 - r * Math.cos(-direction)), paint);

 }

 public void update(float dir) {
  direction = dir;
  invalidate();
 }
}


Por último queda la actividad que controla las actualizaciones en el acelerómetro y en el magnetómetro. En el método onCreate se recuperan los TextView donde se va mostrar información sobre los valores de los tres ejes de inclinación, azimut, pitch y roll. Además se recupera del main.xml el objeto BrujulaView sobre el que se actuará para notificar la dirección actual. Además se recuperan las instancias a los sensores, acelerómetro y magnetómetro mediante Sensor.TYPE_ACCELEROMETER y Sensor.TYPE_MAGNETIC_FIELD.
Se implementa el listener de sensores SensorEventListener implemtando la interfaz en el cuerpo de la actividad. Se sobrescribe el método onSensorChanged para actualizar la orientación del teléfono en relación al evento SensorEvent. En caso de ser un evento de acelerómetro se actualiza la martriz de valores de acelétometro valoresAcelerometro y equivalentemente para un evento de cambio de campo magnético.
Mediante el método getRotationMatrix al que se le pasan los valores almacenados en los arrays de valores de acelerómetro y mangnetómetro, se recuperan los valores de rotación en relación a la tierra, en vez de los valores en relación al móvil, que nunca cambian. Posteriormente se obtienen los valores de orientación mediante la llamada al método getOrientation y se asigna el primer valor del array, matrixValues a la dirección que sirve para repintar la linea roja que indica el Norte.

package com.m607;

import android.app.Activity;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.TextView;

public class BrujulaActivity extends Activity implements SensorEventListener {

 private SensorManager sensorManager;
 private Sensor sensorAcelerometro;
 private Sensor sensorCampoMagnetico;

 private float[] valoresAcelerometro;
 private float[] valoresCamposMagnetico;

 private float[] matrizRotacion;
 private float[] matrizInclinacion;
 private float[] matrixValues;

 private TextView readingAzimuth, readingPitch, readingRoll;
 
 private BrujulaView brujulaView;

 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  readingAzimuth = (TextView) findViewById(R.id.azimuth);
  readingPitch = (TextView) findViewById(R.id.pitch);
  readingRoll = (TextView) findViewById(R.id.roll);

  brujulaView = (BrujulaView) findViewById(R.id.brujula);

  sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
  sensorAcelerometro = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  sensorCampoMagnetico = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);

  valoresAcelerometro = new float[3];
  valoresCamposMagnetico = new float[3];

  matrizRotacion = new float[9];
  matrizInclinacion = new float[9];
  matrixValues = new float[3];
 }

 @Override
 protected void onResume() {

  sensorManager.registerListener(this, sensorAcelerometro,
    SensorManager.SENSOR_DELAY_NORMAL);
  sensorManager.registerListener(this, sensorCampoMagnetico,
    SensorManager.SENSOR_DELAY_NORMAL);
  super.onResume();
 }

 @Override
 protected void onPause() {

  sensorManager.unregisterListener(this, sensorAcelerometro);
  sensorManager.unregisterListener(this, sensorCampoMagnetico);
  super.onPause();
 }

 @Override
 public void onAccuracyChanged(Sensor arg0, int arg1) {
  // TODO Auto-generated method stub

 }

 @Override
 public void onSensorChanged(SensorEvent event) {
  

  switch (event.sensor.getType()) {
  case Sensor.TYPE_ACCELEROMETER:
   
   System.arraycopy(event.values, 0, valoresAcelerometro, 0, event.values.length);
   
   break;
  case Sensor.TYPE_MAGNETIC_FIELD:
   System.arraycopy(event.values, 0, valoresCamposMagnetico, 0, event.values.length);
   break;
  }

  boolean exito = SensorManager.getRotationMatrix(matrizRotacion, matrizInclinacion,
    valoresAcelerometro, valoresCamposMagnetico);

  if (exito) {
   SensorManager.getOrientation(matrizRotacion, matrixValues);

   double azimuth = Math.toDegrees(matrixValues[0]);
   double pitch = Math.toDegrees(matrixValues[1]);
   double roll = Math.toDegrees(matrixValues[2]);

   readingAzimuth.setText("Azimuth: " + String.valueOf(azimuth));
   readingPitch.setText("Pitch: " + String.valueOf(pitch));
   readingRoll.setText("Roll: " + String.valueOf(roll));

   brujulaView.update(matrixValues[0]);
  }

 }
}

Por último, el main.xml se ve así:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical" >
 
  <TextView
      android:id="@+id/azimuth"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"/>
  <TextView
      android:id="@+id/pitch"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"/>
  <TextView
      android:id="@+id/roll"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"/>
  <View
      class="com.m607.BrujulaView"
      android:id="@+id/brujula"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"/>
 
</LinearLayout>

Y el manifest de la aplicación:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.m607"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="10" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".BrujulaActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

1 comentario:

  1. Excelente tu aporte, pero si no es mucha molestia me gustaría saber si tienes un ejemplo de como comparar el sensor vs el gps ya que en una aplicación de realidad aumentada infiere mucho hacia que dirección este la cámara y según entiendo esto se resuelve con el sensor agradecería mucho tu ayuda

    ResponderEliminar