Compartir a través de


Tocar y listo

Orientación con la brújula de Windows Phone

Charles Petzold

Descargar el ejemplo de código

Charles PetzoldLos humanos pocas veces usamos nuestros sentidos en forma completamente aislada: una combinación de la visión y de la audición nos permite construir una imagen mental de nuestro alrededor; una mezcla del gusto, el olfato y el tacto afecta nuestra capacidad de disfrutar una comida y; usamos en conjunto el tacto, la vista y la audición cuando tocamos un instrumento musical.

Lo mismo pasa con los smartphones: un smartphone puede “ver” a través del lente de la cámara, “oír” por medio del micrófono, “sentir” con la pantalla táctil y conocer su ubicación en el mundo gracias al GPS y el sensor de orientación. Y cuando se combinan los datos de entrada de estos sensores, el resultado solo se puede describir como sinergia.

El problema del acelerómetro

El acelerómetro en Windows Phone es un buen ejemplo de un sensor que proporciona cierta información fundamental, pero que se vuelve mucho más valioso al combinarlo con otro sensor, específicamente la brújula.

El hardware del acelerómetro realmente mide fuerza, pero por lo que sabemos de la física, la fuerza es igual a la masa por la aceleración, así que el acelerómetro responde a cualquier tipo de aceleración. Cuando el teléfono se mantiene quieto, el acelerómetro mide la gravedad y proporciona un vector en 3D que apunta hacia el centro de la Tierra. Este vector es relativo a un sistema de coordenadas en 3D, tal como se aprecia en la Ilustración 1. Este sistema de coordenadas es el mismo, independientemente de si codificamos el programa en Silverlight o en XNA, o si se ejecuta en el modo vertical u horizontal.

The Phone’s Sensor Coordinate System
Ilustración 1 Sistema de coordenadas del sensor del teléfono

La clase Accelerometer proporciona el vector de aceleración en la forma de un valor Vector3. Este es un tipo XNA, por lo tanto, si debemos usarlo en un programa Silverlight, necesitamos una referencia al ensamblado Microsoft.Xna.Framework.

Aunque el vector de aceleración permite que el programa que se ejecuta en el teléfono determine la orientación del teléfono con relación a la Tierra, le falta cierta información fundamental. A continuación voy a mostrar lo que quiero decir.

En el código descargable de este artículo (disponible en archive.msdn.microsoft.com/mag201206TouchAndGo) hay una solución de Visual Studio llamada 3DPointers que contiene cuatro proyectos XNA 3D, los que se parecen bastante entre sí. El programa Accelerometer 3D dibuja un “alfiler” en 3D que flota en el espacio en dirección del vector del acelerómetro, tal como vemos en la Ilustración 2.

The Accelerometer 3D Display
Ilustración 2 Visualización de Accelerometer 3D

Todos los programas que usa un sensor de Windows Phone necesitan una referencia al ensamblado Microsoft.Devices.Sensors. Normalmente, los programas XNA en el teléfono se ejecutan en el modo horizontal, lo que puede ser un problema, ya que no coincide con el sistema de coordenadas de los sensores. Para facilitarme las cosas, reorienté el sistema de coordenadas de XNA para el modo vertical en el constructor del derivado Game del programa:

graphics.IsFullScreen = true;
graphics.PreferredBackBufferWidth = 480;
graphics.PreferredBackBufferHeight = 800;

En Windows Phone 7.1, la API del sensor cambió un poco para ser más coherente entre los diferentes sensores. El constructor del programa Accelerometer 3D usa esta nueva API para crear una instancia de Accelerometer que se guarda como un campo:

if (Accelerometer.IsSupported)
{
  accelerometer = new Accelerometer
  {
    TimeBetweenUpdates = this.TargetElapsedTime
  };
}

La propiedad TimeBetweenUpdates predeterminada es 25 milisegundos; aquí se estableció en la velocidad de fotogramas del programa, que es 33 ms.

El programa usa un reemplazo del método OnActivated para iniciar el acelerómetro:

if (accelerometer != null)
{
  try { accelerometer.Start(); }
  catch { }
}

Si bien es difícil imaginar un escenario en que el método Start sufra un error en este punto, se recomienda el uso de un bloque try. La brújula se detiene en OnDeactivated:

