[Android Kotlin] 안드로이드 가속도 센서의 지구계 변환

 


안드로이드 가속도 센서의 지구계 변환

개요

안드로이드에는 다양한 센서가 있다. 센서의 좌표계는 Device 기준이며, 이를 World 기준으로 바꾸려면 추가적인 변환이 필요하다. 다행히도 SensorManagergetRotationMatrix를 통해 World Coordinate 로 바꿔주는 회전행렬을 쉽게 구할 수 있다. 우리는 이를 이용해서 연산만 해주면 된다!

Device Coordinate System World Coordinate System
1 2

센서 관련 내용과 사용하는 방법은 공식 문서에 아주 잘 나와있다.


getRotationMatrix

getRotationMatrix의 prototype을 보면 다음과 같다.

public static boolean getRotationMatrix (float[] R,	// 회전행렬 R을 저장할 공간
                float[] I, 	                        // 회전행렬 I를 저장할 공간
                float[] gravity,                    // TYPE_ACCELEROMETER에서 반환한 값
                float[] geomagnetic)                // TYPE_MAGNETIC_FIELD에서 반환한 값
  • float[9] R : 장치계를 지구계로 변환해 줄 회전행렬
  • float[9] I : 지자기장벡터를 지구계로 변환해 줄 회전행렬
  • float[3] gravity : TYPE_ACCELEROMETER에서 반환한 값
  • float[3] geomagnetic : TYPE_MAGNETIC_FIELD에서 반환한 값

회전행렬을 구하기 위해서는 가속도 센서지자기장 센서 값이 필요하다. RI는 결과 값을 저장할 공간이다.


회전행렬을 구하는 Logic은 공식 문서에 따로 나와있지 않아서 직접 코드를 뜯어 정리해보았다.

image

image

위와 같이 구해진 회전 행렬 과 기존의 가속도 센서 벡터 를 곱하면 World 기준의 가속도 센서 값 을 얻을 수 있다.


Source Code

전체 코드는 여기서 볼 수 있다.

  • AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.igluesmik.android_kotlin_practice">

    //add this line
    <uses-feature android:name="android.hardware.sensor.accelerometer" />

    <application
      ...
      >
    </application>

</manifest>


  • MainActivity.kt
class MainActivity : AppCompatActivity(), SensorEventListener {

    private lateinit var sensorManager: SensorManager

    private var accelerationSensor : Sensor?= null
    private var magneticSensor : Sensor?= null

    private var accelerationData = FloatArray(3)
    private var magneticData = FloatArray(3)
    private var earthData = FloatArray(3)
    private var rotationMatrix = FloatArray(9)

    private var info : String = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initMotionSensor()
    }

    override fun onResume() {
        super.onResume()

        sensorManager.apply {
            registerListener(this@MainActivity, accelerationSensor, SensorManager.SENSOR_DELAY_NORMAL)
            registerListener(this@MainActivity, magneticSensor, SensorManager.SENSOR_DELAY_NORMAL)
        }
    }

    override fun onPause() {
        super.onPause()

        sensorManager.unregisterListener(this)
    }

    private fun initMotionSensor() {
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        sensorManager.apply {
            accelerationSensor = getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
            magneticSensor = getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
        }
    }

    override fun onSensorChanged(event: SensorEvent?) {
        if (event != null) {
            when(event.sensor.type){
                Sensor.TYPE_ACCELEROMETER   -> getAccelerationData(event)
                Sensor.TYPE_MAGNETIC_FIELD  -> getMagneticFieldData(event)
            }
        }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {

    }

    private fun getMagneticFieldData(event: SensorEvent) {
        magneticData = event.values.clone()
    }

    private fun getAccelerationData(event: SensorEvent) {
        accelerationData = event.values.clone()

        SensorManager.getRotationMatrix(rotationMatrix, null, accelerationData, magneticData)

        //get acceleration values converted to a world coordinate system
        earthData[0] = rotationMatrix[0] * accelerationData[0] + rotationMatrix[1] * accelerationData[1] + rotationMatrix[2] * accelerationData[2]
        earthData[1] = rotationMatrix[3] * accelerationData[0] + rotationMatrix[4] * accelerationData[1] + rotationMatrix[5] * accelerationData[2]
        earthData[2] = rotationMatrix[6] * accelerationData[0] + rotationMatrix[7] * accelerationData[1] + rotationMatrix[8] * accelerationData[2]

        info = "Device Coordinate\n"+
                "x = ${accelerationData[0]},  y = ${accelerationData[1]}, z = ${accelerationData[2]}"
        acceDeviceCoordinate.text = info

        info = "Device Coordinate\n"+
                "x = ${earthData[0]},  y = ${earthData[1]}, z = ${earthData[2]}"
        acceWorldCoordinate.text = info
    }

}

위의 코드는 직접 행렬과 벡터를 곱하여 값을 구했는데, android.opengl.MatrixmultiplyMV를 사용하여 연산할 수도 있다.

다만, android.opengl.Matrix의 연산이 column 중심이므로 행렬을 전치한 후 연산해야 한다.
따라서 전치행렬을 저장할 추가 공간이 필요하고, ArrayIndexOutOfBoundsException 오류를 보지 않으려면 배열의 크기도 커야한다.

회전행렬의 전치행렬은 역행렬과 같으므로 android.opengl.Matrix.invertM을 통해 전치행렬을 구할 수 있다.


class MainActivity : AppCompatActivity(), SensorEventListener {

    ...

    private var accelerationData = FloatArray(4)
    private var magneticData = FloatArray(3)
    private var rotationMatrix = FloatArray(16)
    private var invertRotationMatrix = FloatArray(16)
    private var earthData = FloatArray(16)


    private fun getAccelerationData(event: SensorEvent) {
        for(i in 0..2){
            accelerationData[i] = event.values[i]
        }
        accelerationData[3] = 0F

        SensorManager.getRotationMatrix(rotationMatrix, null, accelerationData, magneticData)

        //get acceleration values converted to a world coordinate system
        Matrix.invertM(invertRotationData, 0, rotationData, 0)
        Matrix.multiplyMV(earthData, 0, invertRotationData, 0, accelerationData, 0)

        ...
    }

}

개인적으로는 첫 번째 방법이 더 직관적이고 마음에 든다.



:bookmark: REFERENCE
Android | Sensor
Android | SensorManager
Acceleration from device’s coordinate system into absolute coordinate system