Android的getOrientation()方法返回错误结果。

10

我正在创建一个3D指南针应用程序。

我正在使用getOrientation方法获取方向(与此处几乎相同的实现方式)。如果我将手机放在桌子上,它的表现很好,但是当手机朝向天空(图片上的负Z轴;球体代表地球)时,getOrientation开始给出非常糟糕的结果。它会在0到180度之间以几度的实际角度值给出Z轴的值。有没有办法抑制这种行为?我创建了一个小视频来描述问题(抱歉画质不好)。提前谢谢。

enter image description here

解决方案:当你旋转模型时,有以下差异:

gl.glRotatef(_angleY, 0f, 1f, 0f); //ROLL
gl.glRotatef(_angleX, 1f, 0f, 0f); //ELEVATION
gl.glRotatef(_angleZ, 0f, 0f, 1f); //AZIMUTH


gl.glRotatef(_angleX, 1f, 0f, 0f); //ELEVATION
gl.glRotatef(_angleY, 0f, 1f, 0f); //ROLL
gl.glRotatef(_angleZ, 0f, 0f, 1f); //AZIMUTH

你好,能再发一次你的apk文件吗?你在帖子中包含的版本无法被我的手机和平板电脑上的Android正确解码为.apk文件... - epichorns
我已经重新上传了,同一个地址。问题还是一样吗? - skywall
嗨,Skywall。请看一下我对你在我的帖子中关于范围[0, 360[ vs [-180, 180[的评论的回复,因为它会对IIR滤波算法的结果产生不利影响。至于.apk文件,是的,仍然存在同样的问题,希望这不只是我一个人的问题...我只使用我的HTC手机上的Android 2.1版本。 - epichorns
再次问候Skywall。由于我仍无法加载您的.apk文件,因此我实现了自己版本的Compass3D。请参见下面的整个代码。它在我的手机上运行良好,因此也许您可以在您的手机上测试以查看其是否按预期行事...这可能会澄清它是其他问题(硬件问题)还是真正的代码问题... - epichorns
1
很高兴我的代码对你有所帮助,朋友 :-) - epichorns
显示剩余2条评论
4个回答

12

你的方法存在至少一个问题。

我猜测你将磁力计对应的三维向量与平均低通滤波器相结合以平滑数据。虽然这种方法对于传感器值没有不连续性变化的情况非常有效,例如从加速度计中获取的原始数据,但它并不能直接很好地处理从磁力计中获取的角度变量。为什么呢?

因为这些角度变量(方位角、俯仰角、翻转角)有一个上限和一个下限,这意味着任何高于180度的值,比如181度,都会环绕到181-360=-179度,而任何低于-180度的变量都会在另一个方向上环绕。所以当其中一个角度变量接近这些阈值(180或-180)时,该变量 tend to oscillate to values close to those 2 extremes。当你盲目地将低通滤波器应用于这些值时,你要么得到从180度向-180度平滑减少的结果,要么得到从-180度向180度平滑增加的结果。无论哪种情况,结果看起来都很像你上面的视频......只要将平均缓冲区直接应用于getOrientation(...)的原始角度数据上,这个问题就会存在(不仅在手机直立的情况下存在,也应该存在于存在方位角环绕的情况下......也许你还可以测试这些bug......)。

你说你用缓冲区大小为1来测试了这个方法。理论上,如果根本没有平均,问题就不应该存在,尽管在我过去看到的某些循环缓冲区实现中,这可能意味着仍然至少有一个过去的值进行平均,而不是完全没有平均。如果是这种情况,我们已经找到了错误的根源。

不幸的是,如果要坚持使用标准平均滤波器,就没有太多优雅的解决方案。在这种情况下,我通常会切换到另一种类型的低通滤波器,它不需要任何深度缓存来操作:一个简单的IIR滤波器(一阶):

diff = x[n] - y[n-1]

y[n] - y[n-1] = alpha * (x[n] - y[n-1]) = alpha * diff

...其中y是过滤后的角度,x是原始角度,alpha<1类似于时间常数,因为alpha=1对应于没有滤波的情况,随着alpha接近零,低通滤波器的截止频率降低。一个敏锐的眼睛可能已经注意到这对应于一个简单的比例控制器。

这样的滤波器允许补偿角度值的环绕,因为我们可以添加或减去360到diff,以确保abs(diff)<=180,这反过来确保了过滤后的角度值始终以最佳方向增加/减少以达到其“设定点”。

一个例子是一个周期性调用的函数调用,该函数为给定的原始角度值x计算过滤后的角度值y,可能是这

private float restrictAngle(float tmpAngle){
    while(tmpAngle>=180) tmpAngle-=360;
    while(tmpAngle<-180) tmpAngle+=360;
    return tmpAngle;
}

//x is a raw angle value from getOrientation(...)
//y is the current filtered angle value
private float calculateFilteredAngle(float x, float y){ 
    final float alpha = 0.1f;
    float diff = x-y;

    //here, we ensure that abs(diff)<=180
    diff = restrictAngle(diff);

    y += alpha*diff;
    //ensure that y stays within [-180, 180[ bounds
    y = restrictAngle(y);

    return y;
}

接下来可以定期使用类似以下代码(以getOrientation(...)函数返回的方位角为例)调用函数calculateFilteredAngle(float x, float y)

filteredAzimuth = calculateFilteredAngle(azimuth, filteredAzimuth);

使用该方法,过滤器不会像OP提到的平均过滤器那样出现问题。

由于无法加载OP上传的.apk文件,我决定实现自己的测试项目以查看更正是否有效。以下是完整代码(未使用XML作为主要布局,因此没有包含它)。将其复制到测试项目中以查看是否在特定设备上正常运行(在HTC Desire w/ Android v. 2.1上测试功能):

文件1:Compass3DActivity.java:

package com.epichorns.compass3D;

import android.app.Activity;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

public class Compass3DActivity extends Activity {
    //Textviews for showing angle data
    TextView mTextView_azimuth;
    TextView mTextView_pitch;
    TextView mTextView_roll;

    TextView mTextView_filtered_azimuth;
    TextView mTextView_filtered_pitch;
    TextView mTextView_filtered_roll;


    float mAngle0_azimuth=0;
    float mAngle1_pitch=0;
    float mAngle2_roll=0;

    float mAngle0_filtered_azimuth=0;
    float mAngle1_filtered_pitch=0;
    float mAngle2_filtered_roll=0;

    private Compass3DView mCompassView;

    private SensorManager sensorManager;
    //sensor calculation values
    float[] mGravity = null;
    float[] mGeomagnetic = null;
    float Rmat[] = new float[9];
    float Imat[] = new float[9];
    float orientation[] = new float[3];
    SensorEventListener mAccelerometerListener = new SensorEventListener(){
        public void onAccuracyChanged(Sensor sensor, int accuracy) {}

        public void onSensorChanged(SensorEvent event) {
            if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER){
                mGravity = event.values.clone();
                processSensorData();
            }
        }   
    };
    SensorEventListener mMagnetometerListener = new SensorEventListener(){
        public void onAccuracyChanged(Sensor sensor, int accuracy) {}

        public void onSensorChanged(SensorEvent event) {
            if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD){
                mGeomagnetic = event.values.clone();
                processSensorData();                
                update();
            }
        }   
    };

    private float restrictAngle(float tmpAngle){
        while(tmpAngle>=180) tmpAngle-=360;
        while(tmpAngle<-180) tmpAngle+=360;
        return tmpAngle;
    }

    //x is a raw angle value from getOrientation(...)
    //y is the current filtered angle value
    private float calculateFilteredAngle(float x, float y){ 
        final float alpha = 0.3f;
        float diff = x-y;

        //here, we ensure that abs(diff)<=180
        diff = restrictAngle(diff);

        y += alpha*diff;
        //ensure that y stays within [-180, 180[ bounds
        y = restrictAngle(y);

        return y;
    }



    public void processSensorData(){
        if (mGravity != null && mGeomagnetic != null) { 
            boolean success = SensorManager.getRotationMatrix(Rmat, Imat, mGravity, mGeomagnetic);
            if (success) {              
                SensorManager.getOrientation(Rmat, orientation);
                mAngle0_azimuth = (float)Math.toDegrees((double)orientation[0]); // orientation contains: azimut, pitch and roll
                mAngle1_pitch = (float)Math.toDegrees((double)orientation[1]); //pitch
                mAngle2_roll = -(float)Math.toDegrees((double)orientation[2]); //roll               
                mAngle0_filtered_azimuth = calculateFilteredAngle(mAngle0_azimuth, mAngle0_filtered_azimuth);
                mAngle1_filtered_pitch = calculateFilteredAngle(mAngle1_pitch, mAngle1_filtered_pitch);
                mAngle2_filtered_roll = calculateFilteredAngle(mAngle2_roll, mAngle2_filtered_roll);    
            }           
            mGravity=null; //oblige full new refresh
            mGeomagnetic=null; //oblige full new refresh
        }
    }

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);     
        LinearLayout ll = new LinearLayout(this);       
        LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.FILL_PARENT);      
        ll.setLayoutParams(llParams);      
        ll.setOrientation(LinearLayout.VERTICAL);      
        ViewGroup.LayoutParams txtParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);        
        mTextView_azimuth = new TextView(this);
        mTextView_azimuth.setLayoutParams(txtParams);
        mTextView_pitch = new TextView(this);
        mTextView_pitch.setLayoutParams(txtParams);
        mTextView_roll = new TextView(this);
        mTextView_roll.setLayoutParams(txtParams);      
        mTextView_filtered_azimuth = new TextView(this);
        mTextView_filtered_azimuth.setLayoutParams(txtParams);
        mTextView_filtered_pitch = new TextView(this);
        mTextView_filtered_pitch.setLayoutParams(txtParams);
        mTextView_filtered_roll = new TextView(this);
        mTextView_filtered_roll.setLayoutParams(txtParams);

        mCompassView = new Compass3DView(this);        
        ViewGroup.LayoutParams compassParams = new ViewGroup.LayoutParams(200,200);
        mCompassView.setLayoutParams(compassParams);

        ll.addView(mCompassView);
        ll.addView(mTextView_azimuth);
        ll.addView(mTextView_pitch);
        ll.addView(mTextView_roll);
        ll.addView(mTextView_filtered_azimuth);
        ll.addView(mTextView_filtered_pitch);
        ll.addView(mTextView_filtered_roll);

        setContentView(ll);

        sensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);
        sensorManager.registerListener(mAccelerometerListener, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_UI); 
        sensorManager.registerListener(mMagnetometerListener, sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD), SensorManager.SENSOR_DELAY_UI);
        update();       
    }


    @Override
    public void onDestroy(){
        super.onDestroy();
        sensorManager.unregisterListener(mAccelerometerListener);
        sensorManager.unregisterListener(mMagnetometerListener);
    }


    private void update(){
        mCompassView.changeAngles(mAngle1_filtered_pitch,  mAngle2_filtered_roll, mAngle0_filtered_azimuth);

        mTextView_azimuth.setText("Azimuth: "+String.valueOf(mAngle0_azimuth));
        mTextView_pitch.setText("Pitch: "+String.valueOf(mAngle1_pitch));
        mTextView_roll.setText("Roll: "+String.valueOf(mAngle2_roll));

        mTextView_filtered_azimuth.setText("Azimuth: "+String.valueOf(mAngle0_filtered_azimuth));
        mTextView_filtered_pitch.setText("Pitch: "+String.valueOf(mAngle1_filtered_pitch));
        mTextView_filtered_roll.setText("Roll: "+String.valueOf(mAngle2_filtered_roll));

    }
}