if (accelerometer != null)
    accelerometer.Stop();

El programa usa el método LoadContent para crear los vértices 3D del “alfiler” y define BasicEffect para almacenar la información de la cámara e iluminación. El alfiler se define de modo que la base se encuentre en el origen y para que se extienda una unidad por encima del eje Y positivo. La cámara apunta directamente al origen desde el eje Z positivo.

El método Update del programa luego usa el vector accelerometer para definir una transformación del mundo. Esta transformación del mundo desplaza el alfiler eficazmente con relación al origen. En la Ilustración 3 se muestra el código.

Ilustración 3 Método Update en Accelerometer 3D

protected override void Update(GameTime gameTime)
{
  if (GamePad.GetState(PlayerIndex.One).Buttons.Back 
    == ButtonState.Pressed)
      this.Exit();
  if (accelerometer != null && accelerometer.IsDataValid)
  {
    Vector3 acceleration = accelerometer.CurrentValue.Acceleration;
    text = String.Format("X = {0:F3}\nY = {1:F3}\nZ = {2:F3}",
                            acceleration.X,
                            acceleration.Y,
                            acceleration.Z);
    textPosition = new Vector2(0, this.GraphicsDevice.Viewport.Height -
                                    segoe24Font.MeasureString(text).Y);
    acceleration.Normalize();
    Vector3 axis = Vector3.Cross(acceleration, Vector3.UnitY);
    // Special case for magnetometer equal to (0, 1, 0) or (0, -1, 0)
    if (axis.LengthSquared() == 0)
        axis = Vector3.UnitX;
    else
        axis.Normalize();
    float angle = -(float)Math.Acos(Vector3.Dot(Vector3.UnitY, 
      acceleration));
    basicEffect.World = Matrix.CreateFromAxisAngle(axis, angle);
  }
  else
  {
    basicEffect.World = Matrix.Identity;
    text = "";
  }
  base.Update(gameTime);
}

El método obtiene el valor Acceleration directamente del objeto Accelerometer, si la propiedad IsDataValid es verdadera. Se debe girar el alfiler en función del ángulo entre el vector Acceleration y el eje Y positivo. El producto escalar de estos dos vectores proporciona dicho ángulo, mientras que el producto vectorial proporciona el eje de rotación.

Pero pruebe lo siguiente: póngase de pie y sostenga el teléfono en la mano en un ángulo determinado. El alfiler apunta hacia abajo. Ahora, sin cambiar de lugar, gire 360 grados. Mientras gira, el alfiler se mantiene en la misma posición (aproximadamente). El acelerómetro no puede discernir cuando el teléfono se mueve alrededor de un eje paralelo al vector de aceleración. Cuando escribimos una aplicación que debe conocer la orientación 3D completa del teléfono, el acelerómetro solo proporciona una parte de la información necesaria.

La brújula al rescate

Cuando Windows Phone recién apareció, no estaba muy claro si los teléfonos poseían brújula o no. Al parecer nadie tenía una respuesta definitiva. Sin embargo, para los programadores de aplicaciones, la situación era muy sencilla: Como no existía ninguna interfaz de programación para una brújula, incluso si esta existía, no la podíamos usar.

La llegada de Windows Phone 7.1 clarificó la situación: aunque los dispositivos Windows Phone deben contar obligatoriamente con una brújula, algunos dispositivos Windows Phone siempre tuvieron una, inclusive un smartphone con Windows Phone que adquirí en diciembre de 2010. Lo mejor de todo es que los programadores puede usar esta brújula. Windows Phone 7.1 lanzó una nueva clase Compass que le permite al programador de la aplicación saber si la brújula existe (a través de la propiedad estática Compass.IsSupported) y tener acceso a las lecturas. En el emulador de Windows Phone, Compass.IsSupported devuelve falso.

En el centro de la brújula del teléfono se encuentra una pieza de hardware conocida como magnetómetro. Este magnetómetro se ve afectado por cualquier fuerza magnética cercana al teléfono, inclusive los altavoces en el escritorio. Si mantiene alejados estos imanes del teléfono, el magnetómetro medirá la fuerza y dirección del campo magnético de la Tierra.

