可以补偿倾斜和俯仰的安卓指南针

24

我正在尝试在我的Android手机(Nexus 4)上创建一个应用程序,该应用程序将用于模型船。我添加了低通滤波器以过滤传感器的杂波。

然而,只有当手机平放时,指南针才是稳定的。如果我将它倾斜(例如翻书页),那么指南针的方向就会偏离太多,多达50度。

我已经使用Sensor.TYPE_MAGNETIC_FIELD,并结合Sensor.TYPE_GRAVITY和Sensor.TYPE_ACCELEROMETER进行了尝试,但效果都一样。

我尝试了这里和其他很多地方提到的解决方法。我的数学不是很好,但这肯定是一个常见的问题,让我感到沮丧的是竟然没有一个API来处理它。

我已经为此问题工作了3天,仍然没有找到任何解决方案。但是,当我使用Catch的指南针时,无论手机倾斜多少,他们的指南针都保持稳定。所以我知道这一定是可能的。

我想做的就是创建一个指南针,如果手机指向北,则指南针将读取北方,当手机在任何其他轴上移动(滚动或俯仰)时不会跳动。

在我不得不放弃我的项目之前,有人能帮我吗?

谢谢, Adam


当你向上倾斜时,你旋转了手机吗? - Hoan Nguyen
6个回答

39

这个问题我已经思考了几周,因为:

  1. 作为一名数学家,我并不满意我在其他地方看到的任何答案;
  2. 我需要一个好的答案,用于我正在开发的一个应用程序。
因此,在过去的几天里,我想出了自己的方法来计算方位角值,以在指南针中使用。

我在math.stackexchange.com上放置了我正在使用的数学方法,并在下面粘贴了代码。该代码从原始TYPE_GRAVITYTYPE_MAGNETIC_FIELD传感器数据中计算方位角和俯仰角,而不需要通过API调用SensorManager.getRotationMatrix(...)SensorManager.getOrientation(...)等方法来实现。如果输入有点不稳定,代码可能可以通过使用低通滤波器等方式进行改进。请注意,代码通过方法onAccuracyChanged(Sensor sensor,int accuracy)记录传感器的准确性,因此,如果方位角似乎不稳定,则要检查每个传感器的准确性。无论如何,由于所有的计算在这段代码中都是明确可见的,如果存在不稳定性问题(当传感器准确度合理时),那么可以通过查看输入或方向向量m_NormGravityVector[]m_NormEastVector[]m_NormNorthVector[]中的不稳定性来解决这些问题。

