본문으로 바로가기


안녕하세요 PEACE-에요.

안드로이드 스터디 [여덟 번째] 글이네요.


오늘은 2017.04.10에 포스팅했던 '안드로이드 자이로스코프 센서 가속도계 센서의 한계점과 해결방안 모색:1차 상보필터' 와 이어지는 내용으로 안드로이드 모션 센서를 통해 1차 상보필터를 적용해보았던 내용에 대한 내용입니다. 블로그 내 다른 포스팅을 참고해주세요.

참고 - 이전 포스팅 : http://mailmail.tistory.com/4

참고 - 자이로스코프 관련 포스팅 : http://mailmail.tistory.com/3

참고 - 가속도계 관련 포스팅 : http://mailmail.tistory.com/2






1. 자이로스코프 센서와 가속도계 센서의 한계점


자이로스코프 센서는 회전각을 구하는 과정에 적분 과정이 있었는데 이때 적분 오차가 생겼고, 회전각 계산 횟수가 증가할수록 오차 누적에 의한 회전각 드리프트 현상이 발생했다. 가속도계 센서에서는 본래 가속도 특성이 가미된 탓인지 빠른 회전과 빠른 방향 변화에 대해 비정상적인 값을 추출하는 현상이 나타났다. 하지만 이러한 단점이 있는 대신 '자이로스코프'는 운동 중 비교적 안정된 값의 변화도를 보였으며, '가속도계'는 처음과 끝에서 일정한 값(움직이기 전과 움직이고 난 후엔 가속도가 0)을 유지하는 특성이 있어 '상보필터'를 통해 둘의 장점을 적절히 융합는 방법에 대해 얘기했다.




2. 1차 상보필터 적용


1차 상보필터의 블록선도와 수식을 나타내었다.



[그림 1] 1차 상보필터 블록선도


[그림 1]은 1차 상보필터링 과정을 블록선도로 나타낸 것이다. 이를 수식으로 나타내면 아래와 같다.




위 식은 [그림 1]의 블록선도를 수식으로 나타낸 것이다. '세타 f'는 상보필터를 통해 얻어낸 각이고 '닷 세타 g'는 각속도를 나타내며 '세타 c'는 가속도계 센서로부터 얻은 회전각을 의미한다. 즉 자이로스코프 센서에 대해서는 '고역 통과 필링' 하였고 가속도계 센서에 대해서는 '저역 통과 필터링' 한 것이다. 위 수식은 좀 더 다듬으면 아래와 같다.




'a'는 필터계수인데 이를 조정하여 필터링된 회전각을 참 값에 가깝에 만들어주는 작업이 필요하다. 사실 프로젝트를 하시는 분들을 보면 센서와 인코더를 직접 구매해 'a'를 정확하게 구하곤하지만 스마트폰에 내장된 모션 센서를 측정하는데 인코더를 사용하고싶진 않았다. 최종적으로 위 식을 안드로이드 소스코드에 적용하여 보정된 회전각을 구하였다. 소스코드 샘플은 아래와 같다.

/**

* 1st complementary filter.
* mGyroValuess : 각속도 성분.
* mAccPitch : 가속도계를 통해 얻어낸 회전각.

* temp와 pitch는 전역 변수

* dt는 상보필터링 주기

* a는 1차 상보필터의 필터계수(임의의 조정이 필요)
*/
temp = (1/a) * (mAccPitch - pitch) + mGyroValues[1];
pitch = pitch + (temp*dt);


** 자이로스코프와 가속도계 센서가 동시에 SensorEventListener에 들어올 수 없는 한계점 때문에 스위칭을 하여 두 센서에 모두 새로운 값이 들어왔을 때 필터링 하도록 코드를 구성했다.




3. 값의 비교


우선 비교를 하기에 앞서 인코더를 이용해 정확한 참 값 비교를 한 것이 아니라 자이로스코프와 가속도계의 변화에 대한 비교이기 때문에 정확한 측정이라고 말할 수 없음을 말하려한다. 스마트폰의 움직임을 통해 얻어낸 회전각(pitch)을 온라인 무료 매트랩 사이트를 이용하여 그래프로 나타내어 보았다.

[그림 2] 필터링된 회전각 비교


우선 필터계수 a의 값은 0.2로 설정했다. 가속도계의 회전각이 0에서 시작되지 않는 이유는 스마트폰 내부에 부착된 가속도계 센서에 오차때문이라 생각한다. 무시해도 된다. 우선 가장먼저 확인하고 싶었던 부분은 '회전각 드리프트'에 관한 부분이다. 다행히 드리프트 현상은 없었다. 그 다음으로 확인하고 싶었던 부분은 '가속도계의 단점을 얼마나 줄였는가?'였다. 위 그래프는 스마트폰을 비교적 부드럽게 회전했기 때문에 값이 튀는 현상이 나타나지 않았지만 다른 실험에서는 조금씩 나타났다. 이를 제어하기 위해 a값의 변화를 주었지만 a값의 변화는 최종적으로 회전각의 변화폭에 영향을 많이 주었다. 녹색 그래프를 보면 초반에는 회전각이 자이로스코프를 거의 비슷하게 따라간다. 또 다른 변화를 보기 위해 100회 정도부터는 좀 더 빠르게 회전했다. 이때부터는 자이로스코프, 가속도계를 따라가지 못하며 회전각의 폭이 줄어든 것을 알 수 있다. 이는 3가지의 그래프의 최고점과 최소점을 보고 딜레이가 있기 때문이라는걸 알아냈다. 측정 횟수 160~180부분을 보면 아래로 내려온 3개의 그래프 중 녹색 그래프의 최소점이 가장 뒤에있으며 그 부분에는 가속도와 자이로스코프가 이미 감소하고 있는 추세를 보임을 알 수 있다. 결론적으로 '자이로스코프의 드리프트 현상이 제거됐으며, 가속도계의 값 이탈에 대한 부분이 완화됐지만 빠른 회전에 즉각 변화하지 못하는 '딜레이 현상이 있다'라고 판단했다 . 딜레이 현상은 계속해서 생각해봐야 할 문제이며, 가능하면 이 글을 보시는 분들과 함께 해결하고싶다.