文件2:Compass3DView.java:

package com.epichorns.compass3D;

import android.content.Context;
import android.opengl.GLSurfaceView;

public class Compass3DView extends GLSurfaceView {
    private Compass3DRenderer mRenderer;

    public Compass3DView(Context context) {
        super(context);
        mRenderer = new Compass3DRenderer(context);
        setRenderer(mRenderer);
    }

    public void changeAngles(float angle0, float angle1, float angle2){
        mRenderer.setAngleX(angle0);
        mRenderer.setAngleY(angle1);
        mRenderer.setAngleZ(angle2);
    }

}

文件3:Compass3DRenderer.java:

package com.epichorns.compass3D;


import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.opengl.GLSurfaceView;


public class Compass3DRenderer implements GLSurfaceView.Renderer {
    Context mContext;

    // a raw buffer to hold indices
    ShortBuffer _indexBuffer;    
    // raw buffers to hold the vertices
    FloatBuffer _vertexBuffer0;
    FloatBuffer _vertexBuffer1;
    FloatBuffer _vertexBuffer2;
    FloatBuffer _vertexBuffer3;
    FloatBuffer _vertexBuffer4;
    FloatBuffer _vertexBuffer5;
    int _numVertices = 3; //standard triangle vertices = 3

    FloatBuffer _textureBuffer0123;



