问题 如何用罗盘读数和陀螺仪读数获得手机的方位角?


我希望通过以下方法获取手机的当前方向:

  1. 首先通过获取初始方向(方位角) getRotationMatrix() 和 getOrientation()
  2. 随着时间的推移添加陀螺仪读数的集成,以获得当前的方向。

电话介绍:

手机的x-y平面与地平面固定。即,处于“发短信行走”的方向。

getOrientation()“回归:

Android API允许我轻松获得方向,即方位角,俯仰,滚动,来自 getOrientation()

注意 那个方法 总是 返回其范围内的值: [0, -PI] 和 [o, PI]

我的问题:

自陀螺仪读数整合以来,表示为 dR,可能很大,所以当我这样做 CurrentOrientation += dRCurrentOrientation 可能会超过 [0, -PI] 和 [o, PI] 范围。

需要进行哪些操作才能使我始终获得当前的方向 [0, -PI] 和 [o, PI] 范围是多少?

我在Python中尝试了以下内容,但我非常怀疑它的正确性。

rotation = scipy.integrate.trapz(gyroSeries, timeSeries) # integration
if (headingDirection - rotation) < -np.pi:
    headingDirection += 2 * np.pi
elif (headingDirection - rotation) > np.pi:
    headingDirection -= 2 * np.pi
# Complementary Filter
headingDirection = ALPHA * (headingDirection - rotation) + (1 - ALPHA) * np.mean(azimuth[np.array(stepNo.tolist()) == i])
if headingDirection < -np.pi:
    headingDirection += 2 * np.pi
elif headingDirection > np.pi:
    headingDirection -= 2 * np.pi

备注

这并不是那么简单,因为它涉及以下麻烦制造者:

  1. 方向传感器读数来自 0 至 -PI, 接着 直接跳动 至 +PI 并逐渐回归 0 通过 +PI/2
  2. gyrocope阅读的整合也会带来一些麻烦。我应该加 dR 到取向或减去 dR

在给出确认的答案之前,请先参考Android Documentations。

估计的答案无济于事。


1929
2017-08-06 07:04


起源

你为什么首先使用方向然后使用陀螺仪?作为最终结果你想达到什么目的? - pxm
@pxm我首先获得INITIAL方向,然后添加集成陀螺仪读数以获得CURRENT方向。最终结果是当前的方向。 - Sibbs Gambling
你在这里使用陀螺仪的原因是什么?为什么不使用TYPE_MAGNETIC_FIELD和TYPE_GRAVITY来获得方位角? - Hoan Nguyen
@HoanNguyen我明白你的观点。但我的观点是磁场在这里严重失真。所以我决定在这里使用陀螺仪 - Sibbs Gambling
如果磁场失真,那么您的初始方向不准确,因此使用陀螺仪无助于您的后续方位角值取决于初始值。 - Hoan Nguyen


答案:


方向传感器实际上从真实磁力计和加速度计获得其读数。

我想也许这就是混乱的根源。文档中说明了哪些内容?更重要的是,某处的文档是否明确说明陀螺仪读数被忽略了?据我所知,本视频中描述的方法已实施:

Android设备上的传感器融合:运动处理的革命

该方法使用陀螺仪并整合其读数。这几乎使问题的其余部分没有实际意义;尽管如此,我会尽力回答。


方向传感器已经为您整合了陀螺仪读数,这就是你如何获得方向。我不明白你为什么这样做。

您没有正确地整合陀螺仪读数,它比复杂 CurrentOrientation += dR (这是不正确的)。如果你需要集成陀螺仪读数(我不明白为什么,SensorManager已经为你做了),请阅读 方向余弦矩阵IMU:理论 如何正确地做(公式17)。

不要尝试与欧拉角集成 (又名方位角,俯仰,滚动),没有任何好处会出来。

请在计算中使用四元数或旋转矩阵 而不是欧拉角。如果使用旋转矩阵,可以始终将它们转换为欧拉角,请参阅

由Gregory G. Slabaugh从旋转矩阵计算欧拉角

(对于四元数也是如此。)(在非退化情况下)有两种表示旋转的方法,即 你会得到两个欧拉角。选择您需要的范围内的那个。 (的情况下 万向节锁,有无限多的欧拉角,见上面的PDF)。只是承诺在旋转矩阵到欧拉角度转换后,你不会再在计算中再次使用欧拉角。

