Dati GPS fluidi


145

Sto lavorando con i dati GPS, ottenendo valori ogni secondo e visualizzando la posizione corrente su una mappa. Il problema è che a volte (specialmente quando la precisione è bassa) i valori variano molto, facendo "saltare" la posizione corrente tra punti distanti nella mappa.

Mi chiedevo un metodo abbastanza semplice per evitarlo. Come prima idea, ho pensato di scartare i valori con precisione oltre una certa soglia, ma credo che ci siano altri modi migliori per farlo. Qual è il solito modo in cui i programmi eseguono questo?


Sento gli effetti negativi del "rumore GPS" quando provo a calcolare i valori associati (derivati) come la velocità e la pendenza, che sono molto discontinui specialmente per i tracklog ad alta frequenza di campionamento (poiché il tempo ha una risoluzione intera [un secondo]).
Heltonbiker,

4
(Inoltre, se stai navigando attraverso le strade principali, puoi utilizzare l'algoritmo "aggancia alle strade" a condizione che tu abbia un buon set di dati della roadmap [corretto, preciso]. Solo un pensiero)
heltonbiker

Sto affrontando questo problema anche per la massima precisione.
ViruMax,

Risposte:


80

Ecco un semplice filtro Kalman che potrebbe essere utilizzato proprio per questa situazione. È venuto da un lavoro che ho fatto su dispositivi Android.

La teoria generale del filtro di Kalman riguarda le stime per i vettori, con l'accuratezza delle stime rappresentate dalle matrici di covarianza. Tuttavia, per stimare la posizione sui dispositivi Android la teoria generale si riduce a un caso molto semplice. I fornitori di servizi di localizzazione Android forniscono la posizione come latitudine e longitudine, insieme a una precisione specificata come un singolo numero misurato in metri. Ciò significa che invece di una matrice di covarianza, l'accuratezza nel filtro Kalman può essere misurata da un singolo numero, anche se la posizione nel filtro Kalman è misurata da due numeri. Inoltre, il fatto che latitudine, longitudine e metri siano effettivamente tutte le diverse unità può essere ignorato, perché se si inseriscono i fattori di ridimensionamento nel filtro Kalman per convertirli tutti nelle stesse unità,

Il codice potrebbe essere migliorato, poiché si presume che la migliore stima della posizione corrente sia l'ultima posizione nota e se qualcuno si sta spostando dovrebbe essere possibile utilizzare i sensori di Android per produrre una stima migliore. Il codice ha un singolo parametro libero Q, espresso in metri al secondo, che descrive la velocità con cui la precisione diminuisce in assenza di nuove stime di posizione. Un parametro Q più elevato indica che l'accuratezza decade più rapidamente. I filtri Kalman funzionano generalmente meglio quando l'accuratezza diminuisce un po 'più rapidamente di quanto ci si potrebbe aspettare, quindi per andare in giro con un telefono Android trovo che Q = 3 metri al secondo funzioni bene, anche se generalmente cammino più lentamente di così. Ma se si viaggia in un'auto veloce dovrebbe ovviamente essere usato un numero molto più grande.

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;
        }
    }
}

1
Il calcolo della varianza non dovrebbe essere: varianza + = TimeInc_milliseconds * TimeInc_milliseconds * Q_metres_per_second * Q_metres_per_second / 1000000
Horacio

4
@Horacio, so perché lo pensi, ma no! Matematicamente, l'incertezza qui viene modellata da un processo di Wiener (vedi en.wikipedia.org/wiki/Wiener_process ) e con un processo di Wiener la varianza cresce linearmente con il tempo. La variabile Q_metres_per_secondcorrisponde alla variabile sigmanella sezione "Processi correlati" in quell'articolo di Wikipedia. Q_metres_per_secondè una deviazione standard ed è misurata in metri, quindi metri e non metri / secondi sono le sue unità. Corrisponde alla deviazione standard della distribuzione dopo che è trascorso 1 secondo.
Stocasticamente il

3
Ho provato questo approccio e il codice, ma ha finito per accorciare troppo la distanza totale. Reso troppo impreciso.
Andreas Rudolph,

1
@ user2999943 Sì, utilizzare il codice per elaborare le coordinate ottenute da onLocationChanged ().
Stocasticamente il

