一触即发

使用 Windows Phone 指南针确定方位

Charles Petzold

下载代码示例

Charles Petzold在彼此隔离时,我们人类很少会完全使用自己的感官: 通过结合使用视觉和听觉,我们可以在心里留下对周围环境的印象;结合使用味觉、嗅觉和触觉可以影响我们在吃东西时的感受;而在演奏乐器时,我们结合使用了触觉、视觉和听觉。

这也同样适用于智能手机: 智能手机可以通过其相机镜头“看”、通过其麦克风“听”、通过其触摸屏“感觉”以及使用 GPS 和方向传感器了解它在世界上所处的位置。而开始将这些传感器的输入相结合,除了用“相得益彰”来描述产生的效果,再没有更合适的词了。

加速感应器问题

Windows Phone 中的加速感应器是一个很好的传感器示例,它提供了一些基本信息,但在与另一个传感器(具体来说是指南针)结合使用时会更有价值。

实际上,加速感应器硬件测量力,但正如我们从物理学中学到的一样,力等于质量乘以加速度,因此,加速感应器将响应任何种类的加速度。当手机保持静止状态时,加速感应器将测量重力并提供指向地心的三维矢量。此矢量相对于三维坐标系,如图 1 所示。无论您是编写 Silverlight 还是 XNA 程序,在纵向还是横向模式下运行,此坐标系都是相同的。

The Phone’s Sensor Coordinate System
图 1 手机的传感器坐标系

Accelerometer 类以 Vector3 值的形式提供加速度矢量。这是一个 XNA 类型,因此,如果您需要在 Silverlight 程序中使用它,则需要引用 Microsoft.Xna.Framework 程序集。

虽然加速度矢量允许程序在手机上运行以确定手机相对于地球的方向,但它缺少一些关键信息。让我解释一下我讲述的内容。

本专栏的可下载代码(可从 archive.msdn.microsoft.com/mag201206TouchAndGo 获得)中包含一个名为 DPointers 的 Visual Studio 解决方案,它包含四个 XNA 3D 项目,所有这些项目看起来都有点类似。Accelerometer 3D 程序绘制一个三维“指针”,它在空间中的加速感应器矢量方向漂浮,如图 2所示。

The Accelerometer 3D Display
图 2 Accelerometer 3D 显示

使用 Windows Phone 传感器的任何程序需要引用 Microsoft.Devices.Sensors 程序集。通常,手机上的 XNA 程序在横向模式下运行,这可能存在一些问题,因为它与传感器使用的坐标系不匹配。为了更便于我讲述,我在程序的 Game 派生类的构造函数中针对纵向模式重新调整了 XNA 坐标系:

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

在 Windows Phone 7.1 中,传感器 API 已进行了一些更改,以便在不同的传感器之间保持一致。 Accelerometer 3D 程序构造函数使用该新 API 创建一个将保存为字段的 Accelerometer 实例:

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

默认 TimeBetweenUpdates 属性为 25 毫秒;此处,它设置为程序的帧速率(33 毫秒)。

该程序使用 OnActivated 方法覆盖来启动 Accelerometer:

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

虽然此时很难想象 Start 方法失败的场景,但建议您将该方法放在 try 块内。 指南针已在 OnDeactivated 中停止:

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

该程序使用 LoadContent 方法为“指针”建立三维矢量,然后定义 BasicEffect 以存储相机和照明信息。 指针定义为,基点位于原点并沿正 Y 轴向上延伸一个单位。 相机直接从正 Z 轴指向原点。

然后,该程序的 Update 方法使用加速感应器矢量定义世界转换。 此世界转换可有效地相对于原点移动指针。 图 3 显示了相应代码。

图 3 Accelerometer 3D 中的 Update 方法

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);
}

如果 IsDataValid 属性为 true,则该方法直接从 Accelerometer 对象中获取 Acceleration 值。 必须根据加速度矢量和正 Y 轴之间的角度旋转指针。 这两个矢量的点积提供了该角度,而旋转轴是由叉积提供的。

但试试这个: 站起来并将手里的手机朝向特定角度。 指针指向下面。 现在,站在原地不动,旋转 360 度。 在您旋转时,指针将保持相同的位置(或几乎保持不变)。 当手机绕与加速度矢量平行的轴转动时,加速感应器分辨不出来。 如果编写的应用程序需要知道手机的完整三维方向信息,则加速感应器仅提供您所需的一部分信息。

使用指南针解决问题

在首次发布 Windows Phone 时,还不太清楚手机是否包含指南针。 似乎没有人能给出一个明确的答案。 但对于应用程序程序员来说,情况很简单多了: 没有指南针的编程接口,因此,即使有,我们也无法使用它。

但随着 Windows Phone 7.1 的发布,我们澄清了该问题: 虽然 Windows Phone 设备不需要配备指南针,但某些 Windows Phone 设备始终配备了指南针,包括我在 2010 年 12 月购买的运行 Windows Phone 的智能手机。 最令人高兴的是,程序员可以实际使用该指南针。 Windows Phone 7.1 引入了一个新的 Compass 类,应用程序程序员可通过该类了解指南针是否存在(通过静态 Compass.IsSupported 属性)和访问其读数。 Compass.IsSupported 在 Windows Phone 仿真器上返回 false。