    //private FloatBuffer _light0Position;
    //private FloatBuffer _light0Ambient;
    float _light0Position[] = new float[]{10.0f, 10.0f, 10.0f, 0.0f};
    float _light0Ambient[] = new float[]{0.05f, 0.05f, 0.05f, 1.0f};
    float _light0Diffuse[] = new float[]{0.5f, 0.5f, 0.5f, 1.0f};
    float _light0Specular[] = new float[]{0.7f, 0.7f, 0.7f, 1.0f};
    float _matAmbient[] = new float[] { 0.6f, 0.6f, 0.6f, 1.0f };
    float _matDiffuse[] = new float[] { 0.6f, 0.6f, 0.6f, 1.0f };




    private float _angleX=0f;
    private float _angleY=0f;
    private float _angleZ=0f;


    Compass3DRenderer(Context context){
        super();
        mContext = context;
    }

    public void setAngleX(float angle) {
        _angleX = angle;
    }

    public void setAngleY(float angle) {
        _angleY = angle;
    }

    public void setAngleZ(float angle) {
        _angleZ = angle;
    }

    FloatBuffer InitFloatBuffer(float[] src){
        ByteBuffer bb = ByteBuffer.allocateDirect(4*src.length);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer inBuf = bb.asFloatBuffer();
        inBuf.put(src);
        return inBuf;
    }

