使用Android陀螺仪代替加速度计。我找到了很多零散的代码,但没有完整的代码。

26
传感器融合视频效果很棒,但没有代码:http://www.youtube.com/watch?v=C7JQ7Rpwn2k&feature=player_detailpage#t=1315s
这是我的代码,只使用加速度计和指南针。我还在三个方向上使用了卡尔曼滤波器,但这里展示太多代码了。最终,这个方法可以工作,但结果要么太抖动,要么太延迟,具体取决于如何处理结果以及过滤因子设置的低高。
/** Just accelerometer and magnetic sensors */
public abstract class SensorsListener2
    implements
        SensorEventListener
{
    /** The lower this is, the greater the preference which is given to previous values. (slows change) */
    private static final float accelFilteringFactor = 0.1f;
    private static final float magFilteringFactor = 0.01f;

    public abstract boolean getIsLandscape();

    @Override
    public void onSensorChanged(SensorEvent event) {
        Sensor sensor = event.sensor;
        int type = sensor.getType();

        switch (type) {
            case Sensor.TYPE_MAGNETIC_FIELD:
                mags[0] = event.values[0] * magFilteringFactor + mags[0] * (1.0f - magFilteringFactor);
                mags[1] = event.values[1] * magFilteringFactor + mags[1] * (1.0f - magFilteringFactor);
                mags[2] = event.values[2] * magFilteringFactor + mags[2] * (1.0f - magFilteringFactor);

                isReady = true;
                break;
            case Sensor.TYPE_ACCELEROMETER:
                accels[0] = event.values[0] * accelFilteringFactor + accels[0] * (1.0f - accelFilteringFactor);
                accels[1] = event.values[1] * accelFilteringFactor + accels[1] * (1.0f - accelFilteringFactor);
                accels[2] = event.values[2] * accelFilteringFactor + accels[2] * (1.0f - accelFilteringFactor);
                break;

            default:
                return;
        }




        if(mags != null && accels != null && isReady) {
            isReady = false;

            SensorManager.getRotationMatrix(rot, inclination, accels, mags);

            boolean isLandscape = getIsLandscape();
            if(isLandscape) {
                outR = rot;
            } else {
                // Remap the coordinates to work in portrait mode.
                SensorManager.remapCoordinateSystem(rot, SensorManager.AXIS_X, SensorManager.AXIS_Z, outR);
            }

            SensorManager.getOrientation(outR, values);

            double x180pi = 180.0 / Math.PI;
            float azimuth = (float)(values[0] * x180pi);
            float pitch = (float)(values[1] * x180pi);
            float roll = (float)(values[2] * x180pi);

            // In landscape mode swap pitch and roll and invert the pitch.
            if(isLandscape) {
                float tmp = pitch;
                pitch = -roll;
                roll = -tmp;
                azimuth = 180 - azimuth;
            } else {
                pitch = -pitch - 90;
                azimuth = 90 - azimuth;
            }

            onOrientationChanged(azimuth,pitch,roll);
        }
    }




    private float[] mags = new float[3];
    private float[] accels = new float[3];
    private boolean isReady;

    private float[] rot = new float[9];
    private float[] outR = new float[9];
    private float[] inclination = new float[9];
    private float[] values = new float[3];



    /**
    Azimuth: angle between the magnetic north direction and the Y axis, around the Z axis (0 to 359). 0=North, 90=East, 180=South, 270=West
    Pitch: rotation around X axis (-180 to 180), with positive values when the z-axis moves toward the y-axis.
    Roll: rotation around Y axis (-90 to 90), with positive values when the x-axis moves toward the z-axis.
    */
    public abstract void onOrientationChanged(float azimuth, float pitch, float roll);
}

我试图弄清楚如何添加陀螺仪数据,但我做得并不正确。Google文档http://developer.android.com/reference/android/hardware/SensorEvent.html上显示了一些代码,用于从陀螺仪数据中获取一个增量矩阵。想法似乎是我会将加速度计和磁传感器的滤波器降至极低水平以保持长期方向跟踪。

