3

I have implemented the compass reading according to the usual recommendations that I could find on the web. I use the ROTATION_VECTOR sensor type and I transform it into the (azimuth, pitch, roll) triple using the standard API calls. Here's my code:

fun Fragment.receiveAzimuthUpdates(
        azimuthChanged: (Float) -> Unit,
        accuracyChanged: (Int) -> Unit
) {
    val sensorManager = activity!!.getSystemService(Context.SENSOR_SERVICE)
            as SensorManager
    val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)!!
    sensorManager.registerListener(OrientationListener(azimuthChanged, accuracyChanged),
            sensor, 10_000)
}

private class OrientationListener(
        private val azimuthChanged: (Float) -> Unit,
        private val accuracyChanged: (Int) -> Unit
) : SensorEventListener {
    private val rotationMatrix = FloatArray(9)
    private val orientation = FloatArray(3)

    override fun onSensorChanged(event: SensorEvent) {
        if (event.sensor.type != Sensor.TYPE_ROTATION_VECTOR) return
        SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
        SensorManager.getOrientation(rotationMatrix, orientation)
        azimuthChanged(orientation[0])
    }

    override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
        if (sensor.type == Sensor.TYPE_ROTATION_VECTOR) {
            accuracyChanged(accuracy)
        }
    }
}

This results in behavior that's quite good when you hold the phone horizontally, like you would a real compass. However, when you hold it like a camera, upright and in front of you, the reading breaks down. If you tilt it even slightly beyond upright, so it leans towards you, the azimuth turns to the opposite direction (sudden 180 degree rotation).

Apparently this code tracks the orientation of the phone's y-axis, which becomes vertical on an upright phone, and its ground orientation is towards you when the phone leans towards you.

What could I do to improve this behavior so it's not sensitive to the phone's pitch?

AskNilesh
  • 67,701
  • 16
  • 123
  • 163
Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436

1 Answers1

9

TL;DR

Scroll down to "Improved Solution" for the full code of the fixed OrientationListener.

Analysis

Apparently this code tracks the orientation of the phone's y-axis, which becomes vertical on an upright phone, and its ground orientation is towards you when the phone leans towards you.

Yes, this is correct. You can inspect the code of getOrientation() to see what's going on:

