센서 퓨젼 이미지

칼만 필터와 상보 필터: 센서 IMU의 정확도를 올려보자.

가속도 센서와 자이로 센서(IMU) 값을 따로 써보니, 하나는 단기적으로 덜덜 떨리고 다른 하나는 시간이 지날수록 점점 딴 데로 흘러가는 것을 경험했을 것이다. 이 두 센서의 단점을 보완하고 장점만을 합쳐, 깔끔하고 안정적인 데이터를 만드는 기술이 바로 센서 퓨전이다. 이번 글에서는 센서 퓨전의 대표적인 두 가지 방법, 상보 필터칼만 필터의 원리를 이해하고 실제 코드까지 구현해 본다.

센서 뷰젼

왜 필터가 필요한가? 두 센서의 동상이몽

센서 퓨전 필터를 이해하려면, 먼저 가속도 센서와 자이로 센서가 가진 명확한 한계부터 알아야 한다.

  • 자이로스코프의 배신: 드리프트(Drift) 자이로는 각속도(회전 속도)를 측정한다. 이 값을 시간에 따라 계속 더해가면(적분하면) 현재 각도를 알 수 있다. 단기적인 회전 변화를 매우 정확하게 잡아내지만, 시간이 지날수록 센서 자체의 미세한 오차가 계속 누적되어 실제 각도와 점점 멀어지는 드리프트 현상이 발생한다. 가만히 둬도 혼자서 서서히 돌아가는 셈이다.
  • 가속도계의 한계: 노이즈와 운동 가속도 가속도계는 ‘중력’을 기준으로 절대적인 기울기를 알 수 있다. 드리프트 현상이 없어 장기적으로는 안정적이다. 하지만 진동 같은 미세한 움직임(노이즈)에 매우 민감하게 반응하여 값이 심하게 덜덜 떨린다. 또한, 순수한 움직임에 의한 ‘운동 가속도’와 ‘중력 가속도’를 구분하지 못해, 움직이는 동안에는 기울기 값이 왜곡된다.

그래서 우리는 이 둘의 장점만 쏙쏙 빼서 합쳐야 한다. 이것이 바로 센서 퓨전(Sensor Fusion)이다.

상보 필터(Complementary Filter)

상보 필터는 이름 그대로 두 센서가 서로를 ‘보완’하는 가장 직관적이고 간단한 방법이다. 계산량이 적고 빨라서 마이크로컨트롤러(MCU) 환경에서 아주 인기가 많다.

핵심 원리

원리는 간단하다. ‘두 친구’를 떠올려 보자.

  • 자이로(빠르지만 길치 친구): 단기적인 움직임은 빠르고 정확하지만, 시간이 지나면 엉뚱한 곳으로 간다.
  • 가속도계(느리지만 방향 감각 좋은 친구): 느리고 둔하지만, 중력 덕분에 항상 올바른 방향을 알고 있다.

상보 필터는 빠른 움직임(단기적 변화)은 주로 자이로를 믿고, 느린 움직임(장기적 안정성)은 가속도계를 믿어 자이로의 오차를 계속 보정해주는 방식이다. 기술적으로는 자이로 값에 하이패스 필터(High-pass Filter)를, 가속도계 값에 로우패스 필터(Low-pass Filter)를 적용하여 합치는 것과 같다.

공식과 코드 구현

하나의 각도(예: Pitch)에 대한 상보 필터 공식은 다음과 같다.

angle=α×(angle_{prev} +gyroRate×dt)+(1−α)×accelAngle


각 요소는 다음과 같다.

α (알파) : 가중치. 보통 0.95 ~ 0.98 사이 값을 사용한다. 자이로를 얼마나 더 믿을지 결정한다.
(angle_prev + gyroRate * dt) : 이전 각도에 자이로가 측정한 변화량을 더한, 자이로 기반의 예측 각도.
accelAngle : 가속도계로 계산한 절대 기울기 각도.

이를 코드로 구현하면 다음과 같다.

// 상보 필터 함수
// prev_angle: 필터링 전 각도 (이전 스텝의 필터링된 각도)
// gyro_rate: 자이로스코프 측정 각속도 (degree/s)
// accel_angle: 가속도계로 계산한 각도 (degree)
// dt: 시간 간격 (초)
float complementary_filter(float prev_angle, float gyro_rate, float accel_angle, float dt) {
    const float alpha = 0.98; // 자이로 가중치 (0~1, 1에 가까울수록 자이로를 더 신뢰)

    // 1. 자이로스코프 데이터를 이용한 각도 예측
    // 이전 필터 각도에 자이로로 측정된 각도 변화량을 더한다.
    float gyro_predicted_angle = prev_angle + gyro_rate * dt;

    // 2. 가속도계 데이터와 자이로 예측치를 융합
    // 자이로 예측치에 alpha 가중치를, 가속도계 측정치에 (1-alpha) 가중치를 적용하여 최종 각도를 계산한다.
    float final_angle = alpha * gyro_predicted_angle + (1.0 - alpha) * accel_angle;

    return final_angle;
}