然后,我将保留最近N个增量矩阵的历史记录。每次我获得新的增量矩阵时,我将删除最旧的增量矩阵并将它们全部相乘,以获取最终矩阵,然后将其与由加速度计和磁传感器返回的稳定矩阵相乘。

这似乎行不通。或者,至少我的实现有问题。结果比只使用加速度计的要抖动得多。增加陀螺仪历史记录的大小实际上会增加抖动,这让我认为我没有从陀螺仪中计算出正确的值。

public abstract class SensorsListener3
    implements
        SensorEventListener
{
    /** The lower this is, the greater the preference which is given to previous values. (slows change) */
    private static final float kFilteringFactor = 0.001f;
    private static final float magKFilteringFactor = 0.001f;


    public abstract boolean getIsLandscape();

    @Override
    public void onSensorChanged(SensorEvent event) {
        Sensor sensor = event.sensor;
        int type = sensor.getType();

        switch (type) {
            case Sensor.TYPE_MAGNETIC_FIELD:
                mags[0] = event.values[0] * magKFilteringFactor + mags[0] * (1.0f - magKFilteringFactor);
                mags[1] = event.values[1] * magKFilteringFactor + mags[1] * (1.0f - magKFilteringFactor);
                mags[2] = event.values[2] * magKFilteringFactor + mags[2] * (1.0f - magKFilteringFactor);

                isReady = true;
                break;
            case Sensor.TYPE_ACCELEROMETER:
                accels[0] = event.values[0] * kFilteringFactor + accels[0] * (1.0f - kFilteringFactor);
                accels[1] = event.values[1] * kFilteringFactor + accels[1] * (1.0f - kFilteringFactor);
                accels[2] = event.values[2] * kFilteringFactor + accels[2] * (1.0f - kFilteringFactor);
                break;

            case Sensor.TYPE_GYROSCOPE:
                gyroscopeSensorChanged(event);
                break;

            default:
                return;
        }




        if(mags != null && accels != null && isReady) {
            isReady = false;

            SensorManager.getRotationMatrix(rot, inclination, accels, mags);

            boolean isLandscape = getIsLandscape();
            if(isLandscape) {
                outR = rot;
            } else {
                // Remap the coordinates to work in portrait mode.
                SensorManager.remapCoordinateSystem(rot, SensorManager.AXIS_X, SensorManager.AXIS_Z, outR);
            }

            if(gyroUpdateTime!=0) {
                matrixHistory.mult(matrixTmp,matrixResult);
                outR = matrixResult;
            }

            SensorManager.getOrientation(outR, values);

            double x180pi = 180.0 / Math.PI;
            float azimuth = (float)(values[0] * x180pi);
            float pitch = (float)(values[1] * x180pi);
            float roll = (float)(values[2] * x180pi);

            // In landscape mode swap pitch and roll and invert the pitch.
            if(isLandscape) {
                float tmp = pitch;
                pitch = -roll;
                roll = -tmp;
                azimuth = 180 - azimuth;
            } else {
                pitch = -pitch - 90;
                azimuth = 90 - azimuth;
            }

            onOrientationChanged(azimuth,pitch,roll);
        }
    }



    private void gyroscopeSensorChanged(SensorEvent event) {
        // This timestep's delta rotation to be multiplied by the current rotation
        // after computing it from the gyro sample data.
        if(gyroUpdateTime != 0) {
            final float dT = (event.timestamp - gyroUpdateTime) * NS2S;
            // Axis of the rotation sample, not normalized yet.
            float axisX = event.values[0];
            float axisY = event.values[1];
            float axisZ = event.values[2];

            // Calculate the angular speed of the sample
            float omegaMagnitude = (float)Math.sqrt(axisX*axisX + axisY*axisY + axisZ*axisZ);

            // Normalize the rotation vector if it's big enough to get the axis
            if(omegaMagnitude > EPSILON) {
                axisX /= omegaMagnitude;
                axisY /= omegaMagnitude;
                axisZ /= omegaMagnitude;
            }

            // Integrate around this axis with the angular speed by the timestep
            // in order to get a delta rotation from this sample over the timestep
            // We will convert this axis-angle representation of the delta rotation
            // into a quaternion before turning it into the rotation matrix.
            float thetaOverTwo = omegaMagnitude * dT / 2.0f;
            float sinThetaOverTwo = (float)Math.sin(thetaOverTwo);
            float cosThetaOverTwo = (float)Math.cos(thetaOverTwo);
            deltaRotationVector[0] = sinThetaOverTwo * axisX;
            deltaRotationVector[1] = sinThetaOverTwo * axisY;
            deltaRotationVector[2] = sinThetaOverTwo * axisZ;
            deltaRotationVector[3] = cosThetaOverTwo;
        }
        gyroUpdateTime = event.timestamp;
        SensorManager.getRotationMatrixFromVector(deltaRotationMatrix, deltaRotationVector);
        // User code should concatenate the delta rotation we computed with the current rotation
        // in order to get the updated rotation.
        // rotationCurrent = rotationCurrent * deltaRotationMatrix;
        matrixHistory.add(deltaRotationMatrix);
    }



    private float[] mags = new float[3];
    private float[] accels = new float[3];
    private boolean isReady;

    private float[] rot = new float[9];
    private float[] outR = new float[9];
    private float[] inclination = new float[9];
    private float[] values = new float[3];

    // gyroscope stuff
    private long gyroUpdateTime = 0;
    private static final float NS2S = 1.0f / 1000000000.0f;
    private float[] deltaRotationMatrix = new float[9];
    private final float[] deltaRotationVector = new float[4];
//TODO: I have no idea how small this value should be.
    private static final float EPSILON = 0.000001f;
    private float[] matrixMult = new float[9];
    private MatrixHistory matrixHistory = new MatrixHistory(100);
    private float[] matrixTmp = new float[9];
    private float[] matrixResult = new float[9];


    /**
    Azimuth: angle between the magnetic north direction and the Y axis, around the Z axis (0 to 359). 0=North, 90=East, 180=South, 270=West 
    Pitch: rotation around X axis (-180 to 180), with positive values when the z-axis moves toward the y-axis. 
    Roll: rotation around Y axis (-90 to 90), with positive values when the x-axis moves toward the z-axis.
    */
    public abstract void onOrientationChanged(float azimuth, float pitch, float roll);
}