2
@Koray se non si dispone di informazioni sulla precisione, non è possibile utilizzare un filtro Kalman. È completamente fondamentale per ciò che il filtro Kalman sta cercando di fare.
Stocasticamente il

75

Quello che stai cercando si chiama Kalman Filter . Viene spesso utilizzato per lisciare i dati di navigazione . Non è necessariamente banale e ci sono molte regolazioni che puoi fare, ma è un approccio molto standard e funziona bene. È disponibile una libreria KFilter che è un'implementazione C ++.

Il mio prossimo fallback sarebbe adattarsi ai minimi quadrati . Un filtro Kalman uniformerà i dati tenendo conto delle velocità, mentre un approccio di adattamento dei minimi quadrati utilizzerà solo le informazioni di posizione. Tuttavia, è sicuramente più semplice da implementare e comprendere. Sembra che la GNU Scientific Library potrebbe avere un'implementazione di questo.


1
Grazie Chris. Sì, ho letto di Kalman mentre faccio delle ricerche, ma è certamente un po 'al di là delle mie conoscenze matematiche. Sei a conoscenza di un codice di esempio di facile lettura (e comprensione!) O, meglio ancora, di un'implementazione disponibile? (C / C ++ / Java)
Al.

1
@Al purtroppo la mia unica esposizione con i filtri Kalman è attraverso il lavoro, quindi ho un codice meravigliosamente elegante che non posso mostrarti.
Chris Arguin,

Nessun problema :-) Ho provato a cercare ma per qualche motivo sembra che questa cosa di Kalman sia una magia nera. Molte pagine di teoria ma codice poco o niente .. Grazie, proverò con gli altri metodi.
Al.

2
kalman.sourceforge.net/index.php ecco l'implementazione C ++ del filtro Kalman.
Rostyslav Druzhchenko,

1
@ChrisArguin Prego. Fammi sapere se il risultato è buono per favore.
Rostyslav Druzhchenko,

11

Potrebbe arrivare un po 'tardi ...

Ho scritto questo KalmanLocationManager per Android, che racchiude i due provider di posizione più comuni, Network e GPS, kalman filtra i dati e fornisce aggiornamenti a LocationListener(come i due provider "reali").

Lo uso principalmente per "interpolare" tra letture - per ricevere aggiornamenti (previsioni di posizione) ogni 100 millis per esempio (invece della massima frequenza gps di un secondo), il che mi dà una migliore frequenza dei fotogrammi durante l'animazione della mia posizione.

In realtà, utilizza tre filtri kalman, attivi per ogni dimensione: latitudine, longitudine e altitudine. Sono indipendenti, comunque.

Questo rende la matematica della matrice molto più semplice: invece di utilizzare una matrice di transizione di stato 6x6, utilizzo 3 matrici 2x2 diverse. In realtà nel codice, non uso affatto matrici. Risolte tutte le equazioni e tutti i valori sono primitivi (doppio).

Il codice sorgente funziona e c'è un'attività demo. Scusate la mancanza di javadoc in alcuni punti, ci vediamo.