    ShortBuffer InitShortBuffer(short[] src){
        ByteBuffer bb = ByteBuffer.allocateDirect(2*src.length);
        bb.order(ByteOrder.nativeOrder());
        ShortBuffer inBuf = bb.asShortBuffer();
        inBuf.put(src);
        return inBuf;
    }

    //Init data for our rendered pyramid
    private void initTriangles() {

        //Side faces triangles
        float[] coords = {
            -0.25f, -0.5f, 0.25f,
            0.25f, -0.5f, 0.25f,
            0f, 0.5f, 0f
        };

        float[] coords1 = {
            0.25f, -0.5f, 0.25f,
            0.25f, -0.5f, -0.25f,
            0f, 0.5f, 0f
        };

        float[] coords2 = {
            0.25f, -0.5f, -0.25f,
            -0.25f, -0.5f, -0.25f,
            0f, 0.5f, 0f
        };

        float[] coords3 = {
            -0.25f, -0.5f, -0.25f,
            -0.25f, -0.5f, 0.25f,
            0f, 0.5f, 0f
        };

        //Base triangles
        float[] coords4 = {
            -0.25f, -0.5f, 0.25f,
            0.25f, -0.5f, -0.25f,
            0.25f, -0.5f, 0.25f
        };

        float[] coords5 = {
            -0.25f, -0.5f, 0.25f,
            -0.25f, -0.5f, -0.25f, 
            0.25f, -0.5f, -0.25f
        };


        float[] textures0123 = {
                // Mapping coordinates for the vertices (UV mapping CW)
                0.0f, 0.0f,     // bottom left                    
                1.0f, 0.0f,     // bottom right
                0.5f, 1.0f,     // top ctr              
        };


        _vertexBuffer0 = InitFloatBuffer(coords);
        _vertexBuffer0.position(0);

        _vertexBuffer1 = InitFloatBuffer(coords1);
        _vertexBuffer1.position(0);    

        _vertexBuffer2 = InitFloatBuffer(coords2);
        _vertexBuffer2.position(0);

        _vertexBuffer3 = InitFloatBuffer(coords3);
        _vertexBuffer3.position(0);

        _vertexBuffer4 = InitFloatBuffer(coords4);
        _vertexBuffer4.position(0);

        _vertexBuffer5 = InitFloatBuffer(coords5);
        _vertexBuffer5.position(0);

        _textureBuffer0123 = InitFloatBuffer(textures0123);
        _textureBuffer0123.position(0);

        short[] indices = {0, 1, 2};
        _indexBuffer = InitShortBuffer(indices);        
        _indexBuffer.position(0);

    }


    public void onSurfaceCreated(GL10 gl, EGLConfig config) {

        gl.glEnable(GL10.GL_CULL_FACE); // enable the differentiation of which side may be visible 
        gl.glShadeModel(GL10.GL_SMOOTH);

        gl.glFrontFace(GL10.GL_CCW); // which is the front? the one which is drawn counter clockwise
        gl.glCullFace(GL10.GL_BACK); // which one should NOT be drawn

        initTriangles();

        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
    }

    public void onDrawFrame(GL10 gl) {


        gl.glPushMatrix();

        gl.glClearColor(0, 0, 0, 1.0f); //clipping backdrop color
        // clear the color buffer to show the ClearColor we called above...
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

        // set rotation       
        gl.glRotatef(_angleY, 0f, 1f, 0f); //ROLL
        gl.glRotatef(_angleX, 1f, 0f, 0f); //ELEVATION
        gl.glRotatef(_angleZ, 0f, 0f, 1f); //AZIMUTH

        //Draw our pyramid

        //4 side faces
        gl.glColor4f(0.5f, 0f, 0f, 0.5f);
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer0);
        gl.glDrawElements(GL10.GL_TRIANGLES, _numVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);

