Inconsistent orientation sensor values on Android for azimuth/yaw and roll
Asked Answered
C

4

5

I’m having trouble getting good orientation sensor readings. The sensor readings seemed unreliable, so I tested my code against two free sensor test apps (Sensor Tester (Dicotomica) and Sensor Monitoring (R's Software)). I found that while my readings often agreed with the sensor test apps, occasionally the values for azimuth/yaw, and roll differed by up to 40 degrees, although the pitch reading mostly agreed. The two free apps always seemed to agree with each other.

I put my code into a tiny Android activity and got the same inconsistency. The code is as follows:

public class MainActivity extends Activity implements  SensorEventListener {

    private SensorManager mSensorManager;
    private float[] AccelerometerValues;
    private float[] MagneticFieldValues;
    private float[] RotationMatrix;
    private long nextRefreshTime;           // used to ensure dump to LogCat occurs no more than 4 times a second
    private DecimalFormat df;               // used for dumping sensors to LogCat

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mSensorManager = (SensorManager)getSystemService(android.content.Context.SENSOR_SERVICE);
        Sensor SensorAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        mSensorManager.registerListener(this, SensorAccelerometer, SensorManager.SENSOR_DELAY_UI);  
        Sensor SensorMagField = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
        mSensorManager.registerListener(this, SensorMagField, SensorManager.SENSOR_DELAY_UI);
        AccelerometerValues = new float[3];
        MagneticFieldValues = new float[3];
        RotationMatrix = new float[9];
        nextRefreshTime = 0;
        df = new DecimalFormat("#.00");
    }

    @Override
    public void onSensorChanged(SensorEvent event) {

        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER)
            System.arraycopy(event.values, 0, AccelerometerValues, 0, AccelerometerValues.length);
        else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD)
                System.arraycopy(event.values, 0, MagneticFieldValues, 0, MagneticFieldValues.length);

        if (AccelerometerValues != null && MagneticFieldValues != null) {
            if(SensorManager.getRotationMatrix(RotationMatrix, null, AccelerometerValues, MagneticFieldValues)) {
                float[] OrientationValues = new float[3];
                SensorManager.getOrientation(RotationMatrix, OrientationValues);

                // chance conventions to match sample apps
                if (OrientationValues[0] < 0) OrientationValues[0] += 2*(float)Math.PI;
                OrientationValues[2] *= -1;

                // dump to logcat 4 times a second
                long currentTimeMillis = System.currentTimeMillis();
                if (currentTimeMillis > nextRefreshTime) {
                    nextRefreshTime = currentTimeMillis+250;
                    Log.i("Sensors",    // arrange output so that numbers line up in columns :-)
                            "(" + AngleToStr(OrientationValues[0]) + "," + AngleToStr(OrientationValues[1]) + "," + AngleToStr(OrientationValues[2])
                            + ") ("+FloatToStr(AccelerometerValues[0]) + "," + FloatToStr(AccelerometerValues[1]) + "," + FloatToStr(AccelerometerValues[2])
                            + ") ("+FloatToStr(MagneticFieldValues[0]) + "," + FloatToStr(MagneticFieldValues[1]) + "," + FloatToStr(MagneticFieldValues[2])+")");
                }               
            }
        }               
    }

    private String AngleToStr(double AngleInRadians) {
        String Str = "   "+Integer.toString((int)Math.toDegrees(AngleInRadians));
        return Str.substring(Str.length() - 3);
    }
    private String FloatToStr(float flt) {
        String Str = "      "+df.format(flt);
        return Str.substring(Str.length() - 6);
    }   

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSensorManager.unregisterListener(this);
    }

    @Override
    public void onAccuracyChanged(Sensor arg0, int arg1) { }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }

}

I’m using a Galaxy Note 2 running Jelly Bean 4.1.1. Can anyone tell me what I’m doing wrong?

Update 24-Mar-2013: More information. (1) I've disabled switches between portrait and landscape in the manifest, so getWindowManager().getDefaultDisplay().getRotation() is always zero. Hence I don't think remapCoordSystem would help here, because that's for switching axes, whereas the errors that I'm seeing aren't big errors, they're much more subtle. (2) I've checked the accuracy sensitivity, and the inconsistencies occur when both sensors claim to have high accuracy.