La clase Compass proporciona datos en la forma de una estructura CompassReading. Los datos sin procesar del magnetómetro están disponibles en una propiedad llamada MagnetometerReading, que es otro Vector3 relativo al sistema de coordenadas que aparece en la Ilustración 1. Cuando no hay imanes cercanos, este vector se alinea con el campo magnético de la Tierra. Por supuesto, el vector apunta aproximadamente hacia el norte, pero en la mayoría de las ubicaciones en el hemisferio norte, también apunta hacia el interior de la tierra. Si sostiene el teléfono con la pantalla hacia arriba, este vector poseerá un componente -Z significativo.

La solución DPointers contiene un proyecto llamado Magnetometer 3D que es semejante al proyecto Accelerometer 3D, excepto que usa la clase Compass en vez de la clase Accelerometer. En la Ilustración 4 aparece la visualización de este programa cuando sostengo el teléfono en mi departamento en Manhattan con la pantalla apuntando hacia arriba y la parte superior del teléfono apuntando hacia “el barrio alto”, es decir, con los costados izquierdo y derecho del teléfono alineados (lo mejor que pude) con las avenidas de la ciudad de Nueva York. (Un mapa revela que estas avenidas tienen una inclinación de 30 grados de este a norte).


Ilustración 4 Visualización de Magnetometer 3D

La documentación indica que el vector MagnetometerReading está en unidades de microteslas. En dos de los dispositivos comerciales con Windows Phone que poseo, este vector generalmente tiene una magnitud cercana a 30, lo que es aproximadamente correcto. (Los valores del vector que aparecen en la Ilustración 4 tienen una magnitud compuesta de 43). Sin embargo, en un tercer teléfono que poseo, el vector MagnetometerReading está normalizado y siempre tiene una magnitud de 1.

Ahora pruebe lo siguiente: sostenga el teléfono para que el vector Magnetometer 3D esté prácticamente alineado con el eje Y positivo de la Ilustración 1. Ahora gire el teléfono alrededor del eje paralelo a dicho vector. El vector se mantiene igual (aproximadamente), lo que indica que el magnetómetro del teléfono tampoco comprende completamente la orientación del teléfono.

Rumbos de la brújula

Normalmente cuando usamos una brújula, no queremos un vector 3D alineado con el campo magnético de la Tierra. Un vector 2D tangente a la superficie de la Tierra sería mucho más útil.

Como seguramente sabe, el campo magnético de la Tierra no coincide con el eje de rotación de la Tierra. La dirección del eje de la Tierra se llama norte geográfico o norte verdadero y corresponde al norte que se usa en los mapas y para prácticamente todos los otros fines. En las superficies de dos dimensiones generalmente se usan ángulos para representar la dirección del norte. Esta dirección se suele llamar rumbo o marcación de la brújula.

La diferencia entre el norte magnético y el norte verdadero varía en todo el orbe. En la ciudad de Nueva York, se deben restar 13 grados de la marcación magnética para obtener la marcación verdadera, pero en Seattle, se deben sumar 21 grados a la marcación magnética. La clase Compass realiza estos cálculos por usted en función de la ubicación del teléfono. Además del vector MagnetometerReading, CompassReading también proporciona dos propiedades del tipo double llamadas MagneticHeading y TrueHeading. Estos ángulos están expresados en grados (entre 0 y 360) y se miden en dirección contraria a las manecillas del reloj desde el eje Y positivo que aparece en la Ilustración 1.

TrueHeading siempre se debe interpretar como un valor aproximado, e incluso así, no es de absoluta confianza. En dos de los teléfonos que poseo, TrueHeading suele estar en lo correcto, pero en otro teléfono está se desvía en unos 70 grados.

No he logrado entender el valor MagneticHeading. Para una ubicación determinada, la diferencia entre los valores de TrueHeading y MagneticHeading debería ser una constante. Por ejemplo, en el lugar en el que vivo, el valor de TrueHeading menos Magnetic­Heading debería ser aproximadamente -13 grados. En mis tres teléfonos, la diferencia entre TrueHeading y MagneticHeading varía esporádicamente entre dos valores, según la orientación del teléfono. A veces la diferencia es –12 (lo que es aproximadamente correcto), pero generalmente la diferencia es 92. Estos son los únicos valores que he observado para la diferencia. MagneticHeading no coincide en ninguno de mis teléfonos con el ángulo derivado de los valores X e Y del vector MagnetometerReading.

