My augmented reality app needs the compass bearing of the camera view, and there's plenty of examples of getting the direction from the sensormanager.
However I'm finding the resulting value different depending on the phone orientation - landscape rotated to right is about 10 degrees different to landscape rotated to left (difference between ROTATION_0 and ROTATION_180 is less, but still different). This difference is enough to ruin any AR effect.
Is it something to do with calibration? (I'm not convinced I'm doing the figure of 8 thing properly - I've tried various ways I've found on youtube).
Any ideas why there's a difference? Have I messed up on the rotation matrix stuff? I have the option of restricting the app to a single orientation, but it still concerns me that the compass reading still isn't very accurate (even though after filtering it's fairly stable)
public void onSensorChanged(SensorEvent event) {
if (event.accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
return;
}
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) mGravity = event.values;
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) mGeomagnetic = event.values;
if (mGravity != null && mGeomagnetic != null) {
float[] rotationMatrixA = mRotationMatrixA;
if (SensorManager.getRotationMatrix(rotationMatrixA, null, mGravity, mGeomagnetic)) {
float[] rotationMatrixB = mRotationMatrixB;
Display display = getWindowManager().getDefaultDisplay();
int deviceRot = display.getRotation();
switch (deviceRot)
{
// portrait - normal
case Surface.ROTATION_0: SensorManager.remapCoordinateSystem(rotationMatrixA,
SensorManager.AXIS_X, SensorManager.AXIS_Z,
rotationMatrixB);
break;
// rotated left (landscape - keys to bottom)
case Surface.ROTATION_90: SensorManager.remapCoordinateSystem(rotationMatrixA,
SensorManager.AXIS_Z, SensorManager.AXIS_MINUS_X,
rotationMatrixB);
break;
// upside down
case Surface.ROTATION_180: SensorManager.remapCoordinateSystem(rotationMatrixA,
SensorManager.AXIS_X, SensorManager.AXIS_Z,
rotationMatrixB);
break;
// rotated right
case Surface.ROTATION_270: SensorManager.remapCoordinateSystem(rotationMatrixA,
SensorManager.AXIS_MINUS_Z, SensorManager.AXIS_X,
rotationMatrixB);
break;
default: break;
}
float[] dv = new float[3];
SensorManager.getOrientation(rotationMatrixB, dv);
// add to smoothing filter
fd.AddLatest((double)dv[0]);
}
mDraw.invalidate();
}
}
Try this
public void onSensorChanged(SensorEvent event) {
if (event.accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
return;
}
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) mGravity = event.values.clone ();
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) mGeomagnetic = event.values.clone ();
if (mGravity != null && mGeomagnetic != null) {
float[] rotationMatrixA = mRotationMatrixA;
if (SensorManager.getRotationMatrix(rotationMatrixA, null, mGravity, mGeomagnetic)) {
float[] rotationMatrixB = mRotationMatrixB;
SensorManager.remapCoordinateSystem(rotationMatrixA,
SensorManager.AXIS_X, SensorManager.AXIS_Z,
rotationMatrixB);
float[] dv = new float[3];
SensorManager.getOrientation(rotationMatrixB, dv);
// add to smoothing filter
fd.AddLatest((double)dv[0]);
}
mDraw.invalidate();
}
}
You do not need the switch statement, there seems to be a lot of confusion concerning getRotationMatrix, remapCoordinateSystem and getOrientation from stackoverflow questions.
I probably will write a detail explanation of these in the near future.
Hoan's answer is actually incorrect because it doesn't account for the display rotation. This is the correct answer.
Related
I need to be able to measure the change of the accelerometer, not its actual position. I need to move a sprite around with it, and it only works when the device's 'default', if you will, is flat on a table. I need to be able to measure its change. I have heard matrix multiplication and things are needed, but are there any easier ways to do this?
When you register a SensorEventListener, you must Override onSensorChanged(). This returns the change on X, Y, & Z axes. If you want to calculate the rotation matrix from this data, you also need to use the magnetic field sensor. How right? This should get you going:
// Deduced from Accelerometer data
private float[] gravityMatrix = new float[3];
// Magnetic field
private float[] geomagneticMatrix = new float[3];
private boolean sensorReady = false;
private int counter = 0;
public void onSensorChanged(SensorEvent event) {
float[] mIdentityMatrix = new float[16];
mIdentityMatrix[0] = 1;
mIdentityMatrix[4] = 1;
mIdentityMatrix[8] = 1;
mIdentityMatrix[12] = 1;
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
if (counter++ % 10 == 0) {
gravityMatrix = event.values.clone();
sensorReady = true;
}
}
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD)
geomagneticMatrix = event.values.clone();
if (sensorReady)
SensorManager.getRotationMatrix(mRotationMatrix,
mIdentityMatrix, gravityMatrix, geomagneticMatrix);
}
I'm trying to get the direction of the camera in Android. I have code that's working perfectly in portrait (I test it by slowly turning in a circle and looking at updates 1s apart), but it isn't working at all in landscape- The numbers seem to change randomly. It also gets totally out of whack after switching from portrait to landscape. Here's my code
public void onSensorChanged(SensorEvent event) {
switch (event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
accelerometerValues = event.values.clone();
break;
case Sensor.TYPE_MAGNETIC_FIELD:
geomagneticMatrix = event.values.clone();
break;
default:
break;
}
if (geomagneticMatrix != null && accelerometerValues != null) {
float[] R = new float[16];
float[] I = new float[16];
float[] outR = new float[16];
//Get the rotation matrix, then remap it from camera surface to world coordinates
SensorManager.getRotationMatrix(R, I, accelerometerValues, geomagneticMatrix);
SensorManager.remapCoordinateSystem(R, SensorManager.AXIS_X, SensorManager.AXIS_Z, outR);
float values[] = new float[4];
SensorManager.getOrientation(outR,values);
float direction = normalizeDegrees((float) Math.toDegrees(values[0]));
float pitch = normalizeDegrees((float) Math.toDegrees(values[1]));
float roll = normalizeDegrees((float) Math.toDegrees(values[2]));
if((int)direction != (int)lastDirection){
lastDirection = direction;
for(CompassListener listener: listeners){
listener.onDirectionChanged(lastDirection, pitch, roll);
}
}
}
}
Any ideas what I'm doing wrong? I freely admit I don't 100% understand this. I also don't know why Google deprecated the orientation sensor- it seems like a common enough desire.
Did you consider, that when you change from portrait to landscape, accelerometer axes change ? Like Y-axis becomes Z-axis and so on. This might be one source of strange behavior.
I seemed to have solved it, or at least improved it to the point where I know what was the problem. I put in a filter such that instead of delivering a single sensor reading, I'm remembering the last reading and applying a delta to it. Each new sensor point is allowed to add a maximum of 5 degrees. This completely filters out the weird hops, and forces it to converge to a value. I sill see an occasional odd jump, but I figure what I need is a more sophisticated filter. New code:
public void onSensorChanged(SensorEvent event) {
if (event.accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE)
return;
switch (event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
accelerometerValues = event.values.clone();
break;
case Sensor.TYPE_MAGNETIC_FIELD:
geomagneticMatrix = event.values.clone();
break;
}
if (geomagneticMatrix != null && accelerometerValues != null) {
float[] R = new float[16];
float[] I = new float[16];
float[] outR = new float[16];
//Get the rotation matrix, then remap it from camera surface to world coordinates
SensorManager.getRotationMatrix(R, I, accelerometerValues, geomagneticMatrix);
SensorManager.remapCoordinateSystem(R, SensorManager.AXIS_X, SensorManager.AXIS_Z, outR);
float values[] = new float[4];
SensorManager.getOrientation(outR,values);
int direction = filterChange(normalizeDegrees(Math.toDegrees(values[0])));
int pitch = normalizeDegrees(Math.toDegrees(values[1]));
int roll = normalizeDegrees(Math.toDegrees(values[2]));
if((int)direction != (int)lastDirection){
lastDirection = (int)direction;
lastPitch = (int)pitch;
lastRoll = (int)roll;
for(CompassListener listener: listeners){
listener.onDirectionChanged(lastDirection, pitch, roll);
}
}
}
}
//Normalize a degree from 0 to 360 instead of -180 to 180
private int normalizeDegrees(double rads){
return (int)((rads+360)%360);
}
//We want to ignore large bumps in individual readings. So we're going to cap the number of degrees we can change per report
private int filterChange(int newDir){
int change = newDir - lastDirection;
int circularChange = newDir-(lastDirection+360);
int smallestChange;
if(Math.abs(change) < Math.abs(circularChange)){
smallestChange = change;
}
else{
smallestChange = circularChange;
}
smallestChange = Math.max(Math.min(change,5),-5);
return lastDirection+smallestChange;
}
Ive notice when I hold my phone just below the horizon I get > -90. When I begin to tilt the phone so its pointing towards the sky it reflects around -90 so, -88, -90, -88. As its tilted from the ground to the sky.
Has anyone experienced this before. (It doesnt seem to be related to the remapCoordinateSystem). (Ive previously commented it out). When the phone camera is pointed towards the ground the pitch reading is zero. When its pointed towards the ceiling its also zero.
Thanks for any help.
#Override
public void onSensorChanged(SensorEvent event) {
synchronized (MainActivity.this) { // TilteController
switch (event.sensor.getType()) {
case Sensor.TYPE_MAGNETIC_FIELD:
mMagneticValues = event.values.clone();
break;
case Sensor.TYPE_ACCELEROMETER:
mAccelerometerValues = event.values.clone();
break;
}
if (mMagneticValues != null && mAccelerometerValues != null) {
SensorManager.getRotationMatrix(R, null, mAccelerometerValues, mMagneticValues);
Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
int rotation = display.getRotation();
switch (rotation)
{
case Configuration.ORIENTATION_LANDSCAPE:
SensorManager.remapCoordinateSystem(R, SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_MINUS_X, R);//shouldnt be the same R in and out
case Configuration.ORIENTATION_PORTRAIT:
SensorManager.remapCoordinateSystem(R, SensorManager.AXIS_Y, SensorManager.AXIS_X, R);//shouldnt be the same R in and out
}
float[] orientation = new float[3];
SensorManager.getOrientation(R, orientation);
mAzimuth = orientation[0];
mPitch = orientation[1];
mRoll = orientation[2];
dirText.setText("Azimuth, Pitch, Roll || " + radStr(mAzimuth) +", "+ radStr(mPitch) +", "+ radStr(mRoll));
glView.rotate((float)Math.toDegrees(mAzimuth),(float)Math.toDegrees(mPitch),(float)Math.toDegrees(mRoll));
//glView.azimuth=(float)Math.toDegrees(smooth(mAzimuth));
glView.pitch=(float)(Math.toDegrees(smooth(mPitch)));//-90 makes it cenetr
//glView.roll=(float)Math.toDegrees(smooth(-mRoll));
//Log.i("Azimuth, Pitch, Roll", mAzimuth+", "+mPitch+", "+mRoll);
}
}
}
A temporary fix for my needs involves rotating the matrix before calling get orientation.
Matrix.rotateM(R, 0, 90, 0, 1, 0);
How ever you do a complete flip, it experiences the same issue. (This should be solved by adding in Azimuth) But it isnt a brilliant solution.
So if others are reading this and are trying to make the horizon 0 degrees. Rotate the matrix before, as oppose to rotating your display (Im using OpenGL)
Ending up locking the App in landscape and applying the following
SensorManager.getRotationMatrix(R, null, mAccelerometerValues, mMagneticValues);
Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
int rotation = display.getRotation();
SensorManager.remapCoordinateSystem(R, SensorManager.AXIS_X, SensorManager.AXIS_Z, R);//shouldnt be the same R in and out
float[] orientation = new float[3];
SensorManager.getOrientation(R, orientation);
What I want to happen, is to remap the coordinate system, when the phone is turned away from it's "natural" orientation. So that when using a phone, and it's in landscape, it should read the same values, as if it were being held in portrait.
I'm checking to see if rotation equals Surface.ROTATION_90, and if so, then remap the coordinate system.
I admit I don't quite understand how to do it properly, and could use a little guidance.
So, you need to run these two methods:
SensorManager.getRotationMatrix(inR, I, grav, mag);
SensorManager.remapCoordinateSystem(inR, SensorManager.AXIS_Y,SensorManager.AXIS_MINUS_X, outR);
What's required to pass into these methods? I created a new float array, then passed just the orientationsensor data to the mag field, which didn't work. So, I registered both the accelerometer and magnetic field sensors. Fed the data from both of those to the getRotatioMatrix method, and I always get a NullPointerException (even though the JavaDoc says some arguments can be null). I even tried passing data to each argument, and still got a NullPointerException.
My question is, what is the proper data that I need to pass into the getRotationMatrix method?
I found that a very simple way to do this is the one used in the SDK sample AccelerometerPlay.
First you get your display like this, for example in onResume():
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
mDisplay = windowManager.getDefaultDisplay();
Then in onSensorChanged() you can use this simple code:
#Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER)
return;
switch (mDisplay.getRotation()) {
case Surface.ROTATION_0:
mSensorX = event.values[0];
mSensorY = event.values[1];
break;
case Surface.ROTATION_90:
mSensorX = -event.values[1];
mSensorY = event.values[0];
break;
case Surface.ROTATION_180:
mSensorX = -event.values[0];
mSensorY = -event.values[1];
break;
case Surface.ROTATION_270:
mSensorX = event.values[1];
mSensorY = -event.values[0];
break;
}
}
Hope this will help.
this is my code, and it works without NPEs. Note, that I have just one Listener, but you have to register it to listen to both sensors (ACCELEROMETER and MAGNETICFIELD).
private SensorEventListener mOrientationSensorsListener = new SensorEventListener() {
private float[] mR = new float[9];
private float[] mRemappedR = new float[9];
private float[] mGeomagneticVector = new float[3];
private float[] mGravityVector = new float[3];
#Override
public void onSensorChanged(SensorEvent event) {
synchronized(this) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
mGravityVector = Util.exponentialSmoothing(event.values, mGravityVector, 0.2f);
} else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
mGeomagneticVector = Util.exponentialSmoothing(event.values, mGeomagneticVector, 0.5f);
SensorManager.getRotationMatrix(mR, null, mGravityVector, mGeomagneticVector);
SensorManager.remapCoordinateSystem(mR, SensorManager.AXIS_Y,SensorManager.AXIS_MINUS_X, mRemappedR);
}
}
}
The exponentialSmoothing method does some smoothing of the sensor results and looks like that (the alpha value can go from 0 to 1, where 1 means no smoothing at all):
public static float[] exponentialSmoothing(float[] input, float[] output, float alpha) {
for (int i=0; i<input.length; i++) {
output[i] = output[i] + alpha * (input[i] - output[i]);
}
return output;
}
As for the synchronized bit -- I'm not sure that it is needed, just read it somewhere and added it.
I'm having some troubles implementing an augmented reality app for android and I'm hoping that someone could help me. (Sorry for my English...)
Basically I'm getting values from the accelerometer and mangetic field sensors, then as I read remapCoordinatessystem(inR, AXIS_X, AXIS_Z, outR)...and eventually I getOrientation...
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
mGravity = event.values;
}
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
mGeomagnetic = event.values;
}
if (mGravity != null && mGeomagnetic != null) {
float R[] = new float[9];
float I[] = new float[9];
boolean success = SensorManager.getRotationMatrix(R, I, mGravity, mGeomagnetic);
float outR[] = new float[9];
SensorManager.remapCoordinateSystem(R, SensorManager.AXIS_X, SensorManager.AXIS_Z, outR);
if (success) {
float orientation[] = new float[3];
SensorManager.getOrientation(outR, orientation);
// Here I pass the orientation values to the object, which is render by the min3d framework
}
}
}
Am I getting the values correct? Or I need to transform them into degrees?¿ I'm lost...
Then I rotate my 3D object with the values I have read from the sensors... but the obejct it's not moving at all.
public void updateScene() {
objModel.rotation().y = _orientation[2];
objModel.rotation().x = _orientation[1];
objModel.rotation().z = _orientation[0];
}
OpenGL is not my friend...so I'm not sure I'm transforming correctly... which is the order of the rotation axis or it doesn't matter... and which value from orientation should correspond to the axis of the 3D object loaded by Min3D?
If this is not the path I must follow... could someone guide me to the correct one please? It's been a few weeks fighting with this.
Thank you so much... (StackOverflow lover)
I had an issue with
mGeomagnetic = event.values;
you should write
mGeomagnetic = event.values.clone();
instead