目前还不清楚你在使用互补滤波器做什么。 你可以实现一个非常好的传感器融合基于 方向余弦矩阵IMU:理论 手稿,基本上是一个教程。这样做并非易事,但我认为你不会找到比这份手稿更好,更易理解的教程。

基于这个手稿实现传感器融合时,我必须发现自己的一件事就是所谓的 积分饱和 可以发生。我通过限制来照顾它 TotalCorrection (第27页)。如果你实现这种传感器融合,你会明白我在说什么。



更新: 在这里,我回答您在接受答案后在评论中发布的问题。

我认为指南针通过使用重力和磁场给我当前的方向,对吧?陀螺仪是否用于指南针?

是的,如果手机或多或少静止至少半秒钟,您可以通过仅使用重力和指南针来获得良好的方向估计。这是怎么做的: 谁能告诉我重力传感器是否作为倾斜传感器来提高航向精度?

不,罗盘中没有使用陀螺仪。

能否请您解释为什么我所做的整合是错误的?据我所知,如果我的手机的音高指向上,则欧拉角失败。但是我的整合还有什么问题吗?

有两个不相关的东西:(i)整合应该以不同的方式完成,(ii)由于万向节锁定,欧拉角是麻烦的。我再说一遍,这两者是无关的。

至于集成:这是一个简单的例子,你可以如何实际 看到 你的集成有什么问题。设x和y为房间中水平面的轴。拿到手机。将手机围绕x轴(房间的)旋转45度,然后围绕y轴(房间的)旋转45度。然后,从头开始重复这些步骤,但现在先绕y轴旋转,然后绕x轴旋转。手机最终完全不同。如果按照说明进行集成 CurrentOrientation += dR 你会看到没有区别!如果你想正确地进行整合,请阅读上面链接的方向余弦矩阵IMU:理论手稿。

至于欧拉角:他们搞砸了应用程序的稳定性,这对我来说不足以在3D中进行任意旋转。

我仍然不明白你为什么要自己尝试,为什么你不想使用平台提供的方向估计。机会是,你不能做得更好。


8
2017-08-08 15:22



1.抱歉可能导致误导:我使用指南针而不是方向传感器。我认为指南针通过使用重力和磁场给我当前的方向,对吧?陀螺仪是否用于指南针?由于罗盘读数被金属材料严重扭曲,我希望只使用它一次并添加陀螺仪报告的方向变化。你能否解释为什么我所做的整合是错误的?据我所知,如果我的手机的音高指向上,则欧拉角失败。但是我的整合还有什么问题吗? - Sibbs Gambling
@perfectionm1ng我已经更新了答案,希望这有帮助。 - Ali
谢谢!请阅读这个: thousand-thoughts.com/2012/03/android-sensor-fusion-tutorial 我在做与此相同的事情。由于磁场失真,直接从平台获得的方向非常不准确,而陀螺仪不会受此影响。所以我希望用户定位从平台ONLY ONCE作为初始方向,然后完全依赖陀螺仪。 - Sibbs Gambling
@ perfectionm1ng我对那篇博文略作了看法。据我所知,他正在做的事情可能还不错。我没有详细检查这件事。如果您有疑问,请鼓励您与他联系(参见第3页),这也是我的建议。 - Ali
你能解释为什么我不能直接整合陀螺仪读数然后做'CurrentOrientation + = dR'吗?为什么我要转向四分之一呢?谢谢!!! - Sibbs Gambling


我认为你应该避免使用折旧的“方向传感器”,并使用传感器融合方法,如getRotationVector,getRotationMatrix,它已经实现了已经使用陀螺仪数据的Invensense专用的融合算法。

