9

I'm having a really annoying problem with a AR view acting like a compass. So when I hold the phone in portrait (so that the screen is pointing to my face), then I call the remapCoordinateSystem that the pitch is 0 when holding it portrait. Then the azimuth (compass functionality) is perfect, but as soon as I tilt the phone the azimuth gets ruined, if I bend forward the azimuth increases and if I bend backwards it decreases.

I use 2 sensors to get the readings, Sensor.TYPE_MAGNETIC_FIELD and Sensor.TYPE_GRAVITY.

I use a lowpassfilter which is pretty basic, it's implemented with an alpha constant and is used directly on the read values from the sensors.

Here is my code:

float[] rotationMatrix = new float[9];
SensorManager.getRotationMatrix(rotationMatrix, null, gravitymeterValues,
    magnetometerValues);

float[] remappedRotationMatrix = new float[9];

SensorManager.remapCoordinateSystem(rotationMatrix, SensorManager.AXIS_X,
    SensorManager.AXIS_Z, remappedRotationMatrix);

float results[] = new float[3];
SensorManager.getOrientation(remappedRotationMatrix, results);

float azimuth = (float) (results[0] * 180 / Math.PI);
if (azimuth < 0) {
    azimuth += 360;
}

float pitch = (float) (results[1] * 180 / Math.PI);
float roll = (float) (results[2] * 180 / Math.PI);

As you see there is no magic here. I call this piece of code when the gravitymeterValues and the magnetometerValues are ready to be used.

My question is how do I stop the azimuth from going crazy when I tilt the phone?

I checked a free app on the Google Play Store, Compass and it hasn't solved this problem, but I hope there is a solution.

I have 2 solutions in mind:

  1. Make the AR view only work in very constrainted pitch angles, right now I have something like pitch >= -5 && pitch <= 30. If this isn't fullfilled the user is shown a screen that asks him/her to rotate the phone to portrait.

  2. Somehow use the pitch to suppress the azimuth, this seems like a pretty device-specific solution though, but of course I'm open for suggestions.

I can also add that I've been searching for a couple of hours for a decent solution and I haven't found any that has given me any better solutions than 2) here.

Thanks in advance!

Johan S
  • 3,531
  • 6
  • 35
  • 63
  • What do you low pass filter? You do not need to filter when using TYPE_GRAVITY. – Hoan Nguyen Jul 31 '13 at 19:42
  • @HoanNguyen Do I need to low-pass the `Sensor.TYPE_MAGNETIC_FIELD`? Now I lowpass both `Sensor.TYPE_MAGNETIC_FIELD` and `Sensor.TYPE_GRAVITY`. – Johan S Jul 31 '13 at 19:45
  • 1
    No, the use of low pass filter is to remove acceleration in the x an y direction. The getRotationMatrix required the third parameter to be approximately the acceleration in the z direction alone. If you use TYPE_GRAVItY there is no need to low pass filter, but many device does not have TYPE_GRAVITY, so you need to use TYPE_ACCELEROMETER and low pass filter for devices that do not have TYPE_GRAVITY. – Hoan Nguyen Jul 31 '13 at 19:49
  • Okay thanks. But still when I tilt the phone my azimuth values are bad, any idea how to solve that? – Johan S Jul 31 '13 at 19:56
  • I save a history and average out the result. I have no problem bending backward and forward, of course the azimuth can change but by no more than 2 or 3 degrees. – Hoan Nguyen Jul 31 '13 at 20:00
  • Okay, is that result device-independant? Because on my Nexus 4 I have some serious issues. A tilt on maybe 10 - 20 degrees gives me +- 30 in azimuth. Can you post the code if you please? – Johan S Jul 31 '13 at 20:03
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/34561/discussion-between-johan-s-and-hoan-nguyen) – Johan S Jul 31 '13 at 20:05

1 Answers1

14

For complete code see https://github.com/hoananguyen/dsensor
Keep a history and average out, I do not know the correct interpretation of pitch and roll so the following code is for azimuth only.

Class members