手机指南针基于一个称为磁力仪的硬件。 这种磁力仪受手机附近的任何磁力的影响,包括您的电脑桌上的扬声器中可能存在的磁力。 如果将这些磁铁远离手机,磁力仪将测量地球磁场的强度和方向。

Compass 类以 CompassReading 结构的形式提供数据。 原始磁力仪数据是在名为 MagnetometerReading 的属性中提供的,它是相对于图 1 中显示的坐标系的另一个 Vector3。 在附近没有磁铁的情况下,该矢量与地球磁场保持一致。 当然啦,该矢量指向正北方向,但在北半球上的大多数地方,它还会指向地球。 如果将手机屏幕朝上放置,该矢量将具有有效的 –Z 分量。

3DPointers 解决方案包含一个名为 Magnetometer 3D 的项目,它类似于 Accelerometer 3D 项目,所不同的是它使用 Compass 类,而不是 Accelerometer 类。 图 4 显示了此程序在下列情况下的显示内容:我在位于曼哈顿的公寓中将手机屏幕朝上并且手机顶部指向“住宅区,即,手机的左右两侧与纽约市的街道对齐(我尽力使它们接近对齐)。 (地图显示这些街道大约北偏东 30 度。)


图 4 Magnetometer 3D 显示

文档指出 MagnetometerReading 矢量单位是微特斯拉。 在我拥有的两个商用 Windows Phone 设备上,该矢量的值一般为 30 左右,这大致是正确的。 (图 4 中显示的矢量值的复合矢量值为 43。)不过,对于我拥有的第三部手机,MagnetometerReading 矢量已进行标准化并始终具有矢量值 1。

现在请试一试: 握住手机,以使 Magnetometer 3D 矢量几乎与图 1 中的正 Y 轴对齐。 现在绕与该矢量平行的轴旋转手机。 该矢量保持不变(或几乎不变),表明手机的磁力仪没有完全识别手机的方向。

指南针方向

通常,在我们使用指南针时,我们不希望三维矢量与地球磁场对齐。 与地球表面相切的二维矢量用处要大得多。

正如您可能知道的一样,地球磁场与地球旋转轴不重合。 地球轴方向也称为地理北或真北方向,这是地图和几乎所有其他用途所使用的正北方向。 二维表面上的角度通常用于表示正北方向。 此方向通常称为方向或方位。

在全球的不同地方,磁北和真北之间的差异也有所不同。 在纽约,必须从磁场方位中减去约 13 度才能得到真北方位,但在西雅图,必须在磁场方位中加上 21 度。 Compass 类将根据手机所在的位置执行这些计算。 除了 MagnetometerReading 矢量之外,CompassReading 还提供了两个名为 MagneticHeading 和 TrueHeading 的双精度类型属性。 这些属性是从图 1 显示的正 Y 轴按逆时针方向测量的角度(范围在 0 到 360 度之间)。

TrueHeading 应始终解释为近似值,即使这样也不应完全信任该值。 在我拥有的两部手机上,TrueHeading 通常是大致正确的,但在另一部手机上,它足足相差了 70 度。

我从来没有弄明白 MagneticHeading 值。 对于特定位置,TrueHeading 和 MagneticHeading 的差值应该是不变的。 例如,在我住的地方,TrueHeading 值减去 MagneticHeading 值应该为 -13 度左右。 在我的所有三部手机上,TrueHeading 和 MagneticHeading 的差值偶尔在两个值之间波动,具体取决于手机的方向。 有时,差值为 -12(大致正确),但大多数时候差值为 92。 这是我所见过的唯一两个差值。 在我的所有手机上,MagneticHeading 都没有与从 MagnetometerReading 矢量的 X 和 Y 值派生的角度保持一致。

在 XNA 程序中,正如您看到的一样,您可以直接在 Update 方法期间从传感器中获取当前值。 在 Silverlight 程序中使用传感器类时,您需要为 CurrentValueChanged 事件设置处理程序。 然后,您可以从事件参数中获取传感器读数对象。

本文的可下载代码包含两个 Silverlight 程序(Arrow Compass 和 Dial Compass),它们使用 TrueHeading 属性显示正北方向。 所有图像都是在 XAML 中定义的。 与 XNA 程序一样,这些 Silverlight 程序在其构造函数中创建 Compass 对象,但它们还为 CurrentValueChanged 属性设置处理程序:

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

在 Arrow Compass 中,此处理程序设置附加到箭头图像的 RotateTransform 对象上的角度:

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

将在单独线程中调用 CurrentValueChanged 处理程序,因此,您需要使用调度程序更新任何 UI 对象。 由于 TrueHeading 角度表示逆时针偏移,而 Silverlight 旋转是顺时针的,因此,此代码使用旋转方向角度的相反值。

