'Azimuth mirrored after remapping axis coordinate system

I am trying to remap a device that has an alternate coordinate system.

Device Axis Oreintation

The sensor is reporting values that are rotated 90° around the X axis. The format is a Quaternion in standard Android Rotation Vector notation. If I use the data unmodified I can hold the device 90° offset and successfully call getOrientation via:

private void updateOrientationFromVector(float[] rotationVector) {
   float[] rotationMatrix = new float[9];
   SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector);
   final int worldAxisForDeviceAxisX = SensorManager.AXIS_X;
   final int worldAxisForDeviceAxisY = SensorManager.AXIS_Z;
   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);
   // Convert radians to degrees
   float azimuth = orientation[0] * 57;
   float pitch = orientation[1] * 57;
   float roll = orientation[2] * 57;
   // Normalize for readability
   if(azimuth < 0) {
     azimuth = azimuth + 360;
   }
  Log.d("Orientation", "Azimuth: " + azimuth + "° Pitch: " + pitch + "° Roll: " + roll + "°);
}

This code works fine for all normal Android devices. If I hold a reference phone in front of me as shown, the data is converted properly and shows my correct bearings. But when I use this test device, I must hold it at the wrong orientation to show me the correct bearings.

I want to pre-process the data from this test device to rotate the axes so the this device matches all other Android devices. This will let the display logic be generic.

Unfortunately I have tried many different techniques and none are working.

First, I tried to use a the Android calls again:

private fun rotateQuaternionAxes(rotationVector :FloatArray) : FloatArray {
    val rotationMatrix = FloatArray(9)
    SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector)
    val worldAxisForDeviceAxisX = SensorManager.AXIS_X
    val worldAxisForDeviceAxisY = SensorManager.AXIS_Z
    val adjustedRotationMatrix = FloatArray(9)
    SensorManager.remapCoordinateSystem(rotationMatrix, worldAxisForDeviceAxisX, worldAxisForDeviceAxisY, adjustedRotationMatrix)
    val axisRemappedData = Quaternion.fromRotationMatrix(adjustedRotationMatrix)
    val rotationData = floatArrayOf(
        axisRemappedData.x,
        axisRemappedData.y,
        axisRemappedData.z,
        axisRemappedData.w
    )
    return rotationData
}

My private Quaternion.fromRotationMatrix is not show here, and came from euclideanspace.com When I pre-process my rotation data with this, the logic works for everything, except north and south are swapped! East and west are correct, and my pitch and roll are correct.

So I decided to follow the suggestions for Rotating a Quaternion on 1-Axis with the following code:

private fun rotateQuaternionAxes(rotationVector :FloatArray) : FloatArray {
    // https://stackoverflow.com/questions/4436764/rotating-a-quaternion-on-1-axis
    // Device X+ is towards power button; Y+ is toward camera; Z+ towards nav buttons
    // So rotate the reported data 90 degrees around X and the axes move appropriately
    val sensorQuaternion: Quaternion = Quaternion(rotationVector[0], rotationVector[1], rotationVector[2], rotationVector[3])
    val manipulationQuaternion = Quaternion.axisAngle(-1.0f, 0.0f, 0.0f, 90.0f)
    val axisRemappedData = Quaternion.multiply(sensorQuaternion, manipulationQuaternion)
    val rotationData = floatArrayOf(
        axisRemappedData.x,
        axisRemappedData.y,
        axisRemappedData.z,
        axisRemappedData.w
    )
    //LogUtil.debug("Orientation Orig: $sensorQuaternion Rotated: $axisRemappedData")
    return rotationData
}

This does the exact same thing! Everything is fine, except north and south are mirrored, leaving east and west correct.

My Quaternion math came from sceneform-android-sdk and I double-checked it against several online sources.

I also tried simply changing my data by just grabbing the same data differently according to Convert quaternion to a different coordinate system.

private fun rotateQuaternionAxes(rotationVector :FloatArray) : FloatArray {
    // No change:
    //val rotationData = floatArrayOf(x_val, y_val, z_val, w_val)
    val x_val = rotationVector[0]
    val y_val = rotationVector[1]
    val z_val = rotationVector[2]
    val w_val = rotationVector[3]
    val rotationData = floatArrayOf(x_val, z_val, -y_val, w_val)
    return rotationData
}

This was not even close. I played with the axes and ended up finding rotationData = floatArrayOf(-z_val, -x_val, y_val, w_val) was had correct pitch and roll, but the azimuth was completely non-functional. So I've abandoned a simple remapping as an option.

Since the Android remapCoordinateSystem and the quaternion math give the same result, they seem mathematically equivalent. And multiple sources indicate they should accomplish what I'm trying to do.

Can any one explain why remapping my axes would swap the north/south? I believe I am getting a quaternion reflection instead of rotation. There is no physical point on the device that tracks the direction it shows.



Solution 1:[1]

Answer

As you said, it looks like you are expecting your data to be on the East-North-Up (ENU) Frame of Reference (FoR) but you are seeing data on an East-Down-North (EDN) FoR. The link you cited to convert quaternion to another coordinate system converts from an ENU to a NDW FoR - which evidently is not what you are looking for.

There are two ways you can solve this. Either use another rotation matrix, or swap your variables. Using another rotation matrix means doing more computation - but if you really want to learn how to do this, you can check out my self-plug introduction to quaternions for reference frame rotations.

The easiest way would be to swap your variables by recognizing that your X axis is not changing, but your expected Y is measured in z' and your expected Z is measured in -y'. Where X,Y,Z are the expected FoR, and x',y',z' are the actual measured FoR. The following "swaps" should allow you to get the same behavior as your other Android devices:

x_expected = x_actual
y_expected = z_actual
z_expected = -y_actual

!!! HOWEVER !!! If your measurements are given in quaternions, then you will have to use a rotation matrix. If your measurements are given as X,Y,Z measurements, you can get away with the swap provided above.

ENU/NED/NDW Notation

East-North-Up and all other similar axes notations are defined by the order of the coordinate system, expressed as X, then Y, and lastly Z, with respect to a Global inertial (static) Frame of Reference. I've defined your expected coordinate system as if you were to lay your phone flat on the ground with the screen of the phone facing the sky and the top of your phone pointing Northward.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 StolenLight