我非常希望得到任何人对这种方法的反馈。我发现,在我的应用程序中,只要设备是正面朝上、垂直或介于两者之间,它就像梦一样运行。然而,正如我在math.stackexchange.com文章中提到的那样,随着设备接近被颠倒的状态,会出现问题。在那种情况下,需要仔细定义所需的行为。

    import android.app.Activity;
    import android.hardware.Sensor;
    import android.hardware.SensorEvent;
    import android.hardware.SensorEventListener;
    import android.hardware.SensorManager;
    import android.view.Surface;

    public static class OrientationSensor implements  SensorEventListener {

    public final static int SENSOR_UNAVAILABLE = -1;

    // references to other objects
    SensorManager m_sm;
    SensorEventListener m_parent;   // non-null if this class should call its parent after onSensorChanged(...) and onAccuracyChanged(...) notifications
    Activity m_activity;            // current activity for call to getWindowManager().getDefaultDisplay().getRotation()

    // raw inputs from Android sensors
    float m_Norm_Gravity;           // length of raw gravity vector received in onSensorChanged(...).  NB: should be about 10
    float[] m_NormGravityVector;    // Normalised gravity vector, (i.e. length of this vector is 1), which points straight up into space
    float m_Norm_MagField;          // length of raw magnetic field vector received in onSensorChanged(...). 
    float[] m_NormMagFieldValues;   // Normalised magnetic field vector, (i.e. length of this vector is 1)

    // accuracy specifications. SENSOR_UNAVAILABLE if unknown, otherwise SensorManager.SENSOR_STATUS_UNRELIABLE, SENSOR_STATUS_ACCURACY_LOW, SENSOR_STATUS_ACCURACY_MEDIUM or SENSOR_STATUS_ACCURACY_HIGH
    int m_GravityAccuracy;          // accuracy of gravity sensor
    int m_MagneticFieldAccuracy;    // accuracy of magnetic field sensor

    // values calculated once gravity and magnetic field vectors are available
    float[] m_NormEastVector;       // normalised cross product of raw gravity vector with magnetic field values, points east
    float[] m_NormNorthVector;      // Normalised vector pointing to magnetic north
    boolean m_OrientationOK;        // set true if m_azimuth_radians and m_pitch_radians have successfully been calculated following a call to onSensorChanged(...)
    float m_azimuth_radians;        // angle of the device from magnetic north
    float m_pitch_radians;          // tilt angle of the device from the horizontal.  m_pitch_radians = 0 if the device if flat, m_pitch_radians = Math.PI/2 means the device is upright.
    float m_pitch_axis_radians;     // angle which defines the axis for the rotation m_pitch_radians

    public OrientationSensor(SensorManager sm, SensorEventListener parent) {
        m_sm = sm;
        m_parent = parent;
        m_activity = null;
        m_NormGravityVector = m_NormMagFieldValues = null;
        m_NormEastVector = new float[3];
        m_NormNorthVector = new float[3];
        m_OrientationOK = false;
    }

    public int Register(Activity activity, int sensorSpeed) {
        m_activity = activity;  // current activity required for call to getWindowManager().getDefaultDisplay().getRotation()
        m_NormGravityVector = new float[3];
        m_NormMagFieldValues = new float[3];
        m_OrientationOK = false;
        int count = 0;
        Sensor SensorGravity = m_sm.getDefaultSensor(Sensor.TYPE_GRAVITY);
        if (SensorGravity != null) {
            m_sm.registerListener(this, SensorGravity, sensorSpeed);
            m_GravityAccuracy = SensorManager.SENSOR_STATUS_ACCURACY_HIGH;
            count++;
        } else {
            m_GravityAccuracy = SENSOR_UNAVAILABLE;
        }
        Sensor SensorMagField = m_sm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
        if (SensorMagField != null) {
            m_sm.registerListener(this, SensorMagField, sensorSpeed);
            m_MagneticFieldAccuracy = SensorManager.SENSOR_STATUS_ACCURACY_HIGH;     
            count++;
        } else {
            m_MagneticFieldAccuracy = SENSOR_UNAVAILABLE;
        }
        return count;
    }

    public void Unregister() {
        m_activity = null;
        m_NormGravityVector = m_NormMagFieldValues = null;
        m_OrientationOK = false;
        m_sm.unregisterListener(this);
    }

    @Override
    public void onSensorChanged(SensorEvent evnt) {
        int SensorType = evnt.sensor.getType();
        switch(SensorType) {
            case Sensor.TYPE_GRAVITY:
                if (m_NormGravityVector == null) m_NormGravityVector = new float[3];
                System.arraycopy(evnt.values, 0, m_NormGravityVector, 0, m_NormGravityVector.length);                   
                m_Norm_Gravity = (float)Math.sqrt(m_NormGravityVector[0]*m_NormGravityVector[0] + m_NormGravityVector[1]*m_NormGravityVector[1] + m_NormGravityVector[2]*m_NormGravityVector[2]);
                for(int i=0; i < m_NormGravityVector.length; i++) m_NormGravityVector[i] /= m_Norm_Gravity;
                break;
            case Sensor.TYPE_MAGNETIC_FIELD:
                if (m_NormMagFieldValues == null) m_NormMagFieldValues = new float[3];
                System.arraycopy(evnt.values, 0, m_NormMagFieldValues, 0, m_NormMagFieldValues.length);
                m_Norm_MagField = (float)Math.sqrt(m_NormMagFieldValues[0]*m_NormMagFieldValues[0] + m_NormMagFieldValues[1]*m_NormMagFieldValues[1] + m_NormMagFieldValues[2]*m_NormMagFieldValues[2]);
                for(int i=0; i < m_NormMagFieldValues.length; i++) m_NormMagFieldValues[i] /= m_Norm_MagField;  
                break;
        }
        if (m_NormGravityVector != null && m_NormMagFieldValues != null) {
            // first calculate the horizontal vector that points due east
            float East_x = m_NormMagFieldValues[1]*m_NormGravityVector[2] - m_NormMagFieldValues[2]*m_NormGravityVector[1];
            float East_y = m_NormMagFieldValues[2]*m_NormGravityVector[0] - m_NormMagFieldValues[0]*m_NormGravityVector[2];
            float East_z = m_NormMagFieldValues[0]*m_NormGravityVector[1] - m_NormMagFieldValues[1]*m_NormGravityVector[0];
            float norm_East = (float)Math.sqrt(East_x * East_x + East_y * East_y + East_z * East_z);
            if (m_Norm_Gravity * m_Norm_MagField * norm_East < 0.1f) {  // Typical values are  > 100.
                m_OrientationOK = false; // device is close to free fall (or in space?), or close to magnetic north pole.
            } else {
                m_NormEastVector[0] = East_x / norm_East; m_NormEastVector[1] = East_y / norm_East; m_NormEastVector[2] = East_z / norm_East;

                // next calculate the horizontal vector that points due north                   
                float M_dot_G = (m_NormGravityVector[0] *m_NormMagFieldValues[0] + m_NormGravityVector[1]*m_NormMagFieldValues[1] + m_NormGravityVector[2]*m_NormMagFieldValues[2]);
                float North_x = m_NormMagFieldValues[0] - m_NormGravityVector[0] * M_dot_G;
                float North_y = m_NormMagFieldValues[1] - m_NormGravityVector[1] * M_dot_G;
                float North_z = m_NormMagFieldValues[2] - m_NormGravityVector[2] * M_dot_G;
                float norm_North = (float)Math.sqrt(North_x * North_x + North_y * North_y + North_z * North_z);
                m_NormNorthVector[0] = North_x / norm_North; m_NormNorthVector[1] = North_y / norm_North; m_NormNorthVector[2] = North_z / norm_North;

                // take account of screen rotation away from its natural rotation
                int rotation = m_activity.getWindowManager().getDefaultDisplay().getRotation();
                float screen_adjustment = 0;
                switch(rotation) {
                    case Surface.ROTATION_0:   screen_adjustment =          0;         break;
                    case Surface.ROTATION_90:  screen_adjustment =   (float)Math.PI/2; break;
                    case Surface.ROTATION_180: screen_adjustment =   (float)Math.PI;   break;
                    case Surface.ROTATION_270: screen_adjustment = 3*(float)Math.PI/2; break;
                }
                // NB: the rotation matrix has now effectively been calculated. It consists of the three vectors m_NormEastVector[], m_NormNorthVector[] and m_NormGravityVector[]

                // calculate all the required angles from the rotation matrix
                // NB: see https://math.stackexchange.com/questions/381649/whats-the-best-3d-angular-co-ordinate-system-for-working-with-smartfone-apps
                float sin = m_NormEastVector[1] -  m_NormNorthVector[0], cos = m_NormEastVector[0] +  m_NormNorthVector[1];
                m_azimuth_radians = (float) (sin != 0 && cos != 0 ? Math.atan2(sin, cos) : 0);
                m_pitch_radians = (float) Math.acos(m_NormGravityVector[2]);
                sin = -m_NormEastVector[1] -  m_NormNorthVector[0]; cos = m_NormEastVector[0] -  m_NormNorthVector[1];
                float aximuth_plus_two_pitch_axis_radians = (float)(sin != 0 && cos != 0 ? Math.atan2(sin, cos) : 0);
                m_pitch_axis_radians = (float)(aximuth_plus_two_pitch_axis_radians - m_azimuth_radians) / 2;
                m_azimuth_radians += screen_adjustment;
                m_pitch_axis_radians += screen_adjustment;
                m_OrientationOK = true;                                 
            }
        }
        if (m_parent != null) m_parent.onSensorChanged(evnt);
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        int SensorType = sensor.getType();
        switch(SensorType) {
            case Sensor.TYPE_GRAVITY: m_GravityAccuracy = accuracy; break;
            case Sensor.TYPE_MAGNETIC_FIELD: m_MagneticFieldAccuracy = accuracy; break;
        }
        if (m_parent != null) m_parent.onAccuracyChanged(sensor, accuracy);
    }
}

