Kursy • Poradniki • Inspirujące DIY • Forum
Napiszemy także kilka programów, które pokażą na przykładach zastosowanie filtru Kalmana w różnych sytuacjach życiowych. Programy będą napisane w Octave oraz w C++ z użyciem biblioteki OpenCV. Algorytm zgodnie z którym działa FK jest prosty w implementacji, dodatkowo nie wymaga złożonych obliczeń, a więc analiza zamieszczonych kodów nie powinna sprawić trudności.
Najważniejszą rzeczą przy implementacji FK jest opisanie poprawnymi równaniami naszego procesu i systemu pomiarowego. Postaram się na prostym przykładzie pokazać jak robi się to krok po kroku, dlatego na początek zaczniemy od przyswojenia niezbędnej teorii.
Artykuły powiązane tematycznie:
Filtr Kalmana - teoria
Aby możliwe było zastosowanie filtru Kalmana, nasz proces powinien dać się opisać za pomocą następujących równań linowych:
Pierwsze zwane jest równaniem stanu lub modelem procesu, natomiast drugie to model pomiaru. W powyższych równaniach A, B, H są macierzami, wektor x nazywany jest stanem systemu, wektor u zawiera informacje wejściowe dla systemu, np. zadaną z góry prędkość poruszania się robota lub jego przyspieszenie itp., wektor z to mierzone wyjście systemu.
Natomiast w oraz v oznaczają szum (odchylenia standardowe). Przy czym w to szum procesu, natomiast v jest szumem pomiaru. Wszystkie informacje o stanie systemu w danej chwili k zawiera wektor x, jednak nie możemy tych informacji wydobyć bezpośrednio z x, możemy je uzyskać poprzez wektor z dokonując pomiaru, co wprowadza nam szum pomiarowy reprezentowany wektorem v.
Dlatego dane otrzymane z wektora z nie są godne zaufania, możemy z nich skorzystać w procesie wyznaczania bardziej wiarygodnych danych, ale branie ich w ciemno nie jest najlepszym rozwiązaniem.
Aby zrozumieć to lepiej wyobraźmy sobie łódkę płynącą po rzece, widzianą z lotu ptaka. Możemy przyjąć, że łódka nie porusza się po rzece na boki tylko płynie wzdłuż niej, równolegle do brzegu, a więc ruch zachodzi w jednym wymiarze, wzdłuż jednej osi.
Przypuśćmy, że chcemy estymować położenie łódki na wodzie, mając pomiar położenia łódki z GPS. Łódka porusza się w dodatku z pewnym znanym przyspieszeniem, będzie to nasze u. Z fizyki wiadomo, że prędkość łódki w chwili k wyrażą się równaniem:
Gdzie u jest przyspieszeniem. Jednakowoż w rzeczywistym świecie, łódka płynąca po rzece napotka pewne opory, będą one związane ze zmieniającymi się prędkością i kierunkiem wiatru, siłą prądu w rzece, kolizjami z obiektami unoszącymi się na wodzie itp.
W wyniku tego rzeczywista prędkość będzie nieco się różnić od tej wyliczonej za pomocą powyższego równania, możemy powiedzieć, ze prędkość będzie zaszumiona. Lepszym pomysłem jest więc napisanie równania w postaci:
Gdzie dodatkowy parametr oznacza szumy. Podobnie możemy zapisać równanie na położenie:
Gdzie ostatni parametr oznacza szum położenia. Natomiast wektor stanu przyjmie więc postać:
A nasze równania systemu przyjmują postać:
Otrzymaliśmy więc równania dla naszego problemu, jednowymiarowego ruchu łódki na wodzie z pomiarem jej pozycji z GPS. Z równań wynika że nasze macierze A ,B, H są następujące:
Dlaczego macierz H jest taka? Otóż macierz ta określa przejście ze stanu do pomiaru. Z racji, że wielkością którą chcemy mierzyć w naszym przykładzie jest, położeniu łódki konstruujemy macierz H w ten sposób żeby w równaniu modelu pomiaru uzyskać na koniec pomiar tejże wielkości. Po pomnożeniu macierzy H oraz wektora stanu xk otrzymamy (położenie):
Jak widzimy nasz system nie jest pozbawiony szumów, a żeby móc poprawnie go wyregulować trzeba się ich pozbyć. W tym momencie przychodzi nam z pomocą Filtr Kalmana.
Algorytm, zgodnie z którym działa Filtr Kalmana, dzielimy na dwie fazy (etapy). Pierwszy etap zwany jest predykcją, a drugi korekcją. Podczas predykcji bazując na poprzedniej wartości stanu x wyznaczana jest nowa wartość stanu x oraz jej kowariancja.
Wartości te wyznaczane są bez korzystania z informacji ze świata zewnętrznego, tzn. w przypadku naszej łódki bez odniesienia do pomiaru z czujnika GPS. Są one więc niejako przewidywane na podstawie równań stanu x za pomocą których opisaliśmy nasz system. W przypadku naszej łódki były by tu wykorzystane równania fizyczne opisujące jej położenie i prędkość w zależności od czasu.
Ten etap to niejako czysta matematyka, poparta wiedzą o wcześniejszym stanie procesu, równania które realizuje są następujące:
Gdzie:
oznacza kowariancją szumu procesu. Póxniej wytłumaczę w jaki sposób się ją wyznacza. Na razie możemy powiedzieć, że wnosi informacje na temat szumów z naszego procesu. Jeśli ktoś nie wie co oznacza symbol T to należy szukać pod hasłem tranpozycja macierzy.
Gdybyśmy zostawili algorytm w tym momencie i pozwolili mu działać, dostali byśmy w wyniku taki sam przebieg jaki mają funkcje fizyczne opisujące prędkość oraz położenie naszego robota.
Jednak my dysponując pomiarami z czujników możemy uzyskać dane bliższe rzeczywistości niż zgadywanie z zamkniętymi oczami na podstawie matematyki, ale bez informacji ze świata zewnętrznego. W tym momencie przychodzi nam z pomocą faza korekcji.
W fazie korekcji wyznaczamy pewną wielkość K, zwaną dalej wzmocnieniem Kalmana.
Gdzie:
oznacza kowariancją szumu pomiaru. Dalej wytłumaczę w jaki sposób się ją wyznacza.
Prześledźmy teraz fazę korekcji. Na początek wyznaczana jest wartość wzmocnienia Kalmana K. Jeżeli zwrócimy uwagę na sposób w jaki jest wyznaczana, dojdziemy do wniosku, że im większe szumy pomiarowe, które są tu reprezentowane przez kowariancję R tym mniejsza wartość K, gdyż R znajduje się w mianowniku.
W następnym równaniu uaktualniany jest wektor stanu x, jego wartość jest korygowana przy użyciu pomiarów z czujników (z). Duże znaczenie przy korekcji ma wzmocnienie Kalmana K.
Przypominam raz jeszcze, że będzie ono tym mniejsze im większe sumy pomiarowe, w więc w przypadku dużych szumów, nowa wartość x nie będzie się wiele różniła od tej wyliczonej w fazie predykcji.
Wniosek z tego jest następujący, za pomocą K określamy jak bardzo można polegać na danych z czujników, jeżeli szumy są duże, a więc K małe, to informacje z pomiarów będą miały niewielki wpływ na ostateczną wartość x, jeżeli natomiast szumy w układzie są małe, a więc K duże, oznacza to, że dane z pomiarów są o wiele bardziej wiarygodne i będą miały znaczny wpływa na ostateczną wartość x.
Na tym polega właśnie nasza faza korekcji. Na koniec należy jeszcze skorygować macierz kowariancji P, co realizuje ostatnie z równań fazy korekcji.
Więc algorytm składa się z 5 prostych równań, które właściwie można zapisać w formie trzech. Po wykonaniu fazy korekcji wracamy z powrotem do fazy predykcji i tak w kółko w pętli. Należy jeszcze pamiętać o wstępnej inicjalizacji wartości dla zmiennych z fazy korekcji, gdyż przy pierwszym przebiegu wykonywana jest najpierw faza predykcji, która wykorzystuje zmienne z fazy korekcji.
Nie są one jeszcze wtedy znane, należy więc ustawić je na przypadkowe lub przemyślane wartości początkowe, często po prostu się je zeruje ale o tym później przy omawianiu przykładowych programów.
Przykład pierwszy - jednowymiarowy ruch łódki
Rozważmy więc jeszcze raz nasz przykład z łódką i policzmy dla niej tajemnicze kowariancję R i Q. Równania opisujące ten proces były następujące:
Załóżmy więc, że nasza łódka porusza się ze stałym przyspieszeniem 1 [m/(s*s)] i występują szumy przyspieszenia o wartości 0.1 [m/(s*s)]. Pozycję naszej łódki mierzymy co 0.1 [s]. Dodatkowo wiemy, że niedokładność pomiaru GPS wynosi 0.5 [m]. Skoro pozycja łódki jest mierzona z czasem T = 0.1 możemy zapisać nasze równanie w postaci:
Dodatkowo wiemy, że odchylenie standardowe pomiaru z GPS wynosi 0.5 m, co daje nam wartość kowariancji R równą:
Należy także policzyć kowariancję Q. Wiemy, że na nasz wektor stanu x składają się prędkość i położenie łódki.
Powinniśmy więc policzyć ile wynoszą szumy. W tym celu spójrzmy na równianie stanu:
Wynika z niego, że położenie jest proporcjonalne do 0.005 razy przyspieszenie u. Natomiast przyspieszenie u wnosi nam szum 0.1 [m/(s*s)]. Odchylenie standardowe będzie więc wynosić:
s = (0.1 * 0.005) = 0.0005
Prędkość jest proporcjonalne do 0.1 razy przyspieszenie u. Daje to wartość odchylenia standardowego:
v = 0.1 * 0.1 = 0.01
Ostatecznie wartość Q można wyliczyć jako:
Choć obliczanie wartość dla kowariancji Q i R może wydać się żmudne, my amatorzy w wielu przypadkach możemy je pominąć i wyznaczyć je metodami na oko oraz prób i błędów.
W ten sposób mamy wszystko co trzeba, aby napisać filtr Kalmana dla ruchu łódki w jednym wymiarze, z pomiarem pozycji GPS. Program, który to zilustruje napiszemy w GNU Octave. Będzie on realizował co następuje:
- symulację jednowymiarowego ruchu
- odczyt danych z czujników
- usuwanie szumów
- estymację położenia filtr'em Kalmana.
Wcześniej jeszcze podam kilka ważnych informacji na temat Octave, które przydadzą się szczególnie tym, którzy do czynienia z tym pakietem do tej pory nie mieli.
Czym jest Octave?
Otóż zgodnie z tym co mówi Wikipedia:
GNU Octave – środowisko obliczeń oraz język programowania przeznaczony dla prowadzania obliczeń numerycznych. Octave dostępny jest na większości systemów uniksowych. Rozprowadzany jest na zasadach licencji GNU GPL. Octave jest wolnym odpowiednikiem środowiska MATLAB.
Programy, które tu zamieszczam będą działać także w Matlabie.
Skąd wziąć Octave?
Najprostszym sposobem jest skorzystanie ze środowiska w wersji online. Nie będziemy musieli pobierać plików, ani męczyć się z instalacją, a w mgnieniu oka uruchomimy zamieszczone skrypty.
Jeżeli wybierzemy ten sposób wystarczy wejść na stronę Verbosus i dokonać procesu rejestracji. Wszystko jest całkowicie darmowe, a rejestracja sprowadza się do podania adresu e-mail i hasła.
Po zalogowaniu się, naszym oczom ukaże się następujący widok:
W tym momencie należy stworzyć nowy plik .m wpisując jego nazwę w pole ".m Files" znajdujące się po lewej stronie. Plik powinien nazywać się "kalman_boat.m" następnie wklejamy do niego kod programu, który znajduje się poniżej i zapisujemy go przyciskiem "Save":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
function kalman_boat(duration, step, measnoise, accelnoise, acceleration) %Funkcja kamlan_boat(duration, T, measnoise ) %duration - czas trwania symulacji [s] %step - krok symulacji [s] %measnoise – wspolczynnik szumu pomiaru %accelnoise - wspolczynnik szumu przyspieszenia %acceleration - przyspieszenie lodki [m/s*s] T = step; R = measnoise^2; %Wyznaczenie kowariancji szumu pomiaru Q = accelnoise^2 * [(T^2/2)^2 (T^2/2)*T; (T^2/2)*T T^2]; %Kowariancja szumu procesu A = [1 T; 0 1]; %Macierz przejscia stanu poprzedniego do obecnego B = [T^2 / 2; T ]; %Macierz przejscia dla wejscia ( w tym przypadku przyspieszenie ) H = [1 0]; %Macierz przejscia dla mierzonej wielkosci %Inicjalizacja I = [ 1 0; 0 1 ]; %Macierz jednostkowa P = Q; %Poczatkowa macierz kowariancji P x = [0; 0]; %Stan poczatkowy xhat = x; %Wartosc poczatkowa x u = acceleration; %Przyspieszenie lodki %Przygotowanie miejsca na dane pos = []; %Prawdziwa pozycja poshat = []; %Pozycja estymowana filtrem kalmana posmeas = []; %Pozycja mierzona %Rozpoczecie symulacji for t = 0:T:duration, %Symulacja naszego stystemu liniowego, dodajemy szum przyspieszenia x = A * x + B * u + accelnoise * [(T^2/2)*randn; T * randn]; %Symulacja zaszumionego pomiaru z = H * x + measnoise * randn; %Faza Predykcji xhat = A * xhat + B * u; P = A*P*A' + Q; %' %Faza Korekcji K = P * H' * inv( H * P * H' + R); xhat = xhat + K*(z - H*xhat); P = ( I - K*H)*P; %Uaktualnienie danych pos = [pos; x(1)]; %Pozycja rzeczywista posmeas = [posmeas; z]; %Pozycja mierzona GPS poshat = [poshat; xhat(1)]; %Pozycja estymowana filtrem kalmana end; close all; t = 0:step:duration; figure; plot(t, pos, t, posmeas, t, poshat); grid; xlabel('Czas [s]'); ylabel('Pozycja [m]'); legend('prawdziwa', 'mierzona', 'estymowana'); title('Pozycja lodki'); |
Należy jeszcze utworzyć plik .m z poziomu, którego wywołamy utworzony skrypt. W tym celu tworzymy plik "main.m". Przechodzimy do niego klikając go dwukrotnie i wpisujemy kod wywołujący wcześniejszy skrypt.
1 |
kalman_boat(10, 0.1, 5, 0.9, 1); |
Zapisujemy i wciskamy "Generate". Poniżej efekt działania programu:
Kod skryptu kalman_boat.m jest prosty do zrozumienia. Należy jeszcze raz spojrzeć na równania naszego procesu aby zobaczyć, że macierze A, B, H są z nich wprost przepisane. Kod oblicza także automatycznie wartości dla macierzy R oraz Q zgodnie ze schematem przedstawionym powyżej. Potem następuje początkowa inicjalizacja zmiennych.
Następnie symulujemy środowisko tak by występowały w nim szumy i na koniec próbujemy eliminować te szumy filtrem Kalmana. Program wyświetla wykres pozycji łódki z trzema przebiegami, rzeczywistym, mierzonym GPS'em oraz estymowanym filtrem kalmana, dzięki czemu możemy zobaczyć o ile wiarygodniejsze dane uzyskujemy stosując filtr.
Srypt wywołujemy podając pięć parametrów:
1 2 3 4 5 |
%duration - czas trwania symulacji [s] %step - krok symulacji [s] %measnoise - odchylenie standardowe szumu pomiaru %accelnoise - odchylenie standardowe szumu przyspieszenia %acceleration - przyspieszenie lodki [m/s*s] |
Poniżej zamieszczam wykres wygenerowany przez program:
Przykład drugi - filtracja zaszumionego napięcia
Kolejny przykład prezentuje pomiar zaszumionego napięcia z wykorzystaniem filtru kalmana. Zakładamy że napięcie ma stałą wartość. Wówczas równania opisujące nasz system znacznie się uproszczą.
Z racji że napięcie jest stałe, macierz przejścia z stanu poprzedniego do następnego A = 1. Nie mamy żadnej wielkości sterującej (wejściowej) zatem u = 0. Macierz H natomiast wynosi jeden, gdyż ma tylko jedną wielkość mierzoną i estymowaną, którą jest napięcie. Zatem nasze równania upraszczają się do postaci:
Faza predykcji jest więc następująca:
Natomiast faza korekcji:
Wartości Q i R są przyjmowane na oko. Kod programu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
function kalman_dc(duration, step, U, unoise) %Funkcja kalman_dc(duration, T) %duration - czas trwania symulacj [s] %step - krok symulacji [s] %U - wartosc napiêcia [V] %unoise - wspolczynnik zaszumienia napiecia T = step; %Ustawienie wartosci dla macierzy Q i R Q = 0.0001; %Kowariancja szumu procesu R = 0.09; %Kowariancja szumu pomiaru %Inicjalizacja zmiennych x = U; P = Q; %Miejsce na dane vol = []; volmeas = []; volpred = []; for t=0:T:duration, %Symulacja pomiaru zaszumionego napiecia z = U + unoise * randn; %Faza predykcji P = P + Q; %Faza korekcji K = P * inv( :P + R); x = x + K * (z - x); P = ( 1 - K ) * P; %Uaktualnienie zmiennych vol = [ vol; U ]; volmeas = [ volmeas; z ]; volpred = [ volpred; x ]; end; close all; t = 0:T:duration; figure; plot(t, vol, t, volmeas, t, volpred); grid; ylabel('Napiecie [V]'); xlabel('Czas [s]'); legend('Napiecie rzeczywiste', 'Napiecie mierzone ( wej. )', 'Napiecie estymowane ( wyj. )'); title('Filtr Kalmana - Zaszumione napiecie'); |
Należy utworzyć skrypt o nazwie "kalman_dc.m". Poniżej efekt działania programu wywołanego w main.m z parametrami:
1 |
kalman_dc(10, 0.1, 5, 0.2); |
Przykład trzeci - śledzenie obiektu w przestrzeni 2D
Na koniec rozważymy jeszcze problem śledzenia obiektu w przestrzeni dwuwymiarowej. Problem ten rozważymy najpierw teoretycznie, a potem napiszemy stosowny program w języku C++ przy użyciu biblioteki OpenCV.
Jako czujniki wykorzystamy informacje o położeniu kursora myszy na ekranie.
Zacznijmy więc jeszcze raz od zapisania równań:
Możemy wyobrazić sobie, że sytuacja jest podobna jak podczas przykładu z łódką, zamiast jednego wymiaru mamy 2, a zamiast GPS położenie kursora myszy. Możemy napisać więc 4 proste równania opisujące nasz proces:
Co można też wyrazić jako:
Z czego wynika że macierz A będzie mieć postać:
Natomiast nasza macierz H powinna zapewniać pomiar z dwóch czujników: położenia x oraz y, a więc powinna wyglądać następująco:
W tym przypadku nie mamy sterowania (macierz B), a więc znika nam z równań ta część. Powinniśmy się jeszcze zastanowić nad macierzami R i Q. Można ich wartości przyjąć "na oko". Macierze te mogę mieć wszystkie elementy równe 0 prócz elementów znajdujących się na głównej przekątnej. Wartości Q zazwyczaj przyjmuje się bardzo małe, R nieco większe, przykładowo, możemy je ustalić następująco:
Macierz R może przyjąć następujące wartości:
Kod programu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include "opencv2/objdetect/objdetect.hpp" #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/video/tracking.hpp" #include <string> #include <iostream> using namespace cv; using namespace std; int mouse_x = 0, mouse_y = 0; void my_mouse_callback( int event, int x, int y, int flags, void* param ) { mouse_x = x; mouse_y = y; } int main() { const string window_name = "OpenCV Kalman"; Mat kalman_img = Mat::zeros( Size(600, 600), CV_8UC3 ); namedWindow(window_name, CV_WINDOW_AUTOSIZE); setMouseCallback(window_name, my_mouse_callback ); Point last_mouse = Point(mouse_x,mouse_y); Point last_pred = last_mouse; Point last_est = last_mouse; //Utworzenie i inicjalizacja filtru kalmana argumenty konstruktora : //4 parametry w równaniu stanu : położenie 2D oraz prędkoć 2D //2 wielkoci mierzone : połozenie x oraz y //0 brak wejcia ( macierz B ) KalmanFilter Filter(4, 2, 0); //Inicjalizacja macierzy przejcia A Filter.transitionMatrix = *(Mat_<float>(4, 4) << 1,0,1,0, 0,1,0,1, 0,0,1,0, 0,0,0,1); //cout << Filter.transitionMatrix; //Inicjalizacja wktora stanu z fazy predykcji Filter.statePre.at<float>(0) = mouse_x; //Położenie x Filter.statePre.at<float>(1) = mouse_y; //Położenie y Filter.statePre.at<float>(2) = 0; //Prędkosc x Filter.statePre.at<float>(3) = 0; //Prędkosc y //cout << Filter.statePre; //Inicjalizacja pozostałych macierzy //Macierz H zostanie tak zainicjalizowana by pierwsze dwie wielkosci //x oraz y byly mierzone setIdentity(Filter.measurementMatrix); //cout << Filter.measurementMatrix; setIdentity(Filter.processNoiseCov, Scalar::all(1e-4)); //Kowariancja Q //cout << Filter.processNoiseCov; setIdentity(Filter.measurementNoiseCov, Scalar::all(1e-1)); //Kowariancja R //cout << Filter.measurementNoiseCov; setIdentity(Filter.errorCovPost, Scalar::all(.1)); //Wyżej inicjalizacja macierzy P ( estymowanej - P(k)=(I-K(k)*H)*P'(k) ) //Która bęzie potrzebna do obliczenie macierzy P z fazy predykcji //cout << Filter.errorCovPost; Mat_<float> meas_mouse(2,1); //Miejsce na dane z czujników while ( waitKey(30) != 27 ) { meas_mouse(0) = mouse_x; meas_mouse(1) = mouse_y; //Faza Predykcji Filter.predict(); //Faza korekcji Mat estimated = Filter.correct(meas_mouse); Point estPt(estimated.at<float>(0),estimated.at<float>(1)); line( kalman_img, last_est, estPt, Scalar(50, 190, 20), 2, 8, 0); line( kalman_img, last_mouse, Point(meas_mouse(0), meas_mouse(1)), Scalar(250, 0, 125), 2, 8, 0); imshow(window_name, kalman_img); last_mouse = Point(meas_mouse(0), meas_mouse(1)); last_est = estPt; } return 0; } |
Jest dobrze skomentowany, myślę że nie powinno być większych problemów z jego zrozumieniem, tym bardziej, że w OpenCV algorytm jest niemal realizowanym automatycznie, a główne zadanie sprowadza się do opisania procesu odpowiednimi równaniami. Usuwając znaki komentarza w liniach typu jak poniżej, można zobaczyć jak wyglądają poszczególne macierze.
1 |
//cout << Filter.processNoiseCov; |
Efekt działania programu:
Filtr nie jest idealny, szczególnie widać to przy szybkich ruchach myszką, myślę jednak że do celów szkoleniowych jest wystarczający, szybkość reakcji można zwiększyć dodając np. do naszego równania stanu przyspieszenie jako kolejny parametr.
Podsumowanie
Mam nadzieję, że artykuł rozjaśnił nieco zawiłości filtru Kalmana. Jeśli są jakieś wątpliwości, coś nie jest wystarczająco dokładnie wytłumaczone proszę o informacje. Postaram się też w najbliższym czasie dorzucić kody w c++ do dwóch pierwszych przykładów.
Autor: Elektryk0
Data publikacji na forum: 01-10-2012
To nie koniec, sprawdź również
Przeczytaj powiązane artykuły oraz aktualnie popularne wpisy lub losuj inny artykuł »
Trwa ładowanie komentarzy...