public class MatrixHistory
{
    public MatrixHistory(int size) {
        vals = new float[size][];
    }

    public void add(float[] val) {
        synchronized(vals) {
            vals[ix] = val;
            ix = (ix + 1) % vals.length;
            if(ix==0)
                full = true;
        }
    }

    public void mult(float[] tmp, float[] output) {
        synchronized(vals) {
            if(full) {
                for(int i=0; i<vals.length; ++i) {
                    if(i==0) {
                        System.arraycopy(vals[i],0,output,0,vals[i].length);
                    } else {
                        MathUtils.multiplyMatrix3x3(output,vals[i],tmp);
                        System.arraycopy(tmp,0,output,0,tmp.length);
                    }
                }
            } else {
                if(ix==0)
                    return;
                for(int i=0; i<ix; ++i) {
                    if(i==0) {
                        System.arraycopy(vals[i],0,output,0,vals[i].length);
                    } else {
                        MathUtils.multiplyMatrix3x3(output,vals[i],tmp);
                        System.arraycopy(tmp,0,output,0,tmp.length);
                    }
                }
            }
        }
    }


    private int ix = 0;
    private boolean full = false;
    private float[][] vals;
}

第二段代码包含了我对第一段代码所做的更改,其中加入了陀螺仪。

具体而言,加速度计的滤波因子变小了(使值更加稳定)。MatrixHistory类跟踪最后100个陀螺仪deltaRotationMatrix值,这些值是在gyroscopeSensorChanged方法中计算的。

我在这个网站上看到了很多关于这个主题的问题。它们帮助我达到了这个程度,但我不知道接下来该怎么做。我真希望传感器融合的人能够在某个地方发布一些代码。他显然已经把所有东西都准备好了。