我使用了这种方法,不得不说它相当稳定,但是当我滚动或倾斜设备时,方位角会受到约20度的影响,而且不取决于滚动或倾斜角度。 - Pavan K
@PavanK 不确定您所说的“方位角受到影响”是什么意思。 我的经验是,在存在可能会产生磁场的设备的情况下,磁场传感器不一定可靠到20度以内。 - Stochastically
我的意思是,当我倾斜或改变设备的俯仰角时,指南针会出现超过或低于20度的误差。我知道外部设备的存在可能会影响数值。 - Pavan K
1
TYPE_GRAVITY并非所有设备都可用,GRAVITY是由ACCELEROMETER派生而来的,因此可以尝试使用TYPE_ACCELEROMETER替代GRAVITY来实现您的源代码。https://github.com/android/platform_frameworks_base/tree/ics-mr1/services/sensorservice - GMG
1
@Stochastically,你好,我同意你的观点,但由于某些原因,一款三年前搭载Android 4.1的Galaxy Tab 2没有GRAVITY,所以我不知道每个制造商是否有自由实现或不实现Android传感器的某些部分(也包括软件模拟)。你的工作很完美,我的观察只是为了兼容性。 - GMG
显示剩余7条评论

16

好的,我想我解决了。

我使用Sensor.TYPE_ROTATION_VECTOR而不是Sensor.TYPE_ACCELEROMETER(或TYPE_GRAVITY)和Sensor.TYPE_MAGNETIC_FIELD,并且使用以下内容:

