安卓指南针方向不可靠(低通滤波器)

15

我正在创建一个应用程序,需要根据设备的方向来定位 ImageView。我使用磁场和加速度传感器的值来计算设备的方向。

SensorManager.getRotationMatrix(rotationMatrix, null, accelerometerValues, magneticFieldValues)
SensorManager.getOrientation(rotationMatrix, values);
double degrees = Math.toDegrees(values[0]);

我的问题是,ImageView的位置对于方向的变化非常敏感,导致ImageView在屏幕上不停地跳来跳去(因为度数在改变)。

我了解到这可能是因为我的设备靠近一些会影响磁场读数的物体。但这似乎并不是唯一的原因。

我尝试下载了一些应用程序,发现“3D指南针”和“指南针”在其读数方面保持极其稳定(设置噪声滤波器后),我希望我的应用程序也能达到同样的效果。

我读到可以通过添加“低通滤波器”来调整读数的“噪声”,但由于我缺乏数学知识,不知道如何实现这一点。

我希望有人能帮助我让设备上的读数更加稳定,即使稍微移动设备也不会影响当前的方向。目前我只进行了一些小的调整。

if (Math.abs(lastReadingDegrees - newReadingDegrees) > 1) { updatePosition() }

为了过滤掉一些噪音。但效果不是很好 :)


我的“重复”答案被删除了,但我认为它会真正帮助到人们,因为我知道在Android上经历这种传感器噪声难题的痛苦。无论如何,对于未来的人们,这是适用于我的解决方案--> https://dev59.com/X0rSa4cB1Zd3GeqPZMQY#39276888 - Rahul Sainani
5个回答

32

虽然我没有在Android上使用过指南针,但以下基本处理方式(使用JavaScript)可能适用于您。

它基于加速度计上的低通滤波器,该滤波器被Windows Phone团队推荐,并进行了修改以适应指南针(每360"循环行为)。

我假设指南针读数是以度为单位的,介于0到360之间的浮点数,并且输出应该类似。

您想在过滤器中完成两件事情:

  1. 如果变化很小,则逐渐向那个方向旋转,以防止抖动。
  2. 如果变化很大,则立即向那个方向旋转(如果您希望指南针只以平稳的方式移动,则可以取消它)以防止滞后。

为此,我们将有2个常量:

  1. 缓动浮点数,定义运动的平滑程度(1表示不平滑,0表示从不更新,默认值为0.5)。我们将其称为SmoothFactorCompass。
  2. 距离足够大以立即转向的阈值(0始终跳转,360从不跳转,默认值为30)。我们将其称为SmoothThresholdCompass。

我们有一个跨调用保存的变量,名为oldCompass,它是算法的结果。

因此,变量定义如下:

var SmoothFactorCompass = 0.5;
var SmoothThresholdCompass = 30.0;
var oldCompass = 0.0;

该函数接收 newCompass 参数,并将 oldCompass 作为结果返回。

if (Math.abs(newCompass - oldCompass) < 180) {
    if (Math.abs(newCompass - oldCompass) > SmoothThresholdCompass) {
        oldCompass = newCompass;
    }
    else {
        oldCompass = oldCompass + SmoothFactorCompass * (newCompass - oldCompass);
    }
}
else {
    if (360.0 - Math.abs(newCompass - oldCompass) > SmoothThresholdCompass) {
        oldCompass = newCompass;
    }
    else {
        if (oldCompass > newCompass) {
            oldCompass = (oldCompass + SmoothFactorCompass * ((360 + newCompass - oldCompass) % 360) + 360) % 360;
        } 
        else {
            oldCompass = (oldCompass - SmoothFactorCompass * ((360 - newCompass + oldCompass) % 360) + 360) % 360;
        }
    }
}

我看到这个问题已经提出了5个月,可能已经不再相关了,但我相信其他程序员可能会发现它有用。

Oded Elyada。


3
oldCompass约为0,而newCompass约为359时,这给我带来了奇怪的结果。 - Johan S

19

这个低通滤波器适用于弧度角。对每个罗盘读数使用add函数,然后调用average获得平均值。

public class AngleLowpassFilter {

    private final int LENGTH = 10;

    private float sumSin, sumCos;

    private ArrayDeque<Float> queue = new ArrayDeque<Float>();

    public void add(float radians){

        sumSin += (float) Math.sin(radians);

        sumCos += (float) Math.cos(radians);

        queue.add(radians);

        if(queue.size() > LENGTH){

            float old = queue.poll();

            sumSin -= Math.sin(old);

            sumCos -= Math.cos(old);
        }
    }

    public float average(){

        int size = queue.size();

        return (float) Math.atan2(sumSin / size, sumCos / size);
    }
}

使用 Math.toDegrees()Math.toRadians() 进行转换。


1
你能解释一下为什么在这里需要所有这些三角函数吗? - felixd
5
嗨Felix,你需要三角函数,因为一组角度的简单数值平均并不总是代表中间值。假设你的角度(以度为单位)是[350,10],那么你的平均值将为(350+10)/2 = 180(实际中间值为0)。也感谢您的编辑。 - SharkAlley
好的,是的,那很有道理。不知何故三角函数解决了这个问题。 :) 我对于 average() 返回的角度是以度数为单位的,对吗? - felixd
2
返回值以弧度表示(与输入值和三角函数一致)。我们使用cos和sin将角度分解为[x,y]向量,因为这些向量的平均值确实代表了中线。然后,我们使用atan2将向量转换回弧度角。 - SharkAlley
3
你的实现完全符合我的期望。即使没有插值动画,也很流畅。 - Glogo
我尝试了这种方法和Oded Elyada的方法,最终我决定选择这个。 - Văn Quyết

4

请记住,例如350和10的平均值不是180。我的解决方案:

int difference = 0;
for(int i= 1;i <numberOfAngles;i++){
    difference += ( (angles[i]- angles[0] + 180 + 360 ) % 360 ) - 180;
}
averageAngle = (360 + angles[0] + ( difference / numberOfAngles ) ) % 360;

3
一个低通滤波器(LPF)可以阻止快速变化的信号,只允许信号中的慢速变化。这意味着任何小的突然变化都会被忽略。
在软件中实现这个标准的方法是对过去N个样本进行平均并报告该值。从N=3开始,并不断增加N直到您在应用程序中找到足够平滑的响应为止。
请记住,N越大,系统响应就越慢。

2

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