Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

🐾 Android | Kalman vs EMA — 고도/센서 데이터 필터링 비교와 적용기

Billcorea 2025. 12. 9. 15:56

<!doctype html>

🐾 Android | Kalman vs EMA — 고도/센서 데이터 필터링 비교와 적용기

센서 그래프 예시

 

개요 (Intro)

  • 오늘의 목표 / 배경: 위치·고도 데이터(바로미터 + PDR/IMU)를 안정적으로 시각화/동기화하기 위해 Kalman 필터와 EMA(지수평활)의 차이를 비교 분석하고, 프로젝트에 적용할 방향을 정리한다.
  • 해결하려는 문제: 그래프의 NaN 크래시 방지, 스파이크(이상치)와 드리프트에 강한 필터링, 실시간성(응답속도) 유지 간의 균형.
  • 사용한 기술 스택: Kotlin, Jetpack Compose, Vico chart, Coroutines, Wear OS
📅 날짜: 2025.12.09
🎯 목표: Kalman/EMA 특성 비교 및 프로젝트 적용 가이드 정리
🧰 기술: Kotlin, Compose, Vico, Wear OS, Coroutines

문제 정의 (Problem / Motivation)

  • 바로미터(고도) 데이터는 환경/기압 변화에 민감해 노이즈와 스파이크가 잦음.
  • PDR/IMU는 누적 드리프트가 생기기 쉬움(특히 장시간).
  • 그래프에 NaN이 들어가면 마커/그리기 로직에서 크래시 위험(실제 NaN round 예외 경험).
  • 실시간 UI에서는 빠른 반응과 안정적 시각화 둘 다 필요.
// NaN 방어를 위해 앞값 채움(fill-forward)으로 시계열을 안전화한 부분
fun fillForward(arr: FloatArray): List<Float> {
    val out = ArrayList<Float>(arr.size)
    var last = 0f
    var seen = false
    for (i in arr.indices) {
        val v = arr[i]
        if (!v.isNaN()) { last = v; seen = true; out.add(v) }
        else { out.add(if (seen) last else 0f) }
    }
    return out
}

해결 과정 (How I Solved It)

EMA(지수평활) — 간단·저비용 필터

  • 정의: y[n] = α·x[n] + (1-α)·y[n-1]
  • 특징: 구현 매우 쉬움, 레이턴시 낮음, 이상치에는 약함.
// 초보자도 이해하기 쉬운 EMA 필터 — 주석 상세 버전
class EmaFilter(private val alpha: Float) {
    private var state: Float? = null // 이전 출력값(초기엔 없음)

    // 새 입력값 x를 받아 필터링된 값 반환
    fun update(x: Float): Float {
        val s = state
        val out = if (s == null) {
            // 첫 샘플은 기준값으로 사용
            x
        } else {
            // EMA 핵심 — 현재 입력과 이전 출력의 가중 평균
            alpha * x + (1 - alpha) * s
        }
        state = out
        return out
    }

    fun reset() { state = null }
}

Kalman — 상태공간 기반 최적 추정(선형/가우시안 가정)

  • 특징: 모델(A,H), 노이즈(Q,R)로 예측/갱신. 드리프트·노이즈 균형적으로 제어.
  • 장점: 통계적으로 최적(가정 하), 속도/가속 등 상태 추정 가능.
  • 단점: 튜닝 복잡, 계산량 EMA보다 큼.
// 1D 고도용 아주 간단한 Kalman — 주석 상세
class SimpleKalman1D(
    private var q: Float, // 프로세스(모델) 노이즈 공분산 — 모델 불확실성
    private var r: Float  // 측정 노이즈 공분산 — 센서 불확실성
) {
    private var x: Float = 0f // 상태(고도)
    private var p: Float = 1f // 상태 오차 공분산

    fun init(initial: Float, initialP: Float = 1f) {
        x = initial
        p = initialP
    }

    // 예측 단계 — 간단 모델(고정)에서는 p만 늘려 불확실성 반영
    fun predict() {
        p += q
    }

    // 갱신 단계 — 관측 z 반영
    fun update(z: Float): Float {
        val k = p / (p + r)    // 칼만 이득(측정 신뢰도 vs 상태 신뢰도 비율)
        x = x + k * (z - x)    // 상태 보정
        p = (1 - k) * p        // 오차 공분산 업데이트
        return x
    }
}

상보(Complementary) 융합 — 간단 융합법

  • 예: fusedZ = α·pdrZ + (1-α)·baroZ
  • 장점: 직관적인 튜닝, 구현 쉬움.
  • 단점: 노이즈 통계 반영 없음, 이상치 처리 필요.
// 프로젝트에서 사용 중인 융합 아이디어(유사): baro와 ENU zUp을 비율로 합성
val fusedZ = altitudeFusionRatio * enu.zUp + (1 - altitudeFusionRatio) * baro

결과 (Result)

  • 그래프 입력의 NaN 제거(앞값 채움)로 마커 크래시 방지.
  • EMA/상보/칼만의 트레이드오프를 정리해 적용 방향을 확립.
  • 실시간 UI 요구가 큰 경우: EMA 또는 상보 필터부터 적용하고, 필요 시 칼만으로 단계적 고도화.
✅ 실시간성(EMA/상보)과 안정성(칼만)의 균형을 프로젝트 요구에 맞게 선택할 수 있도록 기준을 수립.

느낀 점 / 회고 (Reflection)

  • NaN 같은 ‘형식적 오류’도 시각화 라이브러리에서 큰 크래시 원인이 된다 — 입력 정규화가 최우선.
  • EMA는 UI엔 매우 좋은 출발점. 칼만은 모델·노이즈 튜닝이 핵심 — 로그 기반 정량 튜닝이 필요.
  • 상보 필터는 간단하지만 이상치 방어(스파이크 클리핑/중앙값 필터)와 함께 쓰면 체감 품질이 훨씬 좋아진다.

참고자료 (References)

  • Kalman Filter — Greg Welch & Gary Bishop, "An Introduction to the Kalman Filter"
  • Signal Processing Stack Exchange — EMA/Moving Average 비교 토론
  • Android Sensor Docs — Barometer, SensorManager Delay 등
  • Vico Chart — https://github.com/patrykandpatrick/vico
다음 단계 제안
  1. 바로미터 측정에 중앙값(Median) 또는 MAD 기반 이상치 제거를 추가.
  2. 상보 필터 계수(altitudeFusionRatio)를 프로파일(배터리/표준/고속)에 따라 자동 조정.
  3. 간단한 1D 칼만(고도+속도)로 샘플 러닝 테스트 — Q/R 튜닝 로그(innovation) 기록.
반응형