float[] roationV = new float[16];
SensorManager.getRotationMatrixFromVector(roationV, rotationVector);

float[] orientationValuesV = new float[3];
SensorManager.getOrientation(roationV, orientationValuesV);

无论手机的倾斜或俯仰如何,都会返回一个稳定的方位角。

如果您在Android运动传感器中查看,在表格1下方,它说旋转传感器非常适合指南针、增强现实等应用。

知道方法后就很简单... 然而,我还没有测试其是否会引入误差。


4
请注意,我认为您会发现,只有当您使用的设备具有陀螺仪时,TYPE_ROTATION_VECTOR 才可用。 - Stochastically
在我的Nexus S手机上,TYPE_ROTATION_VECTOR传感器不能提供稳定的方位角度数值,在俯仰/翻滚变化时波动较大。而且,在我的三星Galaxy Tab 2平板电脑上,由于某种原因,此传感器不可用。 - WindRider
您可以使用SensorManager.getRotationMatrix()和SensorManager.getOrientation()方法。在我尝试的每个设备上,这种方法都提供了倾斜补偿,并且似乎与我尝试过的许多其他算法相当。请记住,Sensor.TYPE_ACCELEROMETER将是所有设备上唯一存在的方法... Sensor.TYPE_GRAVITY、TYPE_LINEAR_ACCELERATION和TYPE_ROTATION_VECTOR则有时会出现问题。 - Kaleb
对于我的AR应用程序来说,这似乎很棒...方位角值足够稳定。 - ashif-ismail

2

这是一种获取磁北方向的另一种方法,不受俯仰或滚转的影响。

private final static double PI = Math.PI;
private final static double TWO_PI = PI*2;

 case Sensor.TYPE_ROTATION_VECTOR:
                float[] orientation = new float[3];
                float[] rotationMatrix = new float[9];

                SensorManager.getRotationMatrixFromVector(rotationMatrix, rawValues);
                SensorManager.getOrientation(rotationMatrix, orientation);

                float heading = mod(orientation[0] + TWO_PI,TWO_PI);//important
                //do something with the heading
                break;



private double mod(double a, double b){
        return a % b;
    }

2
你遇到的问题可能是万向节锁定。如果你想一下,当手机直立时,俯仰角为正或负90度,方位和横滚角就是同一件事。如果你仔细研究一下数学,你会发现在这种情况下,方位角+横滚角或方位角-横滚角是有明确定义的,但它们不能单独定义。所以当俯仰角接近正负90度时,读数变得不稳定。有些人选择重新映射坐标系来解决这个问题,例如我的Android设备不平放时,我应该如何计算方位角、俯仰角和方向?,也许这对你有用。

这并不是万向锁问题。即使您将手机倾斜几度,方位角的值也会更改。其中一个行为是,如果您缓慢地倾斜,则方位角不会改变。但是,如果您快速倾斜它,则会发生改变。 - Adam Davies
我研究了使用SensorFusion(http://www.thousand-thoughts.com/2012/03/android-sensor-fusion-tutorial/),但即使是这个指南针在你倾斜手机时也会改变。 - Adam Davies
@AdamDavies,你在其他设备上测试过你的应用程序吗?我问这个问题是因为我认为我的两个设备(Nexus 10和Galaxy Note2手机)在你将滚动保持为零并将俯仰调整到正负45度时不会改变方位角。但现在我要睡觉了,明天再仔细检查一下。 - Stochastically

1
看一下 Mixare,这是一个开源的增强现实工具,适用于安卓和iPhone系统,里面有一些很好的东西可以通过调整手机的方向/位置来正确地显示屏幕上的内容。
编辑:特别是要看一下MixView java class,它处理传感器事件。

-2

我发现在某些智能手机型号上,启动相机可能会改变指南针数据... 1/10度...(与场景的光线有关)

黑暗场景... 1/2 .... 非常白的场景(10度或更多)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接