상보필터의 장단점

  • 장점: 매우 가볍고 빠르다. 이해하기 쉽고 구현이 간단하다.
  • 단점: α 가중치가 고정되어 있어, 움직임의 종류(빠른/느린)에 따라 최적화하기 어렵다. 외부 충격에 여전히 취약할 수 있다.

다음으로 칼만필터를 알아보자.

칼만 필터(Kalman Filter)

칼만 필터는 상보 필터보다 훨씬 정교하고 복잡한, 통계 기반의 예측 필터다. 단순히 두 값을 섞는 것을 넘어, 현재 상태를 ‘예측’하고, 측정된 값으로 ‘보정’하는 과정을 반복하며 불확실성을 줄여 최적의 값을 추정해낸다.

핵심 원리

칼만필터는 복잡하지만 유능하고 똑똑하다.

  1. 예측(Prediction) 단계: 항해사는 현재 속도와 방향을 바탕으로 “1초 뒤에는 저기쯤 가 있을 거야”라고 예측한다. (시스템의 이전 상태를 기반으로 현재 상태를 예측)
  2. 갱신(Update) 단계: 이때 GPS로 실제 위치를 측정해본다. 예측한 위치와 실제 측정 위치가 다르다면, 항해사는 생각한다. “내 예측도 100% 정확하진 않고, GPS도 약간의 오차는 있겠지. 둘 중 뭘 더 믿을까?” 이 신뢰도를 칼만 이득(Kalman Gain) 이라는 값으로 계산하여, 예측값과 측정값을 최적으로 조합해 현재 위치를 최종 결정한다.

이 과정을 계속 반복하면서 불확실성(노이즈)을 줄여나가며 가장 확률 높은 상태를 추정하는 것이다.

코드 구현 (개념 이해를 위한 1차원 예시)

실제 IMU용 칼만 필터는 다변수(Multi-variable) 상태 추정을 위해 행렬 계산이 들어가는 등 매우 복잡하다. 여기서는 칼만 필터의 핵심 개념인 ‘예측’과 ‘갱신’ 과정을 이해하기 위한 하나의 각도를 추정하는 간단한 1차원 칼만 필터 코드를 소개한다.

// 1차원 칼만 필터 구조체
typedef struct {
    float Q; // 과정 노이즈 공분산 (시스템 모델의 불확실성, 예측에 대한 불확실성)
    float R; // 측정 노이즈 공분산 (센서 측정값의 불확실성, 센서 자체의 노이즈)
    float P; // 추정 오차 공분산 (현재 추정값의 신뢰도)
    float x; // 추정된 값 (최종적으로 필터링된 각도)
    float K; // 칼만 이득 (예측과 측정 중 무엇을 더 신뢰할지 결정)
} KalmanFilter;

// 칼만 필터 초기화 함수
void kalman_init(KalmanFilter* kf, float Q, float R, float initial_P, float initial_x) {
    kf->Q = Q;
    kf->R = R;
    kf->P = initial_P;
    kf->x = initial_x;
    kf->K = 0;
}

// 칼만 필터 갱신 함수 (예측 및 보정)
// measurement: 센서에서 측정된 현재 값 (예: 가속도계로 계산한 각도)
// dt: 시간 간격 (자이로로 예측하는 부분에 활용될 수 있으나, 1차원 예시에서는 단순화)
float kalman_update(KalmanFilter* kf, float measurement, float gyro_rate, float dt) {
    // 1. 예측(Prediction) 단계
    // 1차원 각도 추정에서는 자이로 데이터를 이용하여 다음 각도를 예측한다.
    kf->x = kf->x + gyro_rate * dt; // 이전 추정값에 자이로 변화량을 더해 예측

    // 예측 단계에서 오차 공분산을 갱신 (불확실성 증가)
    kf->P = kf->P + kf->Q;
    
    // 2. 갱신(Update) 단계
    // 칼만 이득 계산 (얼마나 센서 측정치를 믿을지)
    kf->K = kf->P / (kf->P + kf->R);
    
    // 추정값 보정 (예측값과 측정값을 칼만 이득에 따라 조합)
    kf->x = kf->x + kf->K * (measurement - kf->x);
    
    // 오차 공분산 갱신 (추정값의 신뢰도 향상)
    kf->P = (1 - kf->K) * kf->P;
    
    return kf->x;
}
  • QR 값 (process_noise_covariance, measurement_noise_covariance): 이 값들을 어떻게 설정하느냐에 따라 필터의 성능이 결정된다. Q는 시스템 모델(여기서는 자이로 기반 예측)의 불확실성을, R은 센서 측정값(여기서는 가속도계 기반 각도)의 불확실성을 나타낸다. (시스템을 얼마나 믿을 것인지, 센서를 얼마나 믿을 것인지)