        gl.glColor4f(0.5f, 0.5f, 0f, 0.5f);
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer1);
        gl.glDrawElements(GL10.GL_TRIANGLES, _numVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);

        gl.glColor4f(0f, 0.5f, 0f, 0.5f);
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer2);
        gl.glDrawElements(GL10.GL_TRIANGLES, _numVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);

        gl.glColor4f(0f, 0.5f, 0.5f, 0.5f);
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer3);
        gl.glDrawElements(GL10.GL_TRIANGLES, _numVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);

        //Base face
        gl.glColor4f(0f, 0f, 0.5f, 0.5f);
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer4);
        gl.glDrawElements(GL10.GL_TRIANGLES, _numVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer5);
        gl.glDrawElements(GL10.GL_TRIANGLES, _numVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);

        gl.glPopMatrix();
    }

    public void onSurfaceChanged(GL10 gl, int w, int h) {
        gl.glViewport(0, 0, w, h);
        gl.glViewport(0, 0, w, h);

    }



}
请注意,此代码未考虑平板电脑默认的横向方向,因此只能在手机上正常工作(我没有附近的平板电脑来测试任何修正代码)。

首先,感谢您的出色回答。1. 我已经通过在160和-160之间的值与160和200之间的值之间进行转换来解决了“180&-180”问题。我使用公式new_angle = (old_angle + 360)%360。2. 我将尝试您的IIR滤波器,看起来非常好。谢谢。 - skywall
嗨。是的,但我必须提到为了解决这个问题,diff 必须在 [-180, 180[ 范围内,而不是 [0, 360[ 范围内,你的公式没有提供这种限制。因此,如果你添加类似 if(new_angle>=180) new_angle-=360 的语句,就可以解决这个问题。问题在于,使用 0-360 范围的 diff 将导致过滤行为,无法使用最佳路径达到所需的目标。这意味着在接近 360 角度门槛时,diff 可能会在 0 和 360 之间来回跳动,你将遇到相同的问题,因此需要将 diff 限制在 [-180, 180[ 角度范围内。 - epichorns
抱歉,我的上一条评论有些不完整:实际上,如果diff在[0, 360[之间,你的角度范围内会出现问题,因为它只允许你的过滤角度单调递增!尽管如此,当你从diff=0转换到diff=360时,仍然会出现不良跳跃。 - epichorns
所以,你试过我的方法了吗?它改变了什么吗? - epichorns
所以,预览更加流畅了,但问题根本没有解决。 - skywall
显示剩余2条评论

3
您可能应该尝试延长延迟时间,例如游戏,或保持/增加您的循环缓冲区的大小。移动设备上的传感器(加速度计、指南针等)本质上是嘈杂的,因此当我提到“低通滤波器”时,我的意思是您是否使用更多数据来降低应用可用更新的频率。您的视频是在室内拍摄的,我建议您去一个EM干扰较少的地方(比如公园),以检查行为是否一致以及标准的指南针重置操作(在八字形中旋转设备)。最终,您可能需要应用一些启发式方法来排除“坏”数据,以便为用户提供更平滑的体验。

  1. 游戏延迟-同样的问题
  2. 更改缓冲区大小-同样的问题
  3. 停车-仍然一样:(
- skywall

1

我曾经遇到和你相同的问题,就是获取设备方向时没有得到解决(必须在检索时设置一个约束与设备位置相关),而且我不知道你是否能够解决它。

拿一个磁力罗盘,在你描述的情况下尝试获取北方方向,你会得到一样的无意义结果。所以你不能真正指望设备的罗盘会更好!


是的,但在市场上绝大多数设备中,磁力计是一个三轴磁力计,而普通的磁罗盘基本上将磁场向量“投影”到其机械二维平面视角上... - epichorns

0

在得到您的许可后,关于滤波,我想说几句话。

  1. 在将磁场向量转换为角度之前,建议对磁场向量本身进行平均化处理。
  2. 仅对角度进行平均/平滑处理是错误的,需要使用某种幅值。 角度本身提供的数据不足以检测方向/航向/轴承。 例如:当您想知道整天的平均风向时,必须使用风力而不仅仅是角度。 如果仅平均角度,则会得到完全错误的风向。 至于轴承方向,我会使用速度作为幅值。

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