I'm getting my phone orientation with help of
SensorManager.getOrientation
but the results are very unstable, something like +-8 degrees, is there some good way fo filtering the results?
this is how I get the values:
public void onSensorChanged(SensorEvent event)
{
switch (event.sensor.getType ()){
case Sensor.TYPE_ACCELEROMETER:
aValues = event.values.clone();
break;
case Sensor.TYPE_MAGNETIC_FIELD:
mValues = event.values.clone();
break;
}
float[] R = new float[16];
float[] orientationValues = new float[3];
if( aValues == null || mValues == null )
return;
if( !SensorManager.getRotationMatrix (R, null, aValues, mValues) )
return;
float[] outR = new float[16];
SensorManager.remapCoordinateSystem(R, SensorManager.AXIS_Z, SensorManager.AXIS_MINUS_X, outR);
SensorManager.getOrientation (outR, orientationValues);
orientationValues[0] = (float)Math.toDegrees (orientationValues[0]);
orientationValues[1] = (float)Math.toDegrees (orientationValues[1]);
orientationValues[2] = (float)Math.toDegrees (orientationValues[2]);
}
Just buffer the values by calculating the average (there are many different calculation possibilities: http://en.wikipedia.org/wiki/Average#Types ). Also consider that the more you buffer the slower the app will react to changes!
Related
I am working on an Android app wherein I want to scroll a large image horizontally. I used the accelerometer (Sensor.TYPE_ACCELEROMETER) and magnetic field (Sensor.TYPE_MAGNETIC_FIELD) data to get the angle of rotation. This data being to frequent infested with noise I am not able to implement a smooth motion effect.
#Override
public void onSensorChanged(SensorEvent event) {
switch (event.sensor.getType()) {
case Sensor.TYPE_MAGNETIC_FIELD:
mags = event.values.clone();
break;
case Sensor.TYPE_ACCELEROMETER:
accels = event.values.clone();
break;
}
if (mags != null && accels != null) {
gravity = new float[16];
boolean success = SensorManager.getRotationMatrix(gravity, null, accels, mags);
if (success) {
float[] outGravity = new float[16];
SensorManager.remapCoordinateSystem(gravity, SensorManager.AXIS_X, SensorManager.AXIS_Z, outGravity);
SensorManager.getOrientation(outGravity, values);
rollingAverage[0] = roll(rollingAverage[0], values[0]);
rollingAverage[1] = roll(rollingAverage[1], values[1]);
rollingAverage[2] = roll(rollingAverage[2], values[2]);
azimuth = Math.toDegrees(values[0]);
pitch = Math.toDegrees(values[1]);
roll = Math.toDegrees(values[2]);
mags = null;
accels = null;
double diffRoll = lastRoll - roll;
double diffPitch = lastPitch - pitch;
long curTime = System.currentTimeMillis();
if (Math.abs(diffRoll) >= 2) {
if (diffRoll > 0)
imageView.panLeft();
else
imageView.panRight();
lastRoll = roll;
}
}
}
}
Any ideas on achieving this using other methods?
You have to implement sensor fusion techniques based on Kalman filter or other filters. You can use open source libraries if needed. Refer this bitbucket repository. If you want to do yourself, read the tutorial.
For an application I'm making, I need to have a camera and a compass. The application is set to be at landscape mode in the manifest.
First I've implemented the compass. As suggested in Android Developers, I used two sensors - Accelerometer and Magnetic Field. This is how I've done it:
I have my activity implement SensorEventListener. In onCreate() I initialize my sensorManager using:
sManager = (SensorManager) getSystemService(SENSOR_SERVICE);
I register my listeners in onResume() like so:
sManager.registerListener(this, sManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),SensorManager.SENSOR_DELAY_NORMAL);
sManager.registerListener(this, sManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),SensorManager.SENSOR_DELAY_NORMAL);
and of course unregister them in onPause().
I don't use onAccuracyChanged(). this is what I do in onSensorChanged():
#Override
public void onSensorChanged(SensorEvent event) {
switch (event.sensor.getType()) {
case Sensor.TYPE_MAGNETIC_FIELD:
mags = event.values.clone();
break;
case Sensor.TYPE_ACCELEROMETER:
accels = event.values.clone();
break;
}
if (mags != null && accels != null) {
gravity = new float[9];
magnetic = new float[9];
SensorManager.getRotationMatrix(gravity, magnetic, accels, mags);
float[] outGravity = new float[9];
float inclination = (float) Math.acos(gravity[8]);
if (inclination < Math.toRadians(25)
|| inclination > Math.toRadians(155)) {
// device is close to flat. Remap for landscape.
SensorManager.remapCoordinateSystem(gravity, SensorManager.AXIS_Y,SensorManager.AXIS_MINUS_X, outGravity);
SensorManager.getOrientation(outGravity, values);
} else {
// device is not flat. Remap for landscape and perpendicular
SensorManager.remapCoordinateSystem(gravity, SensorManager.AXIS_X,SensorManager.AXIS_Z, outGravity);
SensorManager.getOrientation(outGravity, values);
}
azimuth = Math.round(Math.toDegrees(values[0]));
}
}
As you can see, I differentiate between when the phone is lying flat on the table, and when the user holds it (as you would when taking a picture). When I use this code alone, everything works great more or less. I'm getting correct azimuth values both when phone is lying on the table and when holding it perpendicular to the table (about 5-10 degrees difference, but I can live with that).
The problem starts when adding the camera preview to the application.
I have my activity implement SurfaceHolder.Callback. I initialize my camera in onCreate():
SurfaceView cameraView = (SurfaceView)findViewById(R.id.camera_view);
surfaceHolder = cameraView.getHolder();
surfaceHolder.addCallback(this);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
This is how I implement the interface:
#Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
camera = Camera.open();
camera.setDisplayOrientation(0);
}
#Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
if (isCameraOn) {
camera.stopPreview();
isCameraOn = false;
}
if (camera != null) {
try {
camera.setPreviewDisplay(surfaceHolder);
camera.startPreview();
isCameraOn = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
#Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
camera.stopPreview();
camera.release();
camera = null;
}
When I add the camera code to my project, and show the camera on the phone's screen, my sensors dont work properly when phone is perpendicular suddenly. If phone is lying flat on the table, the azimuth values I'm getting are correct. When phone is being held perpendicular to the table, my azimuth values are off by about 40 degrees (though stable).
I've tried looking for a solution (both by myself and online), but so far my efforts were in vain. I would love to get some direction on how to tackle this problem.Thanks!
First TYPE_MAGNETIC_FIELD sensor will not available in all devices.
You can use TYPE_ACCELEROMETER sensor alone to accomplish your requirement.
Retrieve accelerometer sensor
Sensor accelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
Just compare and copy values when sensor change event call
#Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER)
mGravity = event.values;
}
Then you can use below function to get sensor values of all axis.
public int[] getDeviceAngles() {
float[] g = mGravity.clone();
double normOfG = Math.sqrt(g[0] * g[0] + g[1] * g[1] + g[2] * g[2]);
// Normalize the accelerometer vector
g[0] = (float) (g[0] / normOfG);
g[1] = (float) (g[1] / normOfG);
g[2] = (float) (g[2] / normOfG);
int x = (int) Math.round(Math.toDegrees(Math.atan2(g[1], g[0])));
int pitch = (int) Math.round(Math.toDegrees(Math.atan2(g[1], g[2])));
int rollValue = (int) Math.round(Math.toDegrees(Math.atan2(g[2], g[0])));
int pitchValue = pitch * -1;
int[] values = new int[3];
values[0] = x;
values[1] = pitchValue;
values[2] = rollValue;
//values contains: azimut, pitch and roll
return values;
}
I am trying out to detect a rotation, not a screen orientation change.
I only got a false on getRotationMatrix().
I found a solution here but it didn't work for me.
#Override
public void onSensorChanged(SensorEvent event) {
// This method will be called when the accelerometer values are changing.
if (event == null || event.values.length == 0) throw new IllegalArgumentException();
else {
// Handle the events for which we registered
switch (event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
Log.i(TAG, "detected Accelerometer");
mValuesAccel = event.values;
break;
case Sensor.TYPE_MAGNETIC_FIELD:
Log.i(TAG, "detected Magneticfield");
mValuesMagnet = event.values;
break;
}
Log.i(TAG, ""+ SensorManager.getRotationMatrix(mRotationMatrix, null, mValuesAccel, mValuesMagnet));
SensorManager.getOrientation(mRotationMatrix, mValuesOrientation);
mRotationListener.onChange(mValuesOrientation);
}
}
Check if your device has a compass.
If there's no compass then your mValuesMagnet is always empty and as a result getRotationMatrix returns false.
In my 2d game I have the following code which is responsible for game entity control (flying plane). It all seems to be working fine when it comes for phones, but unfortunately I've getting some information that on Android tablets the steering is completely unreliable (axis are messed up, or it doesn't work at all). Unfortunately I don't have a tablet of my own, so I cannot investigate it closer. So.. what's wrong with the following code? (for the clarity I put only the code related to sensors)
// ...
private float[] accelerometerValues;
private float[] magneticFieldValues;
private float[] R;
private float[] I;
private float[] outR;
private float[] sensorValues;
private Sensor accelerometer;
private Sensor magneticField;
// ...
// ... sensor initialization
sensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);
if(sensorManager == null)
return;
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
magneticField = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
// ...
// ... onPause() sensor is being unregistered
public void onResume() {
if(!sensorManager.registerListener(sensorListener, accelerometer, SensorManager.SENSOR_DELAY_GAME) ||
!sensorManager.registerListener(sensorListener, magneticField, SensorManager.SENSOR_DELAY_GAME))
// ...
// ...
sensorListener = new SensorEventListener() {
public void onAccuracyChanged(Sensor arg0, int arg1) {
}
public void onSensorChanged(android.hardware.SensorEvent event) {
synchronized(InputMgr.this) {
switch(event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
System.arraycopy(event.values, 0, accelerometerValues, 0, 3);
break;
case Sensor.TYPE_MAGNETIC_FIELD:
System.arraycopy(event.values, 0, magneticFieldValues, 0, 3);
break;
}
}
}
};
// ...
// used somewhere in the game
public void getSensorValues(float values[]) {
synchronized(InputMgr.this) {
SensorManager.getRotationMatrix(R, I, accelerometerValues, magneticFieldValues);
SensorManager.remapCoordinateSystem(R, SensorManager.AXIS_Y, SensorManager.AXIS_X, outR);
SensorManager.getOrientation(outR, sensorValues);
System.arraycopy(sensorValues, 0, values, 0, sensorValues.length);
}
}
This wont cheer you up much but I have a similar problem. I can see a TYPE_MAGNETIC_FIELD sensor, I can add a an event listener to it but I never get any data from it. Other sensors work fine. This is on a Galaxy Tab 7.
onSensorChanged(SensorEvent event) never fires a case Sensor.TYPE_MAGNETIC_FIELD:
As such I get no data from the Magnetic Field Sensor.
if you solve it let us know =)
On devices whose default orientation is landscape (-> most tablets), the sensor values are kind of 'wrong' (I don't know why). So you need to catch those devices and remap your Rotation Matrix.
To check whether the matrix needs to be remapped, you can use this code:
public boolean needToRemapOrientationMatrix;
// compute once (e.g. in onCreate() of your Activity):
Display display = ((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
int orientation;
if(display.getWidth() < display.getHeight()) orientation = Configuration.ORIENTATION_PORTRAIT;
else if(display.getWidth() > display.getHeight()) orientation = Configuration.ORIENTATION_LANDSCAPE;
else orientation = Configuration.ORIENTATION_SQUARE;
int rotation = display.getRotation();
needToRemapOrientationMatrix =
(orientation==Configuration.ORIENTATION_LANDSCAPE && (rotation==Surface.ROTATION_0 || rotation==Surface.ROTATION_180)) ||
(orientation==Configuration.ORIENTATION_PORTRAIT && (rotation==Surface.ROTATION_90 || rotation==Surface.ROTATION_270));
And when you read the sensor values, remap the matrix if needed:
public void getSensorValues(float values[]) {
synchronized(InputMgr.this) {
SensorManager.getRotationMatrix(R, I, accelerometerValues, magneticFieldValues);
if(needToRemapOrientationMatrix)
SensorManager.remapCoordinateSystem(R, SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_X, R);
SensorManager.remapCoordinateSystem(R, SensorManager.AXIS_Y, SensorManager.AXIS_X, outR);
SensorManager.getOrientation(outR, sensorValues);
System.arraycopy(sensorValues, 0, values, 0, sensorValues.length);
}
}
This worked for me, I hope it helps.
i made my own application in Android that use compass and accelerometer sensors to display the degrees of rotation and inclination of my device. I initialized all the listener and objects i needed (i followed some tutorials), and now i can catch the degrees as i wished. The problem is that the measures that sensors return aren't accurate. I mean even if i try to round the values of degrees i catch from the sensor, they oscillate between -/+ 7 (or 8) degrees every fraction of a second, even if i stay in a grass field away from any disturbing source. What i want to have is a accurate measure of the degrees, something like a method to round the values i recieve from the sensors.
float[] mags = null;
float[] accels = null;
float[] R = new float[matrix_size];
float[] outR = new float[matrix_size];
float[] I = new float[matrix_size];
float[] values = null;
private void startSensor() {
sensorMan.registerListener(this, sensorMan.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD), SensorManager.SENSOR_DELAY_UI);
sensorMan.registerListener(this, sensorMan.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_UI);
}
#Override
public void onSensorChanged(SensorEvent event) {
if (event.accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
return;
}
switch (event.sensor.getType()) {
case Sensor.TYPE_MAGNETIC_FIELD:
mags = event.values.clone();
break;
case Sensor.TYPE_ACCELEROMETER:
accels = event.values.clone();
break;
}
if (mags != null && accels != null) {
SensorManager.getRotationMatrix(R, I, accels, mags);
// Correct if screen is in Landscape
SensorManager.remapCoordinateSystem(R, SensorManager.AXIS_X,
SensorManager.AXIS_Z, outR);
SensorManager.getOrientation(outR, values);
azimuth = (float) Math.round((Math.toDegrees(values[0]))*7)/7;
azimuth = ( azimuth + 360)%360;
//here is inclination. The problem is just the same with compass
//inclination=-Math.round((float) (values[1]*(360/(2*Math.PI))));
//other code to update my view
//in azimuth i have the degree value. It changes continuously
//even if i aim still the same direction
}
}
See my answer here: Smoothing data from a sensor
I run this filter on both the accelerometer and magetometer event values before passing them to SensorManager.getRotationMatrix(). I think this algorithm has the advantage of not having to keep a large array of historic values, just the prior low-pass output array.
The algorithm was derived from this Wikipedia entry: http://en.wikipedia.org/wiki/Low-pass_filter#Algorithmic_implementation
What you're seeing is the real thing- the orientation sensors on most phones are only good enough to give you a rough compass heading.
If you want to smooth the displayed value out so it gives you something that's doesn't appear to change randomly I recommend implementing a http://en.wikipedia.org/wiki/Moving_average or other smoothing filter in Java on that orientation result.
For the highest performance you could write the filter using the NDK and use the Boost Accumulators library: http://www.boost.org/doc/libs/1_46_1/doc/html/accumulators.html
I did this using a Kalman filter from here:
Greg Czerniak's Website
I'm sending data to a udp port and smoothing it on the PC using python.
But I guess you can find a Kalman filter implementation for java/android out there.