如果你想要一个简单的传感器融合算法,称为平衡滤波器 (参考 http://www.filedump.net/dumped/filter1285099462.pdf)可以使用。方法如同

http://postimg.org/image/9cu9dwn8z/

这将陀螺仪集成到角度,然后通过高通滤波器去除结果 漂移,并将其添加到平滑的加速度计和指南针结果。集成的高通滤波数据和加速度计/指南针数据以这两个部分相加的方式添加 为了使输出是一个有意义的单位的准确估计。 对于平衡滤波器,可以调整时间常数以调整响应。时间越短 恒定,响应越好,但允许通过的加速度噪声越大。

为了了解它是如何工作的,想象一下你有最新的陀螺仪数据点(以rad / s为单位)存储在陀螺仪中 加速度计的最新角度测量值存储在angle_acc中,而dt是时间 最后的陀螺仪数据到现在为止。然后你的新角度将使用

angle = b *(angle + gyro * dt)+(1 - b)*(angle_acc);

例如,您可以尝试b = 0.98。您可能还希望使用快速陀螺仪测量时间dt,以便陀螺仪在下一次测量之前不会漂移超过几度。平衡滤波器非常有用且易于实现,但不是理想的传感器融合方法。 Invensense的方法涉及一些聪明的算法,可能还有某种形式的卡尔曼滤波器。

来源:专业Android传感器编程,Adam Stroud。


2
2017-08-08 22:29



实际上,我使用的是getRotationMatrix。我没有使用方向传感器。 - Sibbs Gambling
@pxm你的第一段基本上重复我的。互补滤波器很好但是 你在这里给出了错误的例子: 平衡过滤器是 不 用于跟踪3D中的任意方向。单个角度如何确定3D中的方向?你有一个3维向量 gyro;您想如何添加3D矢量和数字?如果你转 angle 进入3D矢量,它又错了,它不是你如何在3D中集成陀螺仪读数。 - Ali


如果由于磁干扰导致方位角值不准确,那么就我所知,没有什么可以消除它的。要获得稳定的方位角读数,如果TYPE_GRAVITY不可用,则需要过滤加速度计值。如果TYPE_GRAVITY不可用,那么我很确定该设备没有陀螺仪,因此您可以使用的唯一滤波器是低通滤波器。以下代码是使用TYPE_GRAVITY和TYPE_MAGNETIC_FIELD的稳定罗盘的实现。

public class Compass  implements SensorEventListener
{
    public static final float TWENTY_FIVE_DEGREE_IN_RADIAN = 0.436332313f;
    public static final float ONE_FIFTY_FIVE_DEGREE_IN_RADIAN = 2.7052603f;

    private SensorManager mSensorManager;
    private float[] mGravity;
    private float[] mMagnetic;
    // If the device is flat mOrientation[0] = azimuth, mOrientation[1] = pitch
    // and mOrientation[2] = roll, otherwise mOrientation[0] is equal to Float.NAN
    private float[] mOrientation = new float[3];
    private LinkedList<Float> mCompassHist = new LinkedList<Float>();
    private float[] mCompassHistSum = new float[]{0.0f, 0.0f};
    private int mHistoryMaxLength;

    public Compass(Context context)
    {
         mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
         // Adjust the history length to fit your need, the faster the sensor rate
         // the larger value is needed for stable result.
         mHistoryMaxLength = 20;
    }

    public void registerListener(int sensorRate)
    {
        Sensor magneticSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
        if (magneticSensor != null)
        {
            mSensorManager.registerListener(this, magneticSensor, sensorRate);
        }
        Sensor gravitySensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
        if (gravitySensor != null)
        {
            mSensorManager.registerListener(this, gravitySensor, sensorRate);
        }
    }

    public void unregisterListener()
    {
         mSensorManager.unregisterListener(this);
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy)
    {

    }

    @Override
    public void onSensorChanged(SensorEvent event)
    {
        if (event.sensor.getType() == Sensor.TYPE_GRAVITY)
        {
            mGravity = event.values.clone();
        }
        else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD)
        {
            mMagnetic = event.values.clone();
        }
        if (!(mGravity == null || mMagnetic == null))
        {
            mOrientation = getOrientation();
        } 
    }

    private void getOrientation()
    {
        float[] rotMatrix = new float[9];
        if (SensorManager.getRotationMatrix(rotMatrix, null, 
            mGravity, mMagnetic))
        {
            float inclination = (float) Math.acos(rotMatrix[8]);
            // device is flat
            if (inclination < TWENTY_FIVE_DEGREE_IN_RADIAN 
                || inclination > ONE_FIFTY_FIVE_DEGREE_IN_RADIAN)
            {
                float[] orientation = sensorManager.getOrientation(rotMatrix, mOrientation);
                mCompassHist.add(orientation[0]);
                mOrientation[0] = averageAngle();
            }
            else
            {
                mOrientation[0] = Float.NAN;
                clearCompassHist();
            }
        }
    }

    private void clearCompassHist()
    {
        mCompassHistSum[0] = 0;
        mCompassHistSum[1] = 0;
        mCompassHist.clear();
    }

    public float averageAngle()
    {
        int totalTerms = mCompassHist.size();
        if (totalTerms > mHistoryMaxLength)
        {
            float firstTerm = mCompassHist.removeFirst();
            mCompassHistSum[0] -= Math.sin(firstTerm);
            mCompassHistSum[1] -= Math.cos(firstTerm);
            totalTerms -= 1;
        }
        float lastTerm = mCompassHist.getLast();
        mCompassHistSum[0] += Math.sin(lastTerm);
        mCompassHistSum[1] += Math.cos(lastTerm);
        float angle = (float) Math.atan2(mCompassHistSum[0] / totalTerms, mCompassHistSum[1] / totalTerms);

        return angle;
    }
}