2
根据《专业Android传感器编程》一书,InvenSense的传感器融合算法是专有的,因此很难在公共访问中找到源代码。该库已经包含在大多数现代设备的系统级别中,因此SENSOR.TYPE_ROTATION已经提供了基于陀螺仪的短时校正的测量值。我认为这方面最详细的公共资源是这个。我不确定它是否是一个好的替代品。 - Stan
还有一些与使用卡尔曼滤波器进行传感器融合相关的学术论文。它们通常不包含源代码,但应该具有您所需的技术和数学细节。scholar.google.com - zapl
你为什么要对磁值进行低通滤波? - Hoan Nguyen
2个回答

53
恭喜你知道Kalman滤波是什么。如果需要,我可以编辑这篇文章并给出我几年前编写的代码来实现你想要实现的内容。
但是首先,我会告诉你为什么你不需要它。
如Stan所提到的,Android传感器堆栈的现代实现使用了"Sensor Fusion"。这只是意味着所有可用数据——加速度、磁力计、陀螺仪——都被汇集在一个算法中,然后所有输出以Android传感器的形式读回。
编辑:我刚意外发现了这个绝妙的Google技术讲座:Sensor Fusion on Android Devices: A Revolution in Motion Processing。如果你对这个主题感兴趣,那么花45分钟观看它绝对是值得的。
从本质上讲,Sensor Fusion是一个黑盒子。我查看了Android实现的源代码,它是一个大型的C++ Kalman滤波器。那里有一些相当不错的代码,比我写过的任何滤波器都要复杂得多,可能比你正在写的还要复杂。请记住,这些人靠这个谋生。
我也知道至少有一个芯片制造商拥有自己的传感器融合实现。然后设备制造商根据自己的标准在Android和供应商实现之间进行选择。
最后,就像Stan所提到的那样,Invensense在芯片级别上有自己的传感器融合实现。
总之,所有问题都归结为内置传感器融合在你的设备中可能比我们能够拼凑的任何东西都要好。所以你真正想做的是访问它。
在Android中,有物理和虚拟传感器。虚拟传感器是从可用的物理传感器中合成的。最著名的例子是TYPE_ORIENTATION,它采用加速度计和磁力计,并创建出横滚/俯仰/航向输出。(顺便说一句,你不应该使用这个传感器,因为它有太多限制。)
但是重要的是,新版本的Android包含这两个新的虚拟传感器:
TYPE_GRAVITY是经过运动过滤处理的加速度计输入 TYPE_LINEAR_ACCELERATION是已经过滤掉重力分量的加速度计。
这两个虚拟传感器是通过加速度计输入和陀螺仪输入组合合成的。
另一个值得注意的传感器是TYPE_ROTATION_VECTOR,它是由加速度计、磁力计和陀螺仪合成的四元数。它代表了设备的完整三维方向,已经过滤掉了线性加速度的影响。
然而,对于大多数人来说,四元数有点抽象,由于你可能已经在处理3D变换,因此最好的方法是通过SensorManager.getRotationMatrix()将TYPE_GRAVITY和TYPE_MAGNETIC_FIELD组合起来。

还有一点需要注意:如果你在使用运行较旧版本 Android 的设备时,需要检测无法接收 TYPE_GRAVITY 事件,此时应该使用 TYPE_ACCELEROMETER。理论上说,这是可以使用自己的卡尔曼滤波器的地方,但如果你的设备没有传感器融合功能,则很可能也没有陀螺仪。