1
Ho provato a usare il tuo codice lib, ho ottenuto alcuni risultati indesiderati, non sono sicuro che stia facendo qualcosa di sbagliato ... (Sotto c'è l'URL dell'immagine, il blu è il percorso delle posizioni filtrate, l'arancione sono le posizioni non elaborate
umesh

I picchi che vedi "crescere" dalla media (linea arancione) sembrano aggiornamenti del provider di rete. Puoi provare a tracciare sia aggiornamenti di rete grezzi che gps? Forse staresti meglio senza aggiornamenti di rete, a seconda di ciò che stai cercando di ottenere. A proposito, da dove prendi quegli aggiornamenti grezzi di orange?
villoren,

1
i punti arancioni provengono dal fornitore di gps e quelli blu da Kalman, ho tracciato i log sulla mappa
umesh

Potresti inviarmi quei dati in un formato di testo? Ogni aggiornamento di posizione ha il set di campi Location.getProvider (). Solo un file con tutti Location.toString ().
villoren,

9

Non è necessario calcolare la velocità dal cambio di posizione per volta. Il GPS potrebbe avere posizioni imprecise, ma ha una velocità precisa (superiore a 5 km / h). Quindi usa la velocità dalla posizione GPS. E inoltre non dovresti farlo con il corso, sebbene funzioni la maggior parte delle volte.

Le posizioni GPS, come consegnate, sono già filtrate da Kalman, probabilmente non è possibile migliorare, nel postelaborazione di solito non si hanno le stesse informazioni del chip GPS.

Puoi smussarlo, ma questo introduce anche errori.

Assicurati solo di rimuovere le posizioni quando il dispositivo è fermo, questo rimuove le posizioni di salto, che alcuni dispositivi / configurazioni non rimuovono.


5
Potresti fornire alcuni riferimenti per questo, per favore?
ivyleavedtoadflax

1
Ci sono molte informazioni e molta esperienza professionale in quelle frasi, per quale frase vuoi esattamente un riferimento? per la velocità: cerca l'effetto doppler e il GPS. Kalman interno? Questa è una conoscenza di base del GPS, ogni documento o libro che descrive come funziona un chip GPS interno. errori di smootig: il livellamento continuo introduce errori. stare fermo? Provalo.
AlexWien,

2
Il "saltare in giro" quando si è fermi non è l'unica fonte di errore. Ci sono anche riflessioni del segnale (ad es. Da montagne) in cui la posizione salta intorno. I miei chip GPS (ad esempio Garmin Dakota 20, SonyEricsson Neo) non l'hanno filtrato ... E ciò che è davvero uno scherzo è il valore di elevazione dei segnali GPS quando non combinato con la pressione barometrica. Questi valori non sono filtrati o non voglio vedere i valori non filtrati.
hgoebl,

1
@AlexWien GPS calcola la distanza da un punto alla volta a una tolleranza dandoti una sfera con spessore, una conchiglia centrata attorno a un satellite. Sei da qualche parte in questo volume di shell. L'intersezione di tre di questi volumi di shell fornisce un volume di posizione, il cui centroide è la posizione calcolata. Se disponi di una serie di posizioni segnalate e sai che il sensore è fermo, il calcolo del centroide interseca efficacemente molte più shell, migliorando la precisione. L'errore in questo caso è ridotto .
Peter, finito il

6
"Le posizioni GPS, al momento della consegna, sono già filtrate da Kalman, probabilmente non è possibile migliorare". Se puoi indicare una fonte che lo conferma per gli smartphone moderni (ad esempio), sarebbe molto utile. Non riesco a vederne le prove da solo. Anche il semplice filtro Kalman delle posizioni non elaborate di un dispositivo suggerisce fortemente che non è vero. Le posizioni grezze danzano in modo irregolare, mentre le posizioni filtrate spesso si tengono vicine alla posizione reale (nota).
sobri,

6

Di solito uso gli accelerometri. Un improvviso cambio di posizione in breve tempo implica un'elevata accelerazione. Se ciò non si riflette nella telemetria dell'accelerometro, è quasi certamente dovuto a un cambiamento nei "migliori tre" satelliti utilizzati per calcolare la posizione (a cui mi riferisco come teletrasporto GPS).

Quando una risorsa è a riposo e saltellante a causa del teletrasporto GPS, se si calcola progressivamente il centroide, si interseca effettivamente un insieme sempre più grande di conchiglie, migliorando la precisione.

Per fare ciò quando l'asset non è a riposo, è necessario stimare la sua probabile posizione e orientamento successivi in ​​base a velocità, direzione e dati di accelerazione lineare e rotazionale (se si hanno giroscopi). Questo è più o meno ciò che fa il famoso filtro K. Puoi ottenere tutto in hardware per circa $ 150 su un AHRS contenente tutto tranne il modulo GPS e con un jack per collegarne uno. Ha una propria CPU e filtri Kalman integrati; i risultati sono stabili e abbastanza buoni. La guida inerziale è altamente resistente al jitter ma va alla deriva nel tempo. Il GPS è soggetto a jitter ma non si sposta con il tempo, praticamente sono stati fatti per compensarsi.


4

Un metodo che utilizza meno matematica / teoria è di campionare 2, 5, 7 o 10 punti dati alla volta e determinare quelli che sono anomali. Una misura meno accurata di un valore anomalo rispetto a un filtro Kalman consiste nell'utilizzare il seguente algoritmo per prendere tutte le sagge distanze tra i punti e scartare quella più lontana dalle altre. In genere tali valori vengono sostituiti con il valore più vicino al valore esterno che si sta sostituendo

Per esempio

Livellamento in cinque punti campione A, B, C, D, E

ATOTAL = SOMMA delle distanze AB AC AD AE

BTOTAL = SOMMA delle distanze AB BC BD BE

CTOTAL = SOMMA delle distanze AC BC CD CE

DTOTAL = SOMMA delle distanze DA DB DC DE

ETOTAL = SOMMA delle distanze EA EB EC DE

Se BTOTAL è il più grande, sostituire il punto B con D se BD = min {AB, BC, BD, BE}

Questo livellamento determina valori anomali e può essere aumentato utilizzando il punto medio di BD anziché il punto D per smussare la linea di posizione. Il tuo chilometraggio può variare ed esistono soluzioni matematicamente più rigorose.


Grazie, ci proverò anche io. Nota che voglio rendere più liscia la posizione corrente, poiché è quella visualizzata e quella utilizzata per recuperare alcuni dati. Non mi interessano i punti passati. La mia idea originale era usare mezzi ponderati, ma devo ancora vedere cosa è meglio.
Al.

1
Al, questa sembra essere una forma di mezzi ponderati. Dovrai usare i punti "passati" se vuoi eseguire un livellamento, perché il sistema deve avere più della posizione corrente per sapere anche dove regolare. Se il GPS sta rilevando i punti dati una volta al secondo e l'utente guarda lo schermo una volta ogni cinque secondi, è possibile utilizzare 5 punti dati senza che se ne accorga! Una media mobile verrebbe ritardata anche di un solo dp.
Karl,

4

Per quanto riguarda i minimi quadrati, ecco alcune altre cose con cui sperimentare:

  1. Solo perché è il minimo adattamento dei quadrati non significa che deve essere lineare. È possibile adattare i minimi quadrati di una curva quadratica ai dati, quindi questo si adatterebbe a uno scenario in cui l'utente sta accelerando. (Si noti che con almeno i quadrati si intendono le coordinate come variabile dipendente e il tempo come variabile indipendente.)

  2. Puoi anche provare a ponderare i punti dati in base alla precisione riportata. Quando l'accuratezza ha un peso ridotto, questi punti di dati si abbassano.

  3. Un'altra cosa che potresti voler provare è piuttosto che visualizzare un singolo punto, se la precisione è bassa visualizza un cerchio o qualcosa che indica l'intervallo in cui l'utente potrebbe basarsi sulla precisione riportata. (Questo è ciò che fa l'applicazione Google Maps integrata nell'iPhone.)


3

Puoi anche usare una spline. Inserisci i valori che hai e interpola i punti tra i tuoi punti noti. Collegando questo con un adattamento dei minimi quadrati, la media mobile o il filtro kalman (come menzionato in altre risposte) ti dà la possibilità di calcolare i punti tra i tuoi punti "conosciuti".

Essere in grado di interpolare i valori tra i tuoi noti ti dà una transizione gradevole e una / ragionevole / approssimazione di quali dati sarebbero presenti se tu avessi una fedeltà più alta. http://en.wikipedia.org/wiki/Spline_interpolation

Spline diverse hanno caratteristiche diverse. Quelli che ho visto più comunemente usati sono Akima e spline cubiche.

Un altro algoritmo da considerare è l'algoritmo di semplificazione della linea Ramer-Douglas-Peucker, è abbastanza comunemente usato nella semplificazione dei dati GPS. ( http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm )



0

Mappato su CoffeeScript se qualcuno è interessato. ** modifica -> scusa anche usando la spina dorsale, ma hai avuto l'idea.

Modificato leggermente per accettare un faro con attributi

{latitudine: item.lat, longitudine: item.lng, data: new Date (item.effective_at), precisione: 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]

È tentato di modificare questo, ma c'è un errore di battitura nelle ultime righe dove @late @lngimpostati. Dovrebbe essere +=piuttosto che=
jdixon04 il

0

Ho trasformato il codice Java da @Stochastically a 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
    /// /programming/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
        }
    }
}

0

Ecco un'implementazione Javascript dell'implementazione Java di @ Stochastically per chiunque ne abbia bisogno:

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]
  }
}

Esempio di utilizzo:

   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)
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.