Tal como vimos, en los programa XNA podemos obtener un valor actual de un sensor simplemente durante el método Update. Al usar una clase de sensor en un programa Silverlight, debemos configurar un controlador para el evento CurrentValueChanged. Luego podemos obtener un objeto de lectura del sensor de los argumentos del evento.

El código descargable de este artículo contiene dos programas Silverlight (Arrow Compass y Dial Compass) que usan la propiedad TrueHeading para mostrar la dirección del norte. Todos los gráficos están definidos en XAML. Al igual que en los programas XNA, estos programas Silverlight crean el objeto Compass en los constructores, pero también configuran un controlador para la propiedad CurrentValueChanged:

if (Compass.IsSupported)
{
  compass = new Compass();
  compass.TimeBetweenUpdates = TimeSpan.FromMilliseconds(33);
  compass.CurrentValueChanged += OnCompassCurrentValueChanged;
}

En Arrow Compass este controlador configura el ángulo en un objeto RotateTransform adjunto al gráfico de una flecha:

this.Dispatcher.BeginInvoke(() =>
{
  arrowRotate.Angle = -args.SensorReading.TrueHeading;
  accuracyText.Text = String.Format("±{0}°",
                      args.SensorReading.HeadingAccuracy);
});

El controlador CurrentValueChanged se llama en un subproceso separado, por lo que debemos usar un Dispatcher para actualizar cualquier objeto de la interfaz de usuario. Como el ángulo TrueHeading indica un desplazamiento contrario a las manecillas del reloj y como las rotaciones de Silverlight son en el sentido contrario, el código usa el valor negativo del ángulo del rumbo para la rotación.

Estos resultados aparecen en la Ilustración 5, nuevamente con el teléfono apuntando hacia el barrio alto de la ciudad de Nueva York.

The Arrow Compass Display
Ilustración 5 Visualización de Arrow Compass

En Dial Compass, la flecha se mantiene fija mientras que un disco gira para indicar la dirección, tal como podemos apreciar en la Ilustración 6. Usamos esta variación cuando queremos conocer la dirección a la que apunta la parte superior del teléfono, en vez de la dirección del norte relativa al teléfono.

The Dial Compass Display
Ilustración 6 Visualización de Dial Compass

Si ejecuta cualquiera de estos programas y sostiene el teléfono de manera que la pantalla apunte hacia el suelo, la brújula deja de funcionar correctamente. La rotación es opuesta a lo que debería ser. Si tiene necesidad de corregir esto, deberá usar el valor positivo de TrueHeading cuando el valor Z del vector acceleration es positivo.

Calibración de la brújula

En la esquina inferior izquierda, el programa Arrow Compass muestra la propiedad HeadingAccuracy del valor CompassReading. En teoría, esto proporciona la precisión de los valores de rumbo. En la práctica, he visto valores HeadingAccuracy entre 5 y 30 por ciento. (¡Pero solo he visto el 5 por ciento en el teléfono que está totalmente errado!)

La clase Compass también define un evento llamado Calibrate que se activa cuando el valor de HeadingAccuracy supera el 20 por ciento.

Puede realizar una maniobra de calibración para reducir este HeadingAccuracy: Sostenga el teléfono alejado del cuerpo con la pantalla apuntando hacia la izquierda o derecha y luego mueva el brazo describiendo el patrón infinito varias veces. El código de ejemplo proporcionado con los tutoriales de MSDN (en bit.ly/yYrHrL) incluso incluye un gráfico que puede mostrar para notificarle a los usuarios cuando deben volver a calibrar la brújula.

Combinación de la brújula y el acelerómetro

La brújula del teléfono es un ejemplo perfecto de un sensor que obviamente tiene cierta utilidad por su propia cuenta (especialmente si estamos perdidos en un bosque), pero que se vuelve mucho más valioso al combinarlo con otros sensores, especialmente con el acelerómetro. Juntos, estos dos sensores pueden proporcionar una orientación completa del teléfono en el espacio 3D.

De hecho, Windows Phone 7.1 define una nueva clase con este mismo fin. Aparte de entregar la orientación del teléfono en tres formas diferentes, la clase Motion también incorpora información de la nueva clase Gyroscope, cuando el teléfono cuenta con un giroscopio.