在你的活动中实例化一个Compass对象,如onCreate,onLesume中的registerListener和onPause中的unregisterListener

private Compass mCompass;

@Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);

    mCompass = new Compass(this);
}

@Override
protected void onPause()
{
    super.onPause();

    mCompass.unregisterListener();
}

@Override
protected void onResume()
{
    super.onResume();

    mCompass.registerListener(SensorManager.SENSOR_DELAY_NORMAL);
}

2
2017-08-12 04:03



我在我的项目中的几个文件中剪切并粘贴和修改了代码。我希望不会有任何错误,否则请告诉我,我会制作一个项目并运行它来查找错误。 - Hoan Nguyen
将&&更改为|| in if(!(mGravity == null || mMagnetic == null)) - Hoan Nguyen
能否请您详细说明您的实施与现成的不同 getRotationMatrix() 和 getOrientation() 谷歌? - Sibbs Gambling
唯一的区别是,不是使用getOrientation()返回的方位角值,而是保留getOrientation()返回的方位角的历史记录,然后将方位角设置为这些值的平均值。此外,我使用倾角来确定设备何时是平坦的,因此“罗盘”方向有意义,因为getOrientation()返回的方位角是设备y轴的方向。 - Hoan Nguyen


最好让android的Orientation检测实现处理它。现在,您得到的值是从-PI到PI,您可以将它们转换为度数(0-360)。一些相关部分:

保存要处理的数据:

@Override
public void onSensorChanged(SensorEvent sensorEvent) {
    switch (sensorEvent.sensor.getType()) {
        case Sensor.TYPE_ACCELEROMETER:
            mAccValues[0] = sensorEvent.values[0];
            mAccValues[1] = sensorEvent.values[1];
            mAccValues[2] = sensorEvent.values[2];
            break;
        case Sensor.TYPE_MAGNETIC_FIELD:
            mMagValues[0] = sensorEvent.values[0];
            mMagValues[1] = sensorEvent.values[1];
            mMagValues[2] = sensorEvent.values[2];
            break;
    }

}

计算横滚,俯仰和偏航(方位角)。mR 和 mI 是arrys来保持旋转和倾斜矩阵, mO 是一个临时数组。阵列 mResults 最后有以度为单位的值:

    private void updateData() {
    SensorManager.getRotationMatrix(mR, mI, mAccValues, mMagValues);

    /**
     * arg 2: what world(according to app) axis , device's x axis aligns with
     * arg 3: what world(according to app) axis , device's y axis aligns with
     * world x = app's x = app's east
     * world y = app's y = app's north
     * device x = device's left side = device's east
     * device y = device's top side  = device's north
     */

    switch (mDispRotation) {
        case Surface.ROTATION_90:
            SensorManager.remapCoordinateSystem(mR, SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X, mR2);
            break;
        case Surface.ROTATION_270:
            SensorManager.remapCoordinateSystem(mR, SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_X, mR2);
            break;
        case Surface.ROTATION_180:
            SensorManager.remapCoordinateSystem(mR, SensorManager.AXIS_MINUS_X, SensorManager.AXIS_MINUS_Y, mR2);
            break;
        case Surface.ROTATION_0:
        default:
            mR2 = mR;
    }

    SensorManager.getOrientation(mR2, mO);


    //--upside down when abs roll > 90--
    if (Math.abs(mO[2]) > PI_BY_TWO) {
        //--fix, azimuth always to true north, even when device upside down, realistic --
        mO[0] = -mO[0];

        //--fix, roll never upside down, even when device upside down, unrealistic --
        //mO[2] = mO[2] > 0 ? PI - mO[2] : - (PI - Math.abs(mO[2]));

        //--fix, pitch comes from opposite , when device goes upside down, realistic --
        mO[1] = -mO[1];
    }

    CircleUtils.convertRadToDegrees(mO, mOut);
    CircleUtils.normalize(mOut);

    //--write--
    mResults[0] = mOut[0];
    mResults[1] = mOut[1];
    mResults[2] = mOut[2];
}

2
2017-08-15 07:41