该结果显示在图 5 中,这次手机也是指向纽约住宅区。

The Arrow Compass Display
图 5 Arrow Compass 显示

在 Dial Compass 中,在针盘旋转以指示方向时,箭头将保持固定不变,如图 6 所示。 如果要了解手机顶部指向的方向,而不是相对于手机的正北方向,您可以使用此程序版本。

The Dial Compass Display
图 6 Dial Compass 显示

如果运行这两个程序并将手机屏幕朝向地面,指南针将无法正常工作。 旋转与应有的操作相反。 如果您需要纠正此问题,在加速度矢量的 Z 值为正值时,您需要使用 TrueHeading 值的正值。

校准指南针

在右下角,Arrow Compass 程序显示 CompassReading 值的 HeadingAccuracy 属性。 从理论上讲,这提供了方向值的准确性。 实际上,我看到范围从 5% 到 30% 的 HeadingAccuracy 值。 (但我在手机上仅看到 5%,这还是有很大偏差!)

Compass 类还定义了一个名为 Calibrate 的事件,在 HeadingAccuracy 值超过 20% 时将触发该事件。

您可以执行校准操作以降低该 HeadingAccuracy: 将手机放在手里并扬起手,使屏幕指向左侧或右侧,然后在无限远模式下挥动几次您的手臂。 MSDN 教程 (bit.ly/yYrHrL) 提供的一些示例代码甚至包含一个图像,您可以在指南针需要校准时显示该图像以通知用户。

结合使用指南针和加速感应器

手机的指南针是一个完美的传感器示例,它本身显然包含一些实用程序(尤其是您在森林中迷路时),但在与其他传感器(尤其是加速感应器)结合使用时会更有价值。 总之,这两个传感器可以在三维空间中提供完整的手机方向信息。

实际上,Windows Phone 7.1 定义了一个新类以执行完全相同的操作。 除了使用三种不同的方法提供手机方向以外,Motion 类还包含来自新 Gyroscope 类的信息(如果在手机上提供的话)。

再者,Motion 类通过平滑处理加速感应器和指南针中的数据来完成额外的工作。 如果您已运行了此前介绍的程序,您可能会注意到,这些类中的数据具有较大的波动。 Motion 类可全部消除这种波动。

不过,由于我是那种喜欢应对挑战的人,我想试试“手动”合并加速感应器和指南针数据,我必须承认这种体验让我对 Motion 类更加刮目相看!

Compass 3D 程序显示四个不同颜色的指针,这些指针分布在圆盘中,分别指向北方(银色)、东方(红色)、南方(绿色)和西方(蓝色)。 该程序尝试使显示的指针平面与地球表面平行,并正确朝向指南针的四个点。

我采取的策略是获得欧拉角。 这是三个表示绕 X、Y 和 Z 轴旋转的角,它们共同描述了三维空间中的方向。 在飞行动力学方面,这三个角称为俯仰、横滚和偏航角。 从飞机的角度看,俯仰表示鼻子是朝上还是朝下以及偏离多大角度,而横滚表示向左侧或右侧的横向倾斜。 可以相对于两个轴显示这些旋转角度: 俯仰是绕通过机翼延伸的轴的旋转,而横滚基于从飞机前面到后面的轴。 偏航是绕与地球表面垂直的轴的旋转,并指示飞机的指南针方向。

要相对于图 1 中的手机坐标系显示这些角度,请想象自己像坐在魔毯上一样坐在手机上面,手机顶部朝向前方,三个按钮朝向后方。 俯仰是绕 X 轴的旋转,横滚是绕 Y 轴的旋转,而偏航是绕 Z 轴的旋转。

通过加速度矢量计算横滚和俯仰是相当容易的,这涉及使用下面的标准公式:

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

将手机屏幕面朝上平放在桌子上,横滚和俯仰均为零。 横滚范围从 -π/2 到 π/ 2,当手机面朝下时,值将恢复为零。 俯仰范围从 -π 到 π,当手机面朝上时,将具有最大值。 在手机屏幕朝上时,偏航应该与指南针中的 TrueHeading 值大致相同,但转换为弧度以用于 XNA 目的:

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

然而,正如您在两个 Silverlight 指南针程序中看到的一样,将手机屏幕朝向地球时,TrueHeading 将停止正常工作,因此,需要针对这种情况纠正偏航。 在讲了一些理论和实证方面的题外话后,我回到原来的地方并通过三个角度构造世界转换:

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

结果如图 7 所示。

The Compass 3D Program
图 7 Compass 3D 程序

我还包含了一个 Orientation 3D 程序,可用于从 Motion 类中获取这三个角度。 除了手机朝下放置时可以正常工作以外,您还可以自己看看结果有多平滑。

Motion 类是对传感器 API 的重要补充,而不单单用于此程序。 正如您将在本专栏的下一期中看到的一样,该类实际可以作为进入三维世界的门户。

Charles Petzold 是《MSDN 杂志》的长期供稿人。 他的网站是 charlespetzold.com

衷心感谢以下技术专家对本文的审阅: Drew Batchelor