15

I'm using the Android gravity and magnetic field sensors to calculate orientation via SensorManager.getRotationMatrix and SensorManager.getOrientation. This gives me the azimuth, pitch and orientation numbers. The results look sensible when the device is lying flat on a table.

However, I've disabled switches between portrait and landscape in the manifest, so that getWindowManager().getDefaultDisplay().getRotation() is always zero. When I rotate the device by 90 degrees so that it's standing vertical I run into trouble. Sometimes the numbers seem quite wrong, and I've realised that this relates to Gimbal lock. However, other apps don't seem to have this problem. For example, I've compared my app against two free sensor test apps (Sensor Tester (Dicotomica) and Sensor Monitoring (R's Software)). My app agrees with these apps when the device is flat, but as I rotate the device into the vertical position there can be significant differences. The two apps seem to agree with each other, so how do they get around this problem?

gisking
  • 290
  • 2
  • 3
  • 8

3 Answers3

11

When the device is not flat, you have to call remapCoordinateSystem(inR, AXIS_X, AXIS_Z, outR); before calling getOrientation.

The azimuth returns by getOrientation is obtained by orthogonally project the device unit Y axis into the world East-North plane and then calculate the angle between the resulting projection vector and the North axis.

Now we normally think of direction as the direction where the back camera is pointing. That is the direction of -Z where Z is the device axis pointing out of the screen. When the device is flat we do not think of direction and accept what ever given. But when it is not flat we expect it is the direction of -Z. But getOrientation calculate the direction of the Y axis, thus we need to swap the Y and Z axes before calling getOrientation. That is exactly what remapCoordinateSystem(inR, AXIS_X, AXIS_Z, outR) does, it keep the X axis intact and map Z to Y.

Now so how do you know when to remap or not. You can do that by checking

float inclination = (float) Math.acos(rotationMatrix[8]);
if (result.inclination < TWENTY_FIVE_DEGREE_IN_RADIAN 
            || result.inclination > ONE_FIFTY_FIVE_DEGREE_IN_RADIAN)
{
    // device is flat just call getOrientation
}
else
{
    // call remap
}

The inclination above is the angle between the device screen and the world East-North plane. It shows how much the device is tilting.

Hoan Nguyen
  • 18,033
  • 3
  • 50
  • 54
  • Thanks for the response. However, this isn't what those two free sensor apps are doing because when the inclination gets past 25 (or 155 degress), with your suggestion the pitch and roll figures jump due to the switch of the axes. The pitch and roll figures for the free sensor apps don't jump. Also the switch of the axes to AXIS_X and AXIS_Z is only correct for tilting in one direction, for tilts in other directions it causes incorrect azimuth jumps (although no doubt that could be correct by adjusting the axes which are being switched to). – gisking Apr 07 '13 at 11:23
  • for the pitch and roll values you do not call remap, just for the azimuth. I do not understand what do you mean by tilting in the other direction? If you mean the screen is not facing you, then certainly the direction is opposite to what when the screen is facing you. – Hoan Nguyen Apr 07 '13 at 19:03
  • Thsnks for your thoughts, but the other answer seems to provide a more consistent spproach. – gisking May 06 '13 at 11:58
  • This is the general approach written in almost every post for AR applications, also checked it from some 3D compasses from play store. Each and every app gives the same azimuth but there is a diffence between the value you get when the device is flat. The difference is about 10 degrees. I haven't been able to solve this problem yet. You don't get the same results when device is laying on surface or when it's flat. – Thracian May 13 '17 at 16:37
8

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 definition of the pitch angle which is exactly the angle given by Math.acos(rotationMatrix[8]) that is mentioned in another answer here.

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

equation with definition of R matrix

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

equation defining the azimuth angle

Community
  • 1
  • 1
Stochastically
  • 7,616
  • 5
  • 30
  • 58
  • 1
    Unfortunately this doesn't work for the rotation matrix I got from `SensorManager.getRotationMatrixFromVector` (using the sensor `TYPE_ROTATION_VECTOR`). When I calculate `double a = Math.toDegrees(Math.atan((r[1] - r[3]) / (r[0] + r[4])));` (where r is the rotation matrix) I always end up with angles of -90 <= `a` <= 90 – medihack Jul 19 '16 at 11:01
0

I'll add an example that worked for me, using Hoan's answer, and also highlight some new resources that are available now and may help when looking at apps that need to use the gyro sensors.

Firstly, there is a Google Code lab available with a sample app - I found the completed sample app very useful for understanding a devices behaviour.

The app display looks like this:

enter image description here

The codelabs and the final app version is available at these links (at the time of writing):

In particular this allows you experiment as follows (its easier to do than describe...):

  • put your device down flat on a table and experiment with rotating on the table while flat, lifting top and bottom (i.e. lift the top of the display, where your front camera typically is, from the table while leaving the bottom, where your home button typically is if you have one, on the table. Similarly, lift the left and right edges of the device. Observe the azimuth, pitch and roll values in the app.

  • Put your device against a wardrobe door in portrait mode and again experiment moving it around. Open the door to see the effect also. Rotate to landscape to see the difference.

The key thing I think you may see from the above is that everything is very simple and works well when flat and everything is complex and interrelated when not flat.

Looking specifically at a use case I had to display the pitch and roll when vertical in portrait mode, the following worked for me:

 when (event?.sensor?.type) {
            Sensor.TYPE_ROTATION_VECTOR -> {

                // Calculate the rotation matrix
                val rotMatrix = FloatArray(9)
                var rotationMatrixAdjusted = FloatArray(9)
                val azimuthChanged: (Float) -> Unit
                val orientation = FloatArray(3)
                SensorManager.getRotationMatrixFromVector(rotMatrix, event.values);
                SensorManager.remapCoordinateSystem(rotMatrix,
                        SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X,
                        rotationMatrixAdjusted);
                SensorManager.getOrientation(rotationMatrixAdjusted, orientation)

                val rollAngleRadians = orientation[1]
                val pitchAngleRadians = orientation[2]
                              
                //Report results back to listener
                thisListener.onRollPitchEvent(rollAngleRadians, pitchAngleRadians)
            }
        }
Mick
  • 24,231
  • 1
  • 54
  • 120