안드로이드 가속도 센서의 지구계 변환
개요
안드로이드에는 다양한 센서가 있다. 센서의 좌표계는 Device 기준이며, 이를 World 기준으로 바꾸려면 추가적인 변환이 필요하다. 다행히도 SensorManager
의 getRotationMatrix
를 통해 World Coordinate 로 바꿔주는 회전행렬을 쉽게 구할 수 있다. 우리는 이를 이용해서 연산만 해주면 된다!
Device Coordinate System | World Coordinate System |
---|---|
센서 관련 내용과 사용하는 방법은 공식 문서에 아주 잘 나와있다.
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
에서 반환한 값
회전행렬을 구하기 위해서는 가속도 센서 와 지자기장 센서 값이 필요하다. R
과 I
는 결과 값을 저장할 공간이다.
회전행렬을 구하는 Logic은 공식 문서에 따로 나와있지 않아서 직접 코드를 뜯어 정리해보았다.
위와 같이 구해진 회전 행렬 과 기존의 가속도 센서 벡터 를 곱하면 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.Matrix
의 multiplyMV
를 사용하여 연산할 수도 있다.
다만, 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)
...
}
}
개인적으로는 첫 번째 방법이 더 직관적이고 마음에 든다.
REFERENCE
Android | Sensor
Android | SensorManager
Acceleration from device’s coordinate system into absolute coordinate system