private List<float[]> mRotHist = new ArrayList<float[]>();
private int mRotHistIndex;
// Change the value so that the azimuth is stable and fit your requirement
private int mHistoryMaxLength = 40;
float[] mGravity;
float[] mMagnetic;
float[] mRotationMatrix = new float[9];
// the direction of the back camera, only valid if the device is tilted up by
// at least 25 degrees.
private float mFacing = Float.NAN;

public static final float TWENTY_FIVE_DEGREE_IN_RADIAN = 0.436332313f;
public static final float ONE_FIFTY_FIVE_DEGREE_IN_RADIAN = 2.7052603f;

onSensorChanged

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

     if (mGravity != null && mMagnetic != null)
     {
          if (SensorManager.getRotationMatrix(mRotationMatrix, null, mGravity, mMagnetic))
          {
              // inclination is the degree of tilt by the device independent of orientation (portrait or landscape)
              // if less than 25 or more than 155 degrees the device is considered lying flat
              float inclination = (float) Math.acos(mRotationMatrix[8]);
              if (inclination < TWENTY_FIVE_DEGREE_IN_RADIAN 
                      || inclination > ONE_FIFTY_FIVE_DEGREE_IN_RADIAN)
              {
                  // mFacing is undefined, so we need to clear the history
                  clearRotHist();
                  mFacing = Float.NaN;
              }
              else
              {
                  setRotHist();
                  // mFacing = azimuth is in radian
                  mFacing = findFacing(); 
              }
          }
     }
}

private void clearRotHist()
{
    if (DEBUG) {Log.d(TAG, "clearRotHist()");}
    mRotHist.clear();
    mRotHistIndex = 0;
}

private void setRotHist()
{
    if (DEBUG) {Log.d(TAG, "setRotHist()");}
    float[] hist = mRotationMatrix.clone();
    if (mRotHist.size() == mHistoryMaxLength)
    {
        mRotHist.remove(mRotHistIndex);
    }   
    mRotHist.add(mRotHistIndex++, hist);
    mRotHistIndex %= mHistoryMaxLength;
}

private float findFacing()
{
    if (DEBUG) {Log.d(TAG, "findFacing()");}
    float[] averageRotHist = average(mRotHist);
    return (float) Math.atan2(-averageRotHist[2], -averageRotHist[5]);
}

public float[] average(List<float[]> values)
{
    float[] result = new float[9];
    for (float[] value : values)
    {
        for (int i = 0; i < 9; i++)
        {
            result[i] += value[i];
        }
    }

    for (int i = 0; i < 9; i++)
    {
        result[i] = result[i] / values.size();
    }

    return result;
}
Hoan Nguyen
  • 18,033
  • 3
  • 50
  • 54
  • Great stuff! Now my azimuth is much better, only problem now is on quick movements it jumps alot, but I guess you have to live with that. Thank you very much for your code. One more question though: what update rate is your sensors? UI/Game/ which one? Thanks – Johan S Aug 01 '13 at 15:10
  • Are you using this code in a compass app? I use UI and it's very frequent, I have great results with your code by the way! – Johan S Aug 01 '13 at 19:20
  • 2
    I was trying to write a location base AR library. I did not completed it because I want to include altitude and find out when testing that altitude returned by GPS is terribly inaccurate. – Hoan Nguyen Aug 01 '13 at 19:26
  • Okay, I will experiment with normal vs UI, thank you for your help, it's really appreciated! – Johan S Aug 01 '13 at 19:35
  • @HoanNguyen Can you show how to do this for `Sensor.TYPE_ROTATION_VECTOR` and `SensorManager.getRotationMatrixFromVector()` instead of `SensorManager.getRotationMatrix()?` – unlimited101 Oct 18 '16 at 17:28
  • TYPE_GYROSCOPE will solve this problem, You can try it. – dragonfly Feb 23 '17 at 12:28
  • if the first listener is `TYPE_GRAVITY` (from your `if...else` condition) then the second listener should be `TYPE_MAGNETIC_FIELD`? – user924 Oct 26 '18 at 08:37