无论如何,以下是一些示例代码,展示我是如何做到的。

  // Requires 1.5 or above

  class Foo extends Activity implements SensorEventListener {

    SensorManager sensorManager;
    float[] gData = new float[3];           // Gravity or accelerometer
    float[] mData = new float[3];           // Magnetometer
    float[] orientation = new float[3];
    float[] Rmat = new float[9];
    float[] R2 = new float[9];
    float[] Imat = new float[9];
    boolean haveGrav = false;
    boolean haveAccel = false;
    boolean haveMag = false;

    onCreate() {
        // Get the sensor manager from system services
        sensorManager =
          (SensorManager)getSystemService(Context.SENSOR_SERVICE);
    }

    onResume() {
        super.onResume();
        // Register our listeners
        Sensor gsensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
        Sensor asensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        Sensor msensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
        sensorManager.registerListener(this, gsensor, SensorManager.SENSOR_DELAY_GAME);
        sensorManager.registerListener(this, asensor, SensorManager.SENSOR_DELAY_GAME);
        sensorManager.registerListener(this, msensor, SensorManager.SENSOR_DELAY_GAME);
    }

    public void onSensorChanged(SensorEvent event) {
        float[] data;
        switch( event.sensor.getType() ) {
          case Sensor.TYPE_GRAVITY:
            gData[0] = event.values[0];
            gData[1] = event.values[1];
            gData[2] = event.values[2];
            haveGrav = true;
            break;
          case Sensor.TYPE_ACCELEROMETER:
            if (haveGrav) break;    // don't need it, we have better
            gData[0] = event.values[0];
            gData[1] = event.values[1];
            gData[2] = event.values[2];
            haveAccel = true;
            break;
          case Sensor.TYPE_MAGNETIC_FIELD:
            mData[0] = event.values[0];
            mData[1] = event.values[1];
            mData[2] = event.values[2];
            haveMag = true;
            break;
          default:
            return;
        }

        if ((haveGrav || haveAccel) && haveMag) {
            SensorManager.getRotationMatrix(Rmat, Imat, gData, mData);
            SensorManager.remapCoordinateSystem(Rmat,
                    SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X, R2);
            // Orientation isn't as useful as a rotation matrix, but
            // we'll show it here anyway.
            SensorManager.getOrientation(R2, orientation);
            float incl = SensorManager.getInclination(Imat);
            Log.d(TAG, "mh: " + (int)(orientation[0]*DEG));
            Log.d(TAG, "pitch: " + (int)(orientation[1]*DEG));
            Log.d(TAG, "roll: " + (int)(orientation[2]*DEG));
            Log.d(TAG, "yaw: " + (int)(orientation[0]*DEG));
            Log.d(TAG, "inclination: " + (int)(incl*DEG));
        }
      }
    }

如果你恰好有一个四元数库,那么接收TYPE_ROTATION_VECTOR并将其转换为数组可能更简单。


你最终解决了这个问题吗?我的仍然很卡顿,我使用了低通滤波器。 - Burf2000
我会先编写您的应用程序,不进行过滤,然后查看结果是否足够稳定。如果不是,请尝试应用一些过滤器。GRAVITY可能不需要过滤,但MAGNETIC_FIELD可能需要。您还应该考虑使用ROTATION并将四元数转换为矩阵。ROTATION几乎可以为您完成所有操作。转换为矩阵几乎是微不足道的。 - Edward Falk
感谢您的回复。因此,我编写了代码来结合重力和磁场进行方向估计,并且在过滤磁场时输出更加稳定。我假设从旋转中获得的四元数是设备方向。那么,在这种情况下,我不需要获取方向,对吗?谢谢。 - chintan s
你好 Edward,能否请你看一下我遇到的这个问题。谢谢。http://stackoverflow.com/questions/30529272/android-magnetometer-data-deviates - chintan s
1
这里有一个传感器融合库(https://github.com/majidgolshadi/Android-Orientation-Sensor),可能会引起您的兴趣。 - gaborous
显示剩余9条评论

5
要找完整代码的问题,这里有一个在Android jelly bean上的默认实现: https://android.googlesource.com/platform/frameworks/base/+/jb-release/services/sensorservice/。从fusion.cpp/h开始检查。它使用Modified Rodrigues参数(接近欧拉角)而不是四元数。除了方向之外,卡尔曼滤波器还估计陀螺仪漂移。对于测量更新,它使用磁力计和有点不正确的加速度(比如特定力)。

要使用代码,您应该是巫师或者了解INS和KF的基础知识。许多参数必须进行精细调整才能使滤波器工作。正如Edward所说,这些人正在为生计而努力。

至少在谷歌的Galaxy Nexus上,这个默认实现被闲置不用,并被Invense的专有系统覆盖掉。


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