静止不动时使用 TYPE_ROTATION_VECTOR 实现当前的基本方向方法?
Posted
技术标签:
【中文标题】静止不动时使用 TYPE_ROTATION_VECTOR 实现当前的基本方向方法?【英文标题】:Implementing current cardinal direction method with TYPE_ROTATION_VECTOR while standing still? 【发布时间】:2019-03-24 10:44:22 【问题描述】:在 android 设备上获取当前主要方向的旧示例似乎有很多,但 Google 提供的官方解决方案似乎并未出现在他们的文档中。
最古老的参考 Sensor.TYPE_ORIENTATION
已被弃用,最近的参考提到 Sensor.TYPE_ACCELEROMETER
和 Sensor.TYPE_MAGNETIC_FIELD
(我尝试过但收效甚微 - 准确度会根据设备方向迅速变化)。我一直在尝试使用 this. 之类的这两个实现,我什至看到过一些使用 TYPE.GRAVITY
的实现。
most recent seem to suggestTYPE_ROTATION_VECTOR 显然是一个融合传感器 (reference),但示例实现似乎并不容易获得。
我需要使用这些位置/运动传感器,而不是 GPS,因为在需要进行测量时用户不会移动。还需要测量稳定,无论手机是平的还是垂直的(就像你在拍照一样)
在我们以某种方式拉出度数测量后,转换为基本方向似乎很容易。(https://***.com/a/25349774/1238737)
以前的解决方案
-
How to get Direction in Android (Such as North, West)
https://***.com/a/11068878/1238737
【问题讨论】:
【参考方案1】:我之前从事开源地图项目,例如 OsmAnd、MapsWithMe 和 MapBox。我认为这些项目是地图和导航领域最好的 android 开源。我检查了他们的代码,发现当手机垂直然后围绕垂直轴(y)旋转时,显示指南针的 MapBox 方法是稳定的。如果旋转矢量传感器可用,它使用TYPE_ROTATION_VECTOR
。否则,它使用TYPE_ORIENTATION
传感器或TYPE_ACCELEROMETER
和TYPE_MAGNETIC_FIELD
的组合。在使用TYPE_ACCELEROMETER
和TYPE_MAGNETIC_FIELD
的情况下,可以通过低通滤波器来减少结果中的振荡,以获得更平滑的值。
这是 MapBox 的指南针引擎及其用法。 .
LocationComponentCompassEngine.java:
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Surface;
import android.view.WindowManager;
import timber.log.Timber;
import java.util.ArrayList;
import java.util.List;
/**
* This manager class handles compass events such as starting the tracking of device bearing, or
* when a new compass update occurs.
*/
public class LocationComponentCompassEngine implements SensorEventListener
// The rate sensor events will be delivered at. As the Android documentation states, this is only
// a hint to the system and the events might actually be received faster or slower then this
// specified rate. Since the minimum Android API levels about 9, we are able to set this value
// ourselves rather than using one of the provided constants which deliver updates too quickly for
// our use case. The default is set to 100ms
private static final int SENSOR_DELAY_MICROS = 100 * 1000;
// Filtering coefficient 0 < ALPHA < 1
private static final float ALPHA = 0.45f;
// Controls the compass update rate in milliseconds
private static final int COMPASS_UPDATE_RATE_MS = 500;
private final WindowManager windowManager;
private final SensorManager sensorManager;
private final List<CompassListener> compassListeners = new ArrayList<>();
// Not all devices have a compassSensor
@Nullable
private Sensor compassSensor;
@Nullable
private Sensor gravitySensor;
@Nullable
private Sensor magneticFieldSensor;
private float[] truncatedRotationVectorValue = new float[4];
private float[] rotationMatrix = new float[9];
private float[] rotationVectorValue;
private float lastHeading;
private int lastAccuracySensorStatus;
private long compassUpdateNextTimestamp;
private float[] gravityValues = new float[3];
private float[] magneticValues = new float[3];
/**
* Construct a new instance of the this class. A internal compass listeners needed to separate it
* from the cleared list of public listeners.
*/
LocationComponentCompassEngine(WindowManager windowManager, SensorManager sensorManager)
this.windowManager = windowManager;
this.sensorManager = sensorManager;
compassSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
if (compassSensor == null)
if (isGyroscopeAvailable())
Timber.d("Rotation vector sensor not supported on device, falling back to orientation.");
compassSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
else
Timber.d("Rotation vector sensor not supported on device, falling back to accelerometer and magnetic field.");
gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
magneticFieldSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
public void addCompassListener(@NonNull CompassListener compassListener)
if (compassListeners.isEmpty())
onStart();
compassListeners.add(compassListener);
public void removeCompassListener(@NonNull CompassListener compassListener)
compassListeners.remove(compassListener);
if (compassListeners.isEmpty())
onStop();
public int getLastAccuracySensorStatus()
return lastAccuracySensorStatus;
public float getLastHeading()
return lastHeading;
public void onStart()
registerSensorListeners();
public void onStop()
unregisterSensorListeners();
@Override
public void onSensorChanged(SensorEvent event)
// check when the last time the compass was updated, return if too soon.
long currentTime = SystemClock.elapsedRealtime();
if (currentTime < compassUpdateNextTimestamp)
return;
if (lastAccuracySensorStatus == SensorManager.SENSOR_STATUS_UNRELIABLE)
Timber.d("Compass sensor is unreliable, device calibration is needed.");
return;
if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR)
rotationVectorValue = getRotationVectorFromSensorEvent(event);
updateOrientation();
// Update the compassUpdateNextTimestamp
compassUpdateNextTimestamp = currentTime + COMPASS_UPDATE_RATE_MS;
else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION)
notifyCompassChangeListeners((event.values[0] + 360) % 360);
else if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER)
gravityValues = lowPassFilter(getRotationVectorFromSensorEvent(event), gravityValues);
updateOrientation();
else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD)
magneticValues = lowPassFilter(getRotationVectorFromSensorEvent(event), magneticValues);
updateOrientation();
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy)
if (lastAccuracySensorStatus != accuracy)
for (CompassListener compassListener : compassListeners)
compassListener.onCompassAccuracyChange(accuracy);
lastAccuracySensorStatus = accuracy;
private boolean isGyroscopeAvailable()
return sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null;
@SuppressWarnings("SuspiciousNameCombination")
private void updateOrientation()
if (rotationVectorValue != null)
SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVectorValue);
else
// Get rotation matrix given the gravity and geomagnetic matrices
SensorManager.getRotationMatrix(rotationMatrix, null, gravityValues, magneticValues);
final int worldAxisForDeviceAxisX;
final int worldAxisForDeviceAxisY;
// Remap the axes as if the device screen was the instrument panel,
// and adjust the rotation matrix for the device orientation.
switch (windowManager.getDefaultDisplay().getRotation())
case Surface.ROTATION_90:
worldAxisForDeviceAxisX = SensorManager.AXIS_Z;
worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X;
break;
case Surface.ROTATION_180:
worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X;
worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Z;
break;
case Surface.ROTATION_270:
worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Z;
worldAxisForDeviceAxisY = SensorManager.AXIS_X;
break;
case Surface.ROTATION_0:
default:
worldAxisForDeviceAxisX = SensorManager.AXIS_X;
worldAxisForDeviceAxisY = SensorManager.AXIS_Z;
break;
float[] adjustedRotationMatrix = new float[9];
SensorManager.remapCoordinateSystem(rotationMatrix, worldAxisForDeviceAxisX,
worldAxisForDeviceAxisY, adjustedRotationMatrix);
// Transform rotation matrix into azimuth/pitch/roll
float[] orientation = new float[3];
SensorManager.getOrientation(adjustedRotationMatrix, orientation);
// The x-axis is all we care about here.
notifyCompassChangeListeners((float) Math.toDegrees(orientation[0]));
private void notifyCompassChangeListeners(float heading)
for (CompassListener compassListener : compassListeners)
compassListener.onCompassChanged(heading);
lastHeading = heading;
private void registerSensorListeners()
if (isCompassSensorAvailable())
// Does nothing if the sensors already registered.
sensorManager.registerListener(this, compassSensor, SENSOR_DELAY_MICROS);
else
sensorManager.registerListener(this, gravitySensor, SENSOR_DELAY_MICROS);
sensorManager.registerListener(this, magneticFieldSensor, SENSOR_DELAY_MICROS);
private void unregisterSensorListeners()
if (isCompassSensorAvailable())
sensorManager.unregisterListener(this, compassSensor);
else
sensorManager.unregisterListener(this, gravitySensor);
sensorManager.unregisterListener(this, magneticFieldSensor);
private boolean isCompassSensorAvailable()
return compassSensor != null;
/**
* Helper function, that filters newValues, considering previous values
*
* @param newValues array of float, that contains new data
* @param smoothedValues array of float, that contains previous state
* @return float filtered array of float
*/
private float[] lowPassFilter(float[] newValues, float[] smoothedValues)
if (smoothedValues == null)
return newValues;
for (int i = 0; i < newValues.length; i++)
smoothedValues[i] = smoothedValues[i] + ALPHA * (newValues[i] - smoothedValues[i]);
return smoothedValues;
/**
* Pulls out the rotation vector from a SensorEvent, with a maximum length
* vector of four elements to avoid potential compatibility issues.
*
* @param event the sensor event
* @return the events rotation vector, potentially truncated
*/
@NonNull
private float[] getRotationVectorFromSensorEvent(@NonNull SensorEvent event)
if (event.values.length > 4)
// On some Samsung devices SensorManager.getRotationMatrixFromVector
// appears to throw an exception if rotation vector has length > 4.
// For the purposes of this class the first 4 values of the
// rotation vector are sufficient (see crbug.com/335298 for details).
// Only affects Android 4.3
System.arraycopy(event.values, 0, truncatedRotationVectorValue, 0, 4);
return truncatedRotationVectorValue;
else
return event.values;
public static float shortestRotation(float heading, float previousHeading)
double diff = previousHeading - heading;
if (diff > 180.0f)
heading += 360.0f;
else if (diff < -180.0f)
heading -= 360.f;
return heading;
CompassListener.java:
/**
* Callbacks related to the compass
*/
public interface CompassListener
/**
* Callback's invoked when a new compass update occurs. You can listen into the compass updates
* using @link LocationComponent#addCompassListener(CompassListener) and implementing these
* callbacks. Note that this interface is also used internally to to update the UI chevron/arrow.
*
* @param userHeading the new compass heading
*/
void onCompassChanged(float userHeading);
/**
* This gets invoked when the compass accuracy status changes from one value to another. It
* provides an integer value which is identical to the @code SensorManager class constants:
* <ul>
* <li>@link android.hardware.SensorManager#SENSOR_STATUS_NO_CONTACT</li>
* <li>@link android.hardware.SensorManager#SENSOR_STATUS_UNRELIABLE</li>
* <li>@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_LOW</li>
* <li>@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_MEDIUM</li>
* <li>@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_HIGH</li>
* </ul>
*
* @param compassStatus the new accuracy of this sensor, one of
* @code SensorManager.SENSOR_STATUS_*
*/
void onCompassAccuracyChange(int compassStatus);
MainActivity.java:
import android.content.Context;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.WindowManager;
import android.widget.TextView;
import java.util.Locale;
public class MainActivity extends AppCompatActivity
private LocationComponentCompassEngine compassEngine;
private float previousCompassBearing = -1f;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView textView = findViewById(R.id.textView);
CompassListener compassListener = new CompassListener()
@Override
public void onCompassChanged(float targetCompassBearing)
if (previousCompassBearing < 0)
previousCompassBearing = targetCompassBearing;
float normalizedBearing =
LocationComponentCompassEngine.shortestRotation(targetCompassBearing, previousCompassBearing);
previousCompassBearing = targetCompassBearing;
String status = "NO_CONTACT";
switch (compassEngine.getLastAccuracySensorStatus())
case SensorManager.SENSOR_STATUS_NO_CONTACT:
status = "NO_CONTACT";
break;
case SensorManager.SENSOR_STATUS_UNRELIABLE:
status = "UNRELIABLE";
break;
case SensorManager.SENSOR_STATUS_ACCURACY_LOW:
status = "ACCURACY_LOW";
break;
case SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM:
status = "ACCURACY_MEDIUM";
break;
case SensorManager.SENSOR_STATUS_ACCURACY_HIGH:
status = "ACCURACY_HIGH";
break;
textView.setText(String.format(Locale.getDefault(),
"CompassBearing: %f\nAccuracySensorStatus: %s", normalizedBearing, status));
@Override
public void onCompassAccuracyChange(int compassStatus)
;
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
compassEngine = new LocationComponentCompassEngine(windowManager, sensorManager);
compassEngine.addCompassListener(compassListener);
compassEngine.onStart();
@Override
protected void onDestroy()
super.onDestroy();
compassEngine.onStop();
【讨论】:
有了你描述的实现,这个主要的方向方法***.com/a/2131294/1238737是相关的吗? 如果您想显示指标视图,normalizedBearing
为您提供更好的更改。您可以使用hastebin.com/uvukulecim.cpp 来获取您提到的链接所说的主要方向。以上是关于静止不动时使用 TYPE_ROTATION_VECTOR 实现当前的基本方向方法?的主要内容,如果未能解决你的问题,请参考以下文章
cartographer当机器人不动时,同时收到landmark,如何解决定位问题?
unity 2017.1 播放后画面静止不动的原因是?谢谢!