Además, la clase Motion realiza labores adicionales y suaviza los datos del acelerómetro y de la brújula. Si usted ejecutó los programas que hemos visto hasta el momento, probablemente se percató de que los datos de estas clases son bastante inestables. En la clase Motion desaparece la inestabilidad.

Sin embargo, como soy del tipo de persona que disfruta los desafíos, se me ocurrió combinar los datos del acelerómetro y de la brújula “manualmente”, y debo admitir que la gracias a esta experiencia tengo un aprecio mayor por la clase Motion.

El programa Compass 3D muestra cuatro alfileres de diferentes colores que aparecen en círculo apuntando al norte (plateado), este (rojo), sur (verde) y oeste (azul). El programa intenta mostrar el plano de los cuatro alfileres en forma paralela a la superficie de la Tierra y orientado correctamente hacia los cuatro puntos de la brújula.

La estrategia que seguí fue derivar los ángulos de Euler. Estos son tres ángulos que representan la rotación alrededor de los ejes X, Y y Z y juntos describen una orientación en el espacio tridimensional. En la dinámica de vuelo, estos ángulos se llaman cabeceo, alabeo y guiñada. Desde la perspectiva de una aeronave, el cabeceo indica si la nariz está hacia arriba o abajo y en qué magnitud, mientras que el alabeo indica cualquier inclinación del avión hacia la derecha o izquierda. Estos ángulos de rotación se pueden ver como relativos a dos ejes: el cabeceo corresponde a la rotación sobre un eje que se extiende a través de las alas, mientras que el alabeo se basa en un eje que va desde la parte frontal hasta la posterior del avión. La guiñada es la rotación sobre un eje perpendicular a la superficie de la Tierra e indica el rumbo de brújula del avión.

Para visualizar estos ángulos con relación al sistema de coordenadas del teléfono en la Ilustración 1, imagine que se desplaza sobre su teléfono como si fuera una alfombra mágica, sentado sobre la pantalla con la parte superior del teléfono delante suyo y los tres botones en la parte posterior. El cabeceo es la rotación sobre el eje X, el alabeo es la rotación sobre el eje Y y la guiñada es la rotación sobre el eje Z.

Calcular el cabeceo y el alabeo a partir del vector de aceleración resultó bastante fácil e involucra fórmulas estándar:

float roll = (float)Math.Asin(-acceleration.X);
float pitch = (float)Math.Atan2(acceleration.Y, -acceleration.Z);

Con el teléfono plano sobre una mesa con su pantalla hacia arriba, tanto el cabeceo como el alabeo son cero. El alabeo va desde –π/2 a π/2 y los valores vuelven a cero cuando el teléfono se coloca boca abajo. El cabeceo va desde –π a π y alcanza los valores máximos cuando el teléfono se coloca boca abajo. Cuando la pantalla del teléfono está hacia arriba, la guiñada debería ser aproximadamente el mismo valor que la propiedad TrueHeading de la brújula, pero convertido a radianes para propósitos de XNA:

float yaw = MathHelper.ToRadians((float)compass.CurrentValue.TrueHeading);

Sin embargo, como ya vimos con los dos programas de brújula de Silverlight, TrueHeading deja de funcionar correctamente cuando la pantalla del teléfono apunta hacia la Tierra, por lo que se debe corregir la guiñada. Luego de pasar por algunas excursiones teóricas y empíricas sobre el tema, lo dejé tal cual y creé una transformación del mundo a partir de los tres ángulos:

basicEffect.World = Matrix.CreateRotationZ(yaw) *
                    Matrix.CreateRotationY(roll) *
                    Matrix.CreateRotationX(pitch);

Los resultados aparecen en la Ilustración 7.

The Compass 3D Program
Ilustración 7 Programa Compass 3D

También incluí un programa Orientation 3D que obtiene estos tres ángulos a partir de la clase Motion. Puede comprobar cómo estos resultados son mucho más suaves (además de funcionar mejor cuando el teléfono está al revés).

La clase Motion es un complemento demasiado importante para la API del sensor como para este único programa. Como podrá ver en la siguiente entrega de esta columna, la clase realmente sirve como portal hacia el mundo 3D.

Charles Petzold ha sido colaborador durante largo tiempo de MSDN Magazine. La dirección de su sitio web es charlespetzold.com.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Drew Batchelor