4. 구현 화면 및 소스 코드



구현 화면


[그림 2] 필터링된 회전각 비교




소스 코드


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="peace.js_sensorsample.MainActivity"
android:gravity="center">

<TextView
android:id="@+id/tv_roll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20dp"
android:text="roll" />
<TextView
android:id="@+id/tv_pitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20dp"
android:text="pitch"
android:layout_below="@+id/tv_roll"
android:layout_alignStart="@+id/tv_roll" />

<Button
android:id="@+id/filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="filter"
android:layout_above="@+id/tv_roll"
android:layout_alignStart="@+id/tv_roll"
android:layout_marginBottom="16dp" />

</RelativeLayout>



MainActivity.class

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

/*Wizets*/
private TextView tv_roll, tv_pitch;

/*Used for Accelometer & Gyroscoper*/
private SensorManager mSensorManager = null;
private UserSensorListner userSensorListner;
private Sensor mGyroscopeSensor = null;
private Sensor mAccelerometer = null;

/*Sensor variables*/
private float[] mGyroValues = new float[3];
private float[] mAccValues = new float[3];
private double mAccPitch, mAccRoll;

/*for unsing complementary fliter*/
private float a = 0.2f;
private static final float NS2S = 1.0f/1000000000.0f;
private double pitch = 0, roll = 0;
private double timestamp;
private double dt;
private double temp;
private boolean running;
private boolean gyroRunning;
private boolean accRunning;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

tv_roll = (TextView)findViewById(R.id.tv_roll);
tv_pitch = (TextView)findViewById(R.id.tv_pitch);

mSensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
userSensorListner = new UserSensorListner();
mGyroscopeSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
mAccelerometer= mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);

findViewById(R.id.filter).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

/* 실행 중이지 않을 때 -> 실행 */
if(!running){
running = true;
mSensorManager.registerListener(userSensorListner, mGyroscopeSensor, SensorManager.SENSOR_DELAY_UI);
mSensorManager.registerListener(userSensorListner, mAccelerometer, SensorManager.SENSOR_DELAY_UI);
}

/* 실행 중일 때 -> 중지 */
else if(running)
{
running = false;
mSensorManager.unregisterListener(userSensorListner);

}
}
});
}

/**
* 1차 상보필터 적용 메서드 */
private void complementaty(double new_ts){

/* 자이로랑 가속 해제 */
gyroRunning = false;
accRunning = false;

/*센서 값 첫 출력시 dt(=timestamp - event.timestamp)에 오차가 생기므로 처음엔 break */
if(timestamp == 0){
timestamp = new_ts;
return;
}
dt = (new_ts - timestamp) * NS2S; // ns->s 변환
timestamp = new_ts;

/* degree measure for accelerometer */
mAccPitch = -Math.atan2(mAccValues[0], mAccValues[2]) * 180.0 / Math.PI; // Y 축 기준
mAccRoll= Math.atan2(mAccValues[1], mAccValues[2]) * 180.0 / Math.PI; // X 축 기준

/**
* 1st complementary filter.
* mGyroValuess : 각속도 성분.
* mAccPitch : 가속도계를 통해 얻어낸 회전각.
*/
temp = (1/a) * (mAccPitch - pitch) + mGyroValues[1];
pitch = pitch + (temp*dt);

temp = (1/a) * (mAccRoll - roll) + mGyroValues[0];
roll = roll + (temp*dt);

tv_roll.setText("roll : "+roll);
tv_pitch.setText("pitch : "+pitch);

}

public class UserSensorListner implements SensorEventListener{

@Override
public void onSensorChanged(SensorEvent event) {
switch (event.sensor.getType()){

/** GYROSCOPE */
case Sensor.TYPE_GYROSCOPE:

/*센서 값을 mGyroValues에 저장*/
mGyroValues = event.values;

if(!gyroRunning)
gyroRunning = true;

break;

/** ACCELEROMETER */
case Sensor.TYPE_ACCELEROMETER:

/*센서 값을 mAccValues에 저장*/
mAccValues = event.values;

if(!accRunning)
accRunning = true;

break;

}

/**두 센서 새로운 값을 받으면 상보필터 적용*/
if(gyroRunning && accRunning){
complementaty(event.timestamp);
}

}

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) { }
}
}



# 마치면서

필터관련 공부를 하면서 어려움이 많았는데 많은 도움을 주신 'PinkWink'님 감사합니다.

'PinkWink'님의 블로그 : http://pinkwink.kr/

















 댓글공감은 환영입니다.