私は GPS データを扱っており、毎秒値を取得し、現在の位置を地図上に表示しています。問題は、時々 (特に精度が低い場合) 値が大きく変化し、現在の位置がマップ内の離れたポイント間を「ジャンプ」することです。
これを回避するのに十分簡単な方法について疑問に思っていました。最初のアイデアとして、特定のしきい値を超える精度で値を破棄することを考えましたが、他にもっと良い方法があると思います。プログラムがこれを実行する通常の方法は何ですか?
まさにこの状況に使用できる単純なカルマン フィルターを次に示します。これは、私が Android デバイスで行ったいくつかの作業から生まれました。
一般的なカルマン フィルター理論はベクトルの推定に関するものであり、推定の精度は共分散行列によって表されます。ただし、Android デバイスで位置を推定する場合、一般的な理論は非常に単純なケースに帰着します。Android 位置情報プロバイダーは、メートル単位で測定された単一の数値として指定される精度とともに、緯度と経度として位置を提供します。これは、共分散行列の代わりに、カルマン フィルターの位置が 2 つの数値で測定される場合でも、カルマン フィルターの精度を 1 つの数値で測定できることを意味します。また、緯度、経度、およびメートルが実質的にすべて異なる単位であるという事実は無視できます。これは、スケール係数をカルマン フィルターに入れてそれらをすべて同じ単位に変換すると、
現在の位置の最良の推定値が最後に知られている位置であると想定しているため、コードを改善することができます。また、誰かが移動している場合は、Android のセンサーを使用してより正確な推定値を生成できるはずです。このコードには、1 秒あたりのメートル数で表される 1 つの自由パラメーター Q があり、新しい位置推定がない場合に精度が低下する速さを表します。Q パラメータが高いほど、精度の減衰が速くなります。カルマン フィルターは一般に、予想よりも少し早く精度が低下する場合にうまく機能します。そのため、Android フォンを持って歩き回る場合、Q=3 メートル/秒で問題なく動作することがわかりました。しかし、速い車で移動する場合は、明らかにもっと大きな数値を使用する必要があります。
public class KalmanLatLong {
private final float MinAccuracy = 1;
private float Q_metres_per_second;
private long TimeStamp_milliseconds;
private double lat;
private double lng;
private float variance; // P matrix. Negative means object uninitialised. NB: units irrelevant, as long as same units used throughout
public KalmanLatLong(float Q_metres_per_second) { this.Q_metres_per_second = Q_metres_per_second; variance = -1; }
public long get_TimeStamp() { return TimeStamp_milliseconds; }
public double get_lat() { return lat; }
public double get_lng() { return lng; }
public float get_accuracy() { return (float)Math.sqrt(variance); }
public void SetState(double lat, double lng, float accuracy, long TimeStamp_milliseconds) {
this.lat=lat; this.lng=lng; variance = accuracy * accuracy; this.TimeStamp_milliseconds=TimeStamp_milliseconds;
}
/// <summary>
/// Kalman filter processing for lattitude and longitude
/// </summary>
/// <param name="lat_measurement_degrees">new measurement of lattidude</param>
/// <param name="lng_measurement">new measurement of longitude</param>
/// <param name="accuracy">measurement of 1 standard deviation error in metres</param>
/// <param name="TimeStamp_milliseconds">time of measurement</param>
/// <returns>new state</returns>
public void Process(double lat_measurement, double lng_measurement, float accuracy, long TimeStamp_milliseconds) {
if (accuracy < MinAccuracy) accuracy = MinAccuracy;
if (variance < 0) {
// if variance < 0, object is unitialised, so initialise with current values
this.TimeStamp_milliseconds = TimeStamp_milliseconds;
lat=lat_measurement; lng = lng_measurement; variance = accuracy*accuracy;
} else {
// else apply Kalman filter methodology
long TimeInc_milliseconds = TimeStamp_milliseconds - this.TimeStamp_milliseconds;
if (TimeInc_milliseconds > 0) {
// time has moved on, so the uncertainty in the current position increases
variance += TimeInc_milliseconds * Q_metres_per_second * Q_metres_per_second / 1000;
this.TimeStamp_milliseconds = TimeStamp_milliseconds;
// TO DO: USE VELOCITY INFORMATION HERE TO GET A BETTER ESTIMATE OF CURRENT POSITION
}
// Kalman gain matrix K = Covarariance * Inverse(Covariance + MeasurementVariance)
// NB: because K is dimensionless, it doesn't matter that variance has different units to lat and lng
float K = variance / (variance + accuracy * accuracy);
// apply K
lat += K * (lat_measurement - lat);
lng += K * (lng_measurement - lng);
// new Covarariance matrix is (IdentityMatrix - K) * Covarariance
variance = (1 - K) * variance;
}
}
}
探しているものは、カルマン フィルターと呼ばれます。ナビゲーション データを滑らかにするためによく使用されます。これは必ずしも些細なことではなく、できるチューニングはたくさんありますが、非常に標準的なアプローチであり、うまく機能します。C++ 実装であるKFilter ライブラリが利用可能です。
私の次のフォールバックは、最小二乗適合です。カルマン フィルターは速度を考慮してデータを平滑化しますが、最小二乗法は位置情報のみを使用します。それでも、実装して理解するのは間違いなく簡単です。GNU Scientific Libraryにこれが実装されているようです。
時間ごとの位置変化から速度を計算するべきではありません。GPS の位置は不正確かもしれませんが、速度は正確です (時速 5 km 以上)。そのため、GPS ロケーション スタンプの速度を使用します。さらに、ほとんどの場合は機能しますが、コースでそれを行うべきではありません。
提供された GPS 位置は、すでにカルマン フィルター処理されており、おそらく改善することはできません。後処理では、通常、GPS チップのような同じ情報はありません。
滑らかにすることはできますが、これによりエラーも発生します。
デバイスが静止しているときに位置を削除することを確認してください。これにより、ジャンプ位置が削除されますが、一部のデバイス/構成では削除されません。
数学/理論をあまり使用しない方法の 1 つは、一度に 2、5、7、または 10 個のデータ ポイントをサンプリングし、それらが異常値であると判断することです。カルマン フィルターよりも精度の低い外れ値の測定は、次のアルゴリズムを使用してポイント間のすべてのペアごとの距離を取得し、他のポイントから最も遠いものを除外することです。通常、これらの値は、置換する範囲外の値に最も近い値に置き換えられます
例えば
5 つのサンプル ポイント A、B、C、D、E でのスムージング
ATOTAL = 距離の合計 AB AC AD AE
BTOTAL = 距離の合計 AB BC BD BE
CTOTAL = 距離の合計 AC BC CD CE
DTOTAL = 距離の合計 DA DB DC DE
ETOTAL = 距離の合計 EA EB EC DE
BTOTAL が最大の場合、BD = min { AB, BC, BD, BE } の場合、点 B を D に置き換えます。
この平滑化は外れ値を決定し、点 D の代わりに BD の中点を使用して位置線を平滑化することで拡張できます。走行距離は異なる場合があり、より数学的に厳密なソリューションが存在します。
最小二乗法については、他にもいくつか試してみてください。
最小二乗法だからといって、線形でなければならないというわけではありません。二次曲線をデータに最小二乗適合させることができます。これは、ユーザーが加速しているシナリオに適合します。(最小二乗法とは、従属変数として座標を使用し、独立変数として時間を使用することを意味することに注意してください。)
報告された精度に基づいてデータ ポイントの重み付けを試すこともできます。精度が低い場合、これらのデータ ポイントは低くなります。
試してみたいもう 1 つの方法は、単一のポイントを表示するのではなく、精度が低い場合は、報告された精度に基づいてユーザーがいる可能性のある範囲を示す円または何かを表示することです。(これは、iPhone に組み込まれている Google マップ アプリケーションが行うことです。)
カルマン フィルターに戻ります... ここで GPS データのカルマン フィルターの C 実装を見つけました: http://github.com/lacker/ikalmanまだ試していませんが、有望なようです。
スプラインを使用することもできます。持っている値を入力し、既知のポイント間のポイントを補間します。これを最小二乗近似、移動平均、またはカルマンフィルター(他の回答で説明されているように)とリンクすると、「既知の」ポイント間のポイントを計算することができます。
既知の値の間で値を補間できることにより、スムーズな遷移と、忠実度が高い場合に存在するデータの/合理的な/近似が得られます。http://en.wikipedia.org/wiki/Spline_interpolation
スプラインが異なれば、特性も異なります。私が最も一般的に使用しているのは、アキマスプラインとキュービックスプラインです。
考慮すべきもう1つのアルゴリズムは、Ramer-Douglas-Peuckerライン単純化アルゴリズムです。これは、GPSデータの単純化で非常に一般的に使用されます。(http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm)
@Stochastically の Java 実装の Javascript 実装を、必要とする人のために以下に示します。
class GPSKalmanFilter {
constructor (decay = 3) {
this.decay = decay
this.variance = -1
this.minAccuracy = 1
}
process (lat, lng, accuracy, timestampInMs) {
if (accuracy < this.minAccuracy) accuracy = this.minAccuracy
if (this.variance < 0) {
this.timestampInMs = timestampInMs
this.lat = lat
this.lng = lng
this.variance = accuracy * accuracy
} else {
const timeIncMs = timestampInMs - this.timestampInMs
if (timeIncMs > 0) {
this.variance += (timeIncMs * this.decay * this.decay) / 1000
this.timestampInMs = timestampInMs
}
const _k = this.variance / (this.variance + (accuracy * accuracy))
this.lat += _k * (lat - this.lat)
this.lng += _k * (lng - this.lng)
this.variance = (1 - _k) * this.variance
}
return [this.lng, this.lat]
}
}
使用例:
const kalmanFilter = new GPSKalmanFilter()
const updatedCoords = []
for (let index = 0; index < coords.length; index++) {
const { lat, lng, accuracy, timestampInMs } = coords[index]
updatedCoords[index] = kalmanFilter.process(lat, lng, accuracy, timestampInMs)
}
興味のある方は、CoffeeScript にマップしてください。**編集 -> バックボーンも使用して申し訳ありませんが、アイデアはわかります。
属性付きのビーコンを受け入れるようにわずかに変更されました
{緯度: item.lat,経度: item.lng,日付: 新しい日付(item.effective_at),精度: item.gps_accuracy}
MIN_ACCURACY = 1
# mapped from http://stackoverflow.com/questions/1134579/smooth-gps-data
class v.Map.BeaconFilter
constructor: ->
_.extend(this, Backbone.Events)
process: (decay,beacon) ->
accuracy = Math.max beacon.accuracy, MIN_ACCURACY
unless @variance?
# if variance nil, inititalise some values
@variance = accuracy * accuracy
@timestamp_ms = beacon.date.getTime();
@lat = beacon.latitude
@lng = beacon.longitude
else
@timestamp_ms = beacon.date.getTime() - @timestamp_ms
if @timestamp_ms > 0
# time has moved on, so the uncertainty in the current position increases
@variance += @timestamp_ms * decay * decay / 1000;
@timestamp_ms = beacon.date.getTime();
# Kalman gain matrix K = Covarariance * Inverse(Covariance + MeasurementVariance)
# NB: because K is dimensionless, it doesn't matter that variance has different units to lat and lng
_k = @variance / (@variance + accuracy * accuracy)
@lat = _k * (beacon.latitude - @lat)
@lng = _k * (beacon.longitude - @lng)
@variance = (1 - _k) * @variance
[@lat,@lng]
Java コードを @Stochastically から Kotlin に変換しました
class KalmanLatLong
{
private val MinAccuracy: Float = 1f
private var Q_metres_per_second: Float = 0f
private var TimeStamp_milliseconds: Long = 0
private var lat: Double = 0.toDouble()
private var lng: Double = 0.toDouble()
private var variance: Float =
0.toFloat() // P matrix. Negative means object uninitialised. NB: units irrelevant, as long as same units used throughout
fun KalmanLatLong(Q_metres_per_second: Float)
{
this.Q_metres_per_second = Q_metres_per_second
variance = -1f
}
fun get_TimeStamp(): Long { return TimeStamp_milliseconds }
fun get_lat(): Double { return lat }
fun get_lng(): Double { return lng }
fun get_accuracy(): Float { return Math.sqrt(variance.toDouble()).toFloat() }
fun SetState(lat: Double, lng: Double, accuracy: Float, TimeStamp_milliseconds: Long)
{
this.lat = lat
this.lng = lng
variance = accuracy * accuracy
this.TimeStamp_milliseconds = TimeStamp_milliseconds
}
/// <summary>
/// Kalman filter processing for lattitude and longitude
/// https://stackoverflow.com/questions/1134579/smooth-gps-data/15657798#15657798
/// </summary>
/// <param name="lat_measurement_degrees">new measurement of lattidude</param>
/// <param name="lng_measurement">new measurement of longitude</param>
/// <param name="accuracy">measurement of 1 standard deviation error in metres</param>
/// <param name="TimeStamp_milliseconds">time of measurement</param>
/// <returns>new state</returns>
fun Process(lat_measurement: Double, lng_measurement: Double, accuracy: Float, TimeStamp_milliseconds: Long)
{
var accuracy = accuracy
if (accuracy < MinAccuracy) accuracy = MinAccuracy
if (variance < 0)
{
// if variance < 0, object is unitialised, so initialise with current values
this.TimeStamp_milliseconds = TimeStamp_milliseconds
lat = lat_measurement
lng = lng_measurement
variance = accuracy * accuracy
}
else
{
// else apply Kalman filter methodology
val TimeInc_milliseconds = TimeStamp_milliseconds - this.TimeStamp_milliseconds
if (TimeInc_milliseconds > 0)
{
// time has moved on, so the uncertainty in the current position increases
variance += TimeInc_milliseconds.toFloat() * Q_metres_per_second * Q_metres_per_second / 1000
this.TimeStamp_milliseconds = TimeStamp_milliseconds
// TO DO: USE VELOCITY INFORMATION HERE TO GET A BETTER ESTIMATE OF CURRENT POSITION
}
// Kalman gain matrix K = Covarariance * Inverse(Covariance + MeasurementVariance)
// NB: because K is dimensionless, it doesn't matter that variance has different units to lat and lng
val K = variance / (variance + accuracy * accuracy)
// apply K
lat += K * (lat_measurement - lat)
lng += K * (lng_measurement - lng)
// new Covarariance matrix is (IdentityMatrix - K) * Covarariance
variance = (1 - K) * variance
}
}
}