As an example of the inconsistencies that I'm seeing, when the code above give me (azimuth,pitch,roll) = (235,-52,-11) then the two free apps show similar values. But when I see (278, -58, -52) the apps show (256, -58, -26), so big differences in both Azimuth and roll, although pitch seems OK.

Cotsen answered 21/3, 2013 at 0:4 Comment(0)
E
6

I think the best way of defining your orientation angles when the device isn't flat is to use a more appropriate angular co-ordinate system that the standard Euler angles that you get from SensorManager.getOrientation(...). I suggest the one that I describe here on math.stackexchange.com. I've also put some code that does implements it in an answer here. Apart from a good definition of azimuth, it also has a better definition of the pitch angle, which this methodology defines as rotation out of the horizontal plane irrespective of which axis the rotation occurs along.

You can get full details from the two links that I've given in the first paragraph. However, in summary, your rotation matrix R from SensorManager.getRotationMatrix(...) is

Definition of rotation matrix R

where (Ex, Ey, Ez), (Nx, Ny, Nz) and (Gx, Gy, Gz) are vectors pointing due East, North, and in the direction of Gravity. Then the azimuth angle that you want is given by

Definition of azimuth angle phi

Escamilla answered 7/5, 2013 at 11:36 Comment(6)
It only gives Azimuth from -90 to 90 degree. How can I get Azimuth in the range 0 to 359 Degree when moving from North.Wilber
@Hoosier good question, because as you say, the usual inverse tangent returns numbers in the region -90 to 90 degrees. The answer is to use Atan2 (en.wikipedia.org/wiki/Atan2) which is available in both the languages that I use (namely C# and Java).Escamilla
Thanks. It works this way. mAzimuthAngleNotFlat = (int) Math.toDegrees(Math .atan2((rotationMatrix[1] - rotationMatrix[3]), (rotationMatrix[0] + rotationMatrix[4]))); if (mAzimuthAngleNotFlat < 0) { mAzimuthAngleNotFlat += 360; }Wilber
In your solution you don't use remapCoordinates(). I read in your post (math.stackexchange.com/questions/381649/…) that you think remapCoordinates() is not a good solution. But in your example roll also affects azimuth. So how to ignore roll with your approach?Side
Gravity or minus gravity?Chiffchaff
I've always wanted to see someone invent an anti-graivity machine, @Xyz, so if you work out how to do it please let me know :-)Escamilla
W
5

It all depends on the orientation of the device. If the device is not flat and there is some rotation ie not quite Portrait or Landscape, then the azimuth is wrong. You have to call remapCoordSystem before calling getOrientation in this case. You also should at least filter the accelerometer values.
For more detail see my answer at Convert magnetic field X, Y, Z values from device into global reference frame
And for the meaning of flatness see How to measure the tilt of the phone in XY plane using accelerometer in Android

You should test your app as follow:

  1. Lay your device flat and start your app and compare your value with the other apps. Give each reading about 30 seconds and do not move the device during this step.
    My guess for this step: Your value should differ little from the other apps values but it may be less stable in the beginning than the other apps.

  2. While your app is running, rotate your device to a new position, wait until the value become stable (does not have to be the same value, but variation is about 1 or 2 degrees). In the same position compare your value to the other apps values. Repeat the same thing but with the other apps running when rotate.
    My guess for this step: your stable values should differ little from the other apps values but it takes longer for your value to become stable. Also, when stop at the new position the difference between the first few values and the stable value are larger for your app than the other.

  3. Now put the following code before the comment //dump to logcat and log it as you do for the orientation
    float rotation = (float) Math.atan2(RotationMatrix[6], RotationMatrix[7]);
    Put your device in upright position in a box so that it can not rotate. Make sure that the value of the rotation given above is as near 0 as possible (it will jump a little bit).
    Repeat step 1 and 2 now with the device upright.
    My guess is that your result will be the same as my guess for steps 1 and 2 above.

  4. Put your device in slanted upright position in a box so that it can not further rotate. Make sure that the value of the rotation given above is as near 25 or -25 as possible (it will jump a little bit).
    Repeat step 1 now with the device slanted upright.
    My guess is that your result will be quite different from the other apps.
    You can repeat step 4 with different rotation value.

If my guesses are right come back and I will give you the explanations. In the mean time you can read my answer at Magnetic Fields, Rotation Matrix And global coordinates

Woodpecker answered 21/3, 2013 at 0:20 Comment(5)
Thanks for the reply, but I don't think it solves my problem. I’ve added some more information to my original question at the bottom. Also, I don't think a filter would solve this problem because these errors are persistent. A filter would help solve intermittent bad readings. Thanks anyway, because your links were useful to help me understand more about what the sensors mean.Cotsen
You're right that everything is OK when the phone is flat, and it's wrong when upright. I've started using the android Sensor.TYPE_GRAVITY, and I think that means I don't need to worry about filtering. The only mystery to me now is how the free apps are able to define azimuth, pitch, roll consistently when my app fails when the phone is upright. I'd be very grateful for an answer to this.Cotsen
I will to give you a detail answer if you post another question and then comment here to give me the link to the question. The reason I ask you to do this is because I do not know how many times I edited this answer. The best answer I had on this site suddenly became wiki and I found out because I edited too often. I am being penalized for giving correct, clear and useful answers.Woodpecker
Thanks, I've now posted the remaining question: #15650184Cotsen
I am writing up the answer.Woodpecker
E
3

I can answer part of this puzzle. Everything will be fine when the device is laying flat on a table, but when it's upright and the pitch is plus or minus 90 degrees then you'll encounter Gimbal lock. When this happens, the azimuth and roll are undefined. In that situation azimuth and roll mean the same thing, and if you look into the maths you'll see that either azumith+roll is defined (when pitch = +90 degrees) or azimuth-roll is defined (when pitch = -90 degrees), but the two of them aren't defined on their own. In the example you gave, notice that sum azimuth+roll is approximately the same between your app and the other apps.

Also, you can use android Sensor.TYPE_GRAVITY so that you don't need to use a low pass filter on the accelerometer readings. Sensor.TYPE_GRAVITY is a sensor which is calculated from other sensors to try and eliminate the effects of acceleration from the accelerometer sensor. However, if you want your app to run on old versions of Android prior to the introduction of Sensor.TYPE_GRAVITY then you'll need to do the work yourself.

Escamilla answered 26/3, 2013 at 0:12 Comment(0)
G
0

@Stochastically said for getOrientation(), use:

azimuth = atan2((Ey-Nx), (Ex-Ny))

However, if you look at Android's implementation, it is actually

azimuth = atan2(Ey, Ny)

This as well will give you values in the range of -180 and 180.

Below is the source code given in SensorManager.class

public static float[] getOrientation(float[] R, float values[]) {
    /*
     * 4x4 (length=16) case:
     *   /  R[ 0]   R[ 1]   R[ 2]   0  \
     *   |  R[ 4]   R[ 5]   R[ 6]   0  |
     *   |  R[ 8]   R[ 9]   R[10]   0  |
     *   \      0       0       0   1  /
     *
     * 3x3 (length=9) case:
     *   /  R[ 0]   R[ 1]   R[ 2]  \
     *   |  R[ 3]   R[ 4]   R[ 5]  |
     *   \  R[ 6]   R[ 7]   R[ 8]  /
     *
     */
    if (R.length == 9) {
        values[0] = (float)Math.atan2(R[1], R[4]);
        values[1] = (float)Math.asin(-R[7]);
        values[2] = (float)Math.atan2(-R[6], R[8]);
    } else {
        values[0] = (float)Math.atan2(R[1], R[5]);
        values[1] = (float)Math.asin(-R[9]);
        values[2] = (float)Math.atan2(-R[8], R[10]);
    }
    return values;
}
Goodrow answered 21/2, 2015 at 1:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.