칼만필터의 장단점

  • 장점: 매우 정확하고, 다양한 종류의 노이즈에 강하다. 다중 센서 융합에 뛰어나며, 실시간으로 최적의 추정값을 제공한다.
  • 단점: 계산이 복잡하고 연산량이 많다. 개념을 이해하고 구현하기 어렵다. QR 값 튜닝이 쉽지 않다.

시작 전 필수: 센서 값 보정 (Calibration)

어떤 필터를 사용하든 가장 먼저 해야 할 일은 센서 보정(Calibration)이다. 공장에서 나온 센서는 완벽하지 않아서, 수평 상태에서도 (0, 0, -1g)가 아닌 미세한 오프셋(offset) 값을 출력한다. 이 오차를 제거하지 않으면 필터가 제대로 동작하지 않는다.

간단한 보정 방법 및 코드

  1. 센서를 움직이지 않는 평평한 곳에 둔다.
  2. 일정 시간(수 초) 동안 센서 값을 수백~수천 개 읽는다.
  3. 읽어온 값들의 평균을 구한다. 이 평균값이 바로 ‘오프셋’이다.
  4. 앞으로 센서를 사용할 때마다, 측정된 값에서 이 오프셋을 빼준다.

// 센서 오프셋을 저장할 구조체
typedef struct {
    float accel_x, accel_y, accel_z;
    float gyro_x, gyro_y, gyro_z;
} SensorOffsets;

// 센서 보정 함수
// offsets: 계산된 오프셋을 저장할 포인터
void calibrate_sensors(SensorOffsets* offsets) {
    const int num_samples = 1000; // 수집할 샘플 개수
    float ax_sum = 0, ay_sum = 0, az_sum = 0;
    float gx_sum = 0, gy_sum = 0, gz_sum = 0;

    // 1. 센서를 평평한 곳에 고정하고, 충분한 수의 샘플을 수집한다.
    for (int i = 0; i < num_samples; i++) {
        // read_sensor_data()는 실제 센서 값을 읽어 ImuData 구조체로 반환하는 함수라고 가정
        ImuData data = read_sensor_data(); 
        ax_sum += data.accel_x;
        ay_sum += data.accel_y;
        az_sum += data.accel_z;
        gx_sum += data.gyro_x;
        gy_sum += data.gyro_y;
        gz_sum += data.gyro_z;
        delay(2); // 샘플링 간격 (예: 2ms)
    }

    // 2. 각 축의 평균을 계산하여 오프셋을 저장한다.
    // 자이로 오프셋은 0이어야 할 값의 평균이다.
    offsets->gyro_x = gx_sum / num_samples;
    offsets->gyro_y = gy_sum / num_samples;
    offsets->gyro_z = gz_sum / num_samples;
    
    // 가속도 오프셋은 X, Y축은 0이어야 할 값의 평균, Z축은 -1g여야 할 값의 평균이다.
    offsets->accel_x = ax_sum / num_samples;
    offsets->accel_y = ay_sum / num_samples;
    offsets->accel_z = (az_sum / num_samples) + 1.0f; // Z축은 중력(-1g)을 기준으로 오프셋 계산
                                                    // (만약 센서가 Z+ 방향을 위로 본다면 -1.0f를 더해야 함)
}

// 센서 값에 오프셋을 적용하는 함수 (사용 예시)
void apply_offsets(ImuData* data, const SensorOffsets* offsets) {
    data->accel_x -= offsets->accel_x;
    data->accel_y -= offsets->accel_y;
    data->accel_z -= offsets->accel_z; // 중력 성분 제외하고 순수한 운동 가속도만 남기려면 추가 처리 필요
    
    data->gyro_x -= offsets->gyro_x;
    data->gyro_y -= offsets->gyro_y;
    data->gyro_z -= offsets->gyro_z;
}

마무리하며: 어떤 필터를 선택할까?

상보 필터칼만 필터는 센서 퓨전의 가장 대표적인 두 가지 무기다.

  • 빠른 프로토타입이나 계산 자원이 부족한 환경이라면 상보 필터가 훌륭한 선택이다.
  • 높은 정밀도와 안정성이 요구되는 로봇이나 드론 제어 등에는 칼만 필터가 더 적합하다.

어떤 필터를 선택하든, 가장 중요한 첫걸음은 정확한 센서 보정이라는 점을 잊지 말자. 제대로 보정된 데이터에서 시작해야 필터도 제 역할을 할 수 있다. 프로토타입에는 상보 필터로 빠르게 시작하고, 더 높은 정밀도가 필요할 때 칼만 필터 도입을 고려해 보는 것이 좋은 전략이다.

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다