public static float[] getOrientation(float[] R, float[] values) {
    /*
     *   /  R[ 0]   R[ 1]   R[ 2]  \
     *   |  R[ 3]   R[ 4]   R[ 5]  |
     *   \  R[ 6]   R[ 7]   R[ 8]  /
     */
     values[0] = (float) Math.atan2(R[1], R[4]);
     ...

values[0] is the azimuth value you got.

You can interpret the rotation matrix R as the components of the vectors that point in the device's three major axes:

  • column 0: vector pointing to phone's right
  • column 1: vector pointing to phone's up
  • column 2: vector pointing to phone's front

The vectors are described from the perspective of the Earth's coordinate system (east, north, and sky).

With this in mind we can interpret the code in getOrientation():

  1. select the phone's up axis (matrix column 1, stored in array elements 1, 4, 7)
  2. project it to the Earth's horizontal plane (this is easy, just ignore the sky component stored in element 7)
  3. Use atan2 to deduce the angle from the remaining east and north components of the vector.

There's another subtlety hiding here: the signature of atan2 is

public static double atan2(double y, double x);

Note the parameter order: y, then x. But getOrientation passes the arguments in the east, north order. This achieves two things:

  • makes north the reference axis (in geometry it's the x axis)
  • mirrors the angle: geometrical angles are anti-clockwise, but azimuth must be the clockwise angle from north

Naturally, when the phone's up axis goes vertical ("skyward") and then beyond, its azimuth flips by 180 degrees. We can fix this in a very simple way: we'll use the phone's right axis instead. Note the following:

  • when the phone is horizontal and facing north, its right axis is aligned with the east axis. The east axis, in the Earth's coordinate system, is the "x" geometrical axis, so our 0-angle reference is correct out-of-the-box.
  • when the phone turns right (eastwards), its azimuth should rise, but its geometrical angle goes negative. Therefore we must flip the sign of the geometrical angle.

Solution

So our new formula is this:

val azimuth = -atan2(R[3], R[0])

And this trivial change is all you need! No need to call getOrientation, just apply this to the orientation matrix.

Improved Solution

So far, so good. But what if the user is using the phone in the landscape orientation? The phone's axes are unaffected, but now the user perceives the phone's "left" or "right" direction as "ahead" (depending on how the user turned the phone). We can correct for this by inspecting the Display.rotation property. If the screen is rotated, we'll use the up axis of the phone to play the same role as the right axis above.

So the full code of the orientation listener becomes this:

private class OrientationListener(
        private val activity: Activity,
        private val azimuthChanged: (Float) -> Unit,
        private val accuracyChanged: (Int) -> Unit
) : SensorEventListener {
    private val rotationMatrix = FloatArray(9)

    override fun onSensorChanged(event: SensorEvent) {
        if (event.sensor.type != Sensor.TYPE_ROTATION_VECTOR) return
        SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
        val (matrixColumn, sense) = when (val rotation = 
                activity.windowManager.defaultDisplay.rotation
        ) {
            Surface.ROTATION_0 -> Pair(0, 1)
            Surface.ROTATION_90 -> Pair(1, -1)
            Surface.ROTATION_180 -> Pair(0, -1)
            Surface.ROTATION_270 -> Pair(1, 1)
            else -> error("Invalid screen rotation value: $rotation")
        }
        val x = sense * rotationMatrix[matrixColumn]
        val y = sense * rotationMatrix[matrixColumn + 3]
        azimuthChanged(-atan2(y, x))
    }

    override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
        if (sensor.type == Sensor.TYPE_ROTATION_VECTOR) {
            accuracyChanged(accuracy)
        }
    }
}

With this code, you're getting the exact same behavior as on Google Maps.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • This does not behave like google maps. When I tilt my phone upright (vertical), or past vertical, the azimuth is heavily distorted, whereas google maps calculates the correct value – hunter Aug 22 '20 at 11:27
  • I take that back! My mistake was using a rotationMatrix with 16 elements instead of 9 (as per your code). Now it works perfectly. I can't believe how simple and elegant your solution is. I've lost about a week on this problems, investigating different sensors, quaternions, even entire libraries. Many many thanks! – hunter Aug 22 '20 at 11:37
  • I lost about a week as well, so I wrote this to help others avoid the same ordeal. I'm really sorry to hear it wasn't easier to google this one out. – Marko Topolnik Aug 23 '20 at 12:18
  • Thanks @MarkoTopolnik for your answer! I am still unsure though if I understood it correctly. I would need to calculate the angle between the z axis of the phone and the magnetic north, would I go for `values[0] = (float) Math.atan2(R[2], R[5]);` then? Could you please have a look at my question at this link? Thanks! (https://stackoverflow.com/questions/69626915/get-angle-between-phones-z-axis-and-the-magnetic-north-pole-instead-of-y-axis) – Mike Oct 20 '21 at 12:01
  • After hours of searching and trying out code samples, this was the only example that worked. Thank you very much! – Bernhard Krenz Sep 15 '22 at 13:11
  • @BernhardKrenz I'm sorry to hear that you had a hard time too, seems everyone who needs this goes through the same ordeal. Maybe I can add some text that would better trigger Google to include this page in the hits, do you have a suggestion? – Marko Topolnik Sep 15 '22 at 15:54
  • can you also make it work more “generic”, i.e. when the phone is half landscape and half portrait? Basically the angle should be stable when rotating the screen. – user3612643 Feb 17 '23 at 22:59
  • @user3612643 It already does that because rotating the screen affects the orientation of the device in Earth's coordinates. The problem that needed solving was identifying the axis of the phone whose orientation we want to track, and this axis must stay fixed relative to the "up" direction of what you see on the screen. The "up" direction doesn't change except when you transition betwen portrait and landscape. – Marko Topolnik Feb 18 '23 at 15:44
  • Wow, that's awesome. First thing I am gonna try on Monday (need to port it to Dart). – user3612643 Feb 18 '23 at 19:09
  • Follow up question: Does that mean that Android changes the axis assignment when the device screen rotates? – user3612643 Feb 18 '23 at 19:16
  • So, tried it. It does not work when the phone is rotated 45 degrees (between landscape and portrait). Basically what I wants is to get the angle the camera lens is pointing at. That is independent of whether the phone is in portrait or landscape or in-between. – user3612643 Feb 18 '23 at 19:42
  • The code above tracks the orientation of the phone's x-axis (the direction of on-screen text). You need to change that to the z-axis. The text of the answer should already provide you with enough information to achieve that. – Marko Topolnik Feb 19 '23 at 20:23
  • You are right — reading your answer again, it’s easy to translate it to any axis. – user3612643 Feb 21 '23 at 08:06