Spisu treści:

Autonomiczny samochód do utrzymania pasa ruchu przy użyciu Raspberry Pi i OpenCV: 7 kroków (ze zdjęciami)
Autonomiczny samochód do utrzymania pasa ruchu przy użyciu Raspberry Pi i OpenCV: 7 kroków (ze zdjęciami)

Wideo: Autonomiczny samochód do utrzymania pasa ruchu przy użyciu Raspberry Pi i OpenCV: 7 kroków (ze zdjęciami)

Wideo: Autonomiczny samochód do utrzymania pasa ruchu przy użyciu Raspberry Pi i OpenCV: 7 kroków (ze zdjęciami)
Wideo: Jak to działa? - Asystent pasa ruchu - www.motomaniacy.tv 2024, Lipiec
Anonim
Autonomiczny samochód do utrzymania pasa ruchu przy użyciu Raspberry Pi i OpenCV
Autonomiczny samochód do utrzymania pasa ruchu przy użyciu Raspberry Pi i OpenCV

W tej instrukcji zostanie wdrożony autonomiczny robot utrzymujący pas ruchu, który przejdzie przez następujące kroki:

  • Zbieranie części
  • Wymagania wstępne dotyczące instalacji oprogramowania
  • Montaż sprzętu
  • Pierwszy test
  • Wykrywanie linii pasów i wyświetlanie linii prowadzącej za pomocą openCV
  • Wdrażanie kontrolera PD
  • Wyniki

Krok 1: Zbieranie komponentów

Zbieranie komponentów
Zbieranie komponentów
Zbieranie komponentów
Zbieranie komponentów
Zbieranie komponentów
Zbieranie komponentów
Zbieranie komponentów
Zbieranie komponentów

Powyższe zdjęcia pokazują wszystkie komponenty użyte w tym projekcie:

  • Samochód RC: kupiłem go w lokalnym sklepie w moim kraju. Jest wyposażony w 3 silniki (2 do dławienia i 1 do sterowania). Główną wadą tego samochodu jest to, że kierowanie jest ograniczone pomiędzy „brak skrętu” a „pełne kierowanie”. Innymi słowy, nie może sterować pod określonym kątem, w przeciwieństwie do samochodów RC z serwosterowaniem. Możesz znaleźć podobny zestaw samochodowy zaprojektowany specjalnie dla raspberry pi tutaj.
  • Raspberry pi 3 model b+: to mózg samochodu, który poradzi sobie z wieloma etapami przetwarzania. Opiera się na czterordzeniowym 64-bitowym procesorze o taktowaniu 1,4 GHz. Ja mam stąd.
  • Moduł kamery Raspberry pi 5 mp: Obsługuje nagrywanie 1080p @ 30 fps, 720p @ 60 fps i 640x480p 60/90. Obsługuje również interfejs szeregowy, który można podłączyć bezpośrednio do raspberry pi. Nie jest to najlepsza opcja dla aplikacji do przetwarzania obrazu, ale wystarcza do tego projektu i jest bardzo tania. Mam stąd swój.
  • Sterownik silnika: Służy do sterowania kierunkami i prędkościami silników prądu stałego. Obsługuje sterowanie 2 silnikami prądu stałego na 1 płytce i może wytrzymać 1,5 A.
  • Power Bank (opcjonalnie): Użyłem banku mocy (o napięciu 5 V, 3 A) do osobnego zasilania Raspberry Pi. Do zasilania Raspberry Pi z 1 źródła należy zastosować konwerter obniżający napięcie (przetwornik buck: prąd wyjściowy 3A).
  • Akumulator LiPo 3s (12 V): Akumulatory litowo-polimerowe są znane z doskonałej wydajności w dziedzinie robotyki. Służy do zasilania sterownika silnika. Kupiłem swoją stąd.
  • Przewody połączeniowe męskie-męskie i żeńskie-żeńskie.
  • Taśma dwustronna: Służy do montażu elementów w samochodzie RC.
  • Niebieska taśma: Jest to bardzo ważny element tego projektu, służy do wykonania dwóch linii pasów, pomiędzy którymi samochód będzie się poruszał. Możesz wybrać dowolny kolor, ale polecam wybrać kolory inne niż w otoczeniu.
  • Opaski na suwak i drewniane drążki.
  • Śrubokręt.

Krok 2: Instalacja OpenCV na Raspberry Pi i konfiguracja zdalnego wyświetlacza

Instalowanie OpenCV na Raspberry Pi i konfigurowanie zdalnego wyświetlacza
Instalowanie OpenCV na Raspberry Pi i konfigurowanie zdalnego wyświetlacza

Ten krok jest trochę irytujący i zajmie trochę czasu.

OpenCV (Open source Computer Vision) to biblioteka oprogramowania do wizji komputerowej i uczenia maszynowego typu open source. Biblioteka posiada ponad 2500 zoptymalizowanych algorytmów. Postępuj zgodnie z TYM bardzo prostym przewodnikiem, aby zainstalować openCV na swoim raspberry pi, a także zainstalować system operacyjny raspberry pi (jeśli nadal tego nie zrobiłeś). Należy pamiętać, że proces budowania openCV może zająć około 1,5 godziny w dobrze wychłodzonym pomieszczeniu (ponieważ temperatura procesora będzie bardzo wysoka!), więc napij się herbaty i czekaj cierpliwie:D.

W przypadku zdalnego wyświetlacza postępuj zgodnie z TYM przewodnikiem, aby skonfigurować zdalny dostęp do raspberry pi z urządzenia z systemem Windows/Mac.

Krok 3: Łączenie części razem

Łączenie części razem
Łączenie części razem
Łączenie części razem
Łączenie części razem
Łączenie części razem
Łączenie części razem

Powyższe zdjęcia pokazują połączenia między raspberry pi, modułem kamery i sterownikiem silnika. Należy pamiętać, że silniki, których użyłem, pochłaniają 0,35 A przy 9 V każdy, co sprawia, że sterownik silnika może bezpiecznie obsługiwać 3 silniki jednocześnie. A ponieważ chcę sterować prędkością 2 silników dławiących (1 z tyłu i 1 z przodu) dokładnie w ten sam sposób, podłączyłem je do tego samego portu. Zamontowałem sterownik silnika po prawej stronie auta za pomocą podwójnej taśmy. Jeśli chodzi o moduł kamery, włożyłem opaskę suwakową między otwory na śruby, jak pokazuje powyższy obrazek. Następnie dopasowuję kamerę do drewnianego drążka, aby móc dostosować pozycję kamery do własnych potrzeb. Postaraj się zainstalować kamerę na środku samochodu tak bardzo, jak to możliwe. Zalecam umieszczenie kamery co najmniej 20 cm nad ziemią, aby pole widzenia przed samochodem było lepsze. Schemat Fritzing znajduje się poniżej.

Krok 4: Pierwszy test

Pierwszy test
Pierwszy test
Pierwszy test
Pierwszy test

Testowanie kamery:

Po zainstalowaniu kamery i zbudowaniu biblioteki openCV nadszedł czas na przetestowanie naszego pierwszego obrazu! Zrobimy zdjęcie z pi cam i zapiszemy je jako "original.jpg". Można to zrobić na 2 sposoby:

1. Za pomocą poleceń terminala:

Otwórz nowe okno terminala i wpisz następujące polecenie:

raspistill -o oryginalny.jpg

Spowoduje to zrobienie nieruchomego obrazu i zapisanie go w katalogu "/pi/original.jpg".

2. Korzystanie z dowolnego IDE Pythona (ja używam IDLE):

Otwórz nowy szkic i napisz następujący kod:

importuj cv2

video = cv2. VideoCapture(0) while True: ret, frame = video.read() frame = cv2.flip(frame, -1) # używane do odwracania obrazu w pionie cv2.imshow('original', frame) cv2. imwrite('original.jpg', frame) key = cv2.waitKey(1) if key == 27: przerwij video.release() cv2.destroyAllWindows()

Zobaczmy, co się stało w tym kodzie. Pierwsza linia to zaimportowanie naszej biblioteki openCV, aby móc korzystać ze wszystkich jej funkcji. funkcja VideoCapture(0) rozpoczyna strumieniowanie wideo na żywo ze źródła określonego przez tę funkcję, w tym przypadku jest to 0, co oznacza kamerę raspi. jeśli masz wiele kamer, należy umieścić różne numery. video.read() odczyta każdą klatkę pochodzącą z kamery i zapisze ją w zmiennej o nazwie "ramka". Funkcja flip() odwróci obraz względem osi y (w pionie), ponieważ montuję kamerę odwrotnie. imshow() wyświetli nasze ramki z nagłówkiem "original", a imwrite() zapisze nasze zdjęcie jako original.jpg. waitKey(1) odczeka 1 ms na naciśnięcie dowolnego przycisku klawiatury i zwróci swój kod ASCII. jeśli zostanie naciśnięty przycisk escape (esc), zwracana jest wartość dziesiętna 27 i odpowiednio przerywa pętlę. video.release() zatrzyma nagrywanie, a destroyAllWindows() zamknie każdy obraz otwarty przez funkcję imshow().

Polecam przetestować swoje zdjęcie drugą metodą, aby zapoznać się z funkcjami openCV. Obraz jest zapisywany w katalogu "/pi/original.jpg". Powyżej pokazano oryginalne zdjęcie, które zrobił mój aparat.

Testowanie silników:

Ten krok jest niezbędny do określenia kierunku obrotów każdego silnika. Najpierw zróbmy krótkie wprowadzenie do zasady działania sterownika silnika. Powyższy obrazek pokazuje pin-out sterownika silnika. Zezwolenie A, wejście 1 i wejście 2 są powiązane ze sterowaniem silnika A. Zezwolenie B, wejście 3 i wejście 4 są powiązane ze sterowaniem silnika B. Sterowanie kierunkiem jest ustalane przez część „Wejście”, a sterowanie prędkością przez część „Włącz”. Na przykład, aby sterować kierunkiem silnika A, ustaw Wejście 1 na WYSOKIE (w tym przypadku 3,3 V, ponieważ używamy Raspberry Pi) i ustaw Wejście 2 na NISKI, silnik będzie się obracał w określonym kierunku i ustawiając przeciwne wartości do wejścia 1 i wejścia 2, silnik będzie się obracał w przeciwnym kierunku. Jeżeli wejście 1 = wejście 2 = (HIGH lub LOW), silnik się nie obraca. Piny Enable pobierają sygnał wejściowy modulacji szerokości impulsu (PWM) z Raspberry (0 do 3,3 V) i odpowiednio uruchamiają silniki. Na przykład sygnał 100% PWM oznacza, że pracujemy z maksymalną prędkością, a sygnał 0% PWM oznacza, że silnik się nie obraca. Poniższy kod służy do określania kierunków silników i testowania ich prędkości.

czas importu

import RPi. GPIO jako GPIO GPIO.setwarnings(False) # Piny silnika sterującego Steering_enable = 22 # Pin fizyczny 15 in1 = 17 # Pin fizyczny 11 in2 = 27 # Pin fizyczny 13 # Piny silników przepustnicy throttle_enable = 25 # Pin fizyczny 22 in3 = 23 # Physical Pin 16 in4 = 24 # Physical Pin 18 GPIO.setmode(GPIO. BCM) # Użyj numeracji GPIO zamiast numeracji fizycznej GPIO.setup(in1, GPIO.out) GPIO.setup(in2, GPIO.out) GPIO. setup(in3, GPIO.out) GPIO.setup(in4, GPIO.out) GPIO.setup(throttle_enable, GPIO.out) GPIO.setup(steering_enable, GPIO.out) # Sterowanie silnikiem sterującym GPIO.output(in1, GPIO. HIGH) GPIO.output(in2, GPIO. LOW) Steering = GPIO. PWM(steering_enable, 1000) # ustaw częstotliwość przełączania na 1000 Hz Steering.stop() # Throttle Motors Control GPIO.output(in3, GPIO. HIGH) GPIO.output(in4, GPIO. LOW) throttle = GPIO. PWM(throttle_enable, 1000) # ustaw częstotliwość przełączania na 1000 Hz throttle.stop() time.sleep(1) throttle.start(25) # uruchamia silnik na 25 % sygnału PWM->(0,25*napięcie akumulatora) - kierowcy loss Steering.start(100) # uruchamia silnik przy 100% sygnale PWM-> (1* Napięcie akumulatora) - strata czasu.snu kierowcy(3) throttle.stop() ster.stop()

Ten kod uruchomi silniki dławiące i silnik sterujący na 3 sekundy, a następnie je zatrzyma. Stratę (stratę kierowcy) można określić za pomocą woltomierza. Na przykład wiemy, że sygnał 100% PWM powinien dawać pełne napięcie akumulatora na zacisku silnika. Ale ustawiając PWM na 100% stwierdziłem, że sterownik powoduje spadek o 3 V, a silnik otrzymuje 9 V zamiast 12 V (dokładnie to, czego potrzebuję!). Strata nie jest liniowa, tj. strata przy 100% różni się bardzo od straty przy 25%. Po uruchomieniu powyższego kodu moje wyniki były następujące:

Wyniki dławienia: jeśli in3 = WYSOKA i in4 = NISKA, silniki dławiące będą miały obrót zgodny z ruchem wskazówek zegara (CW), tj. samochód będzie jechał do przodu. W przeciwnym razie samochód cofnie się.

Wyniki kierowania: jeśli in1 = HIGH i in2 = LOW, silnik kierownicy skręci maksymalnie w lewo, tj. samochód skręci w lewo. W przeciwnym razie samochód skręci w prawo. Po kilku eksperymentach stwierdziłem, że silnik sterujący nie będzie się obracał, jeśli sygnał PWM nie będzie wynosił 100% (tj. Silnik będzie skręcał całkowicie w prawo lub całkowicie w lewo).

Krok 5: Wykrywanie linii pasów i obliczanie linii kursu

Wykrywanie linii pasa i obliczanie linii kursu
Wykrywanie linii pasa i obliczanie linii kursu
Wykrywanie linii pasa i obliczanie linii kursu
Wykrywanie linii pasa i obliczanie linii kursu
Wykrywanie linii pasa i obliczanie linii kursu
Wykrywanie linii pasa i obliczanie linii kursu

W tym kroku wyjaśniony zostanie algorytm, który będzie sterował ruchem samochodu. Pierwszy obraz przedstawia cały proces. Wejściem systemu są obrazy, wyjściem jest theta (kąt skrętu w stopniach). Zwróć uwagę, że przetwarzanie odbywa się na 1 obrazie i będzie powtarzane we wszystkich klatkach.

Kamera:

Kamera rozpocznie nagrywanie wideo w rozdzielczości (320 x 240). Zalecam obniżenie rozdzielczości, aby uzyskać lepszą liczbę klatek na sekundę (kl./s), ponieważ spadek liczby klatek na sekundę nastąpi po zastosowaniu technik przetwarzania do każdej klatki. Poniższy kod będzie główną pętlą programu i doda każdy krok w tym kodzie.

importuj cv2

import numpy as np video = cv2. VideoCapture(0) video.set(cv2. CAP_PROP_FRAME_WIDTH, 320) # ustaw szerokość na 320 p video.set(cv2. CAP_PROP_FRAME_HEIGHT, 240) # ustaw wysokość na 240 p # Pętla podczas True: ret, frame = video.read() frame = cv2.flip(frame, -1) cv2.imshow("original", frame) key = cv2.waitKey(1) if key == 27: przerwij video.release () cv2.destroyAllWindows()

Kod tutaj pokaże oryginalny obraz uzyskany w kroku 4 i jest pokazany na powyższych obrazach.

Konwertuj na przestrzeń kolorów HSV:

Teraz po zrobieniu nagrania wideo jako klatek z kamery, następnym krokiem jest przekonwertowanie każdej klatki na przestrzeń kolorów Barwa, Nasycenie i Wartość (HSV). Główną zaletą takiego rozwiązania jest możliwość rozróżnienia kolorów na podstawie ich poziomu luminancji. A oto dobre wyjaśnienie przestrzeni kolorów HSV. Konwersja do HSV odbywa się za pomocą następującej funkcji:

def konwertuj_na_HSV(ramka):

hsv = cv2.cvtColor(ramka, cv2. COLOR_BGR2HSV) cv2.imshow("HSV", hsv) return hsv

Ta funkcja zostanie wywołana z głównej pętli i zwróci klatkę w przestrzeni kolorów HSV. Klatka uzyskana przeze mnie w przestrzeni kolorów HSV jest pokazana powyżej.

Wykryj niebieski kolor i krawędzie:

Po przekonwertowaniu obrazu do przestrzeni kolorów HSV, nadszedł czas na wykrycie tylko koloru, który nas interesuje (czyli koloru niebieskiego, ponieważ jest to kolor linii pasa). Aby wyodrębnić niebieski kolor z ramki HSV, należy określić zakres odcienia, nasycenia i wartości. patrz tutaj, aby mieć lepszy pomysł na wartości HSV. Po kilku eksperymentach górna i dolna granica koloru niebieskiego są pokazane w poniższym kodzie. Aby zmniejszyć ogólne zniekształcenia w każdej klatce, krawędzie są wykrywane tylko za pomocą inteligentnego detektora krawędzi. Więcej o canny edge znajdziesz tutaj. Zasadą jest wybór parametrów funkcji Canny() w stosunku 1:2 lub 1:3.

def detect_edges(ramka):

dolny_niebieski = np. tablica([90, 120, 0], dtype = "uint8") # dolny limit koloru niebieskiego górny_niebieski = np. tablica([150, 255, 255], dtype="uint8") # górny limit blue color mask = cv2.inRange(hsv, lower_blue, upper_blue) # ta maska odfiltruje wszystko poza niebieskim # wykrywanie krawędzi krawędzi = cv2. Canny(mask, 50, 100) cv2.imshow("edges", edge) return edge

Ta funkcja będzie również wywoływana z głównej pętli, która przyjmuje jako parametr ramkę przestrzeni kolorów HSV i zwraca ramkę z krawędziami. Obramowana ramka, którą uzyskałem, znajduje się powyżej.

Wybierz region zainteresowania (ROI):

Wybranie obszaru zainteresowania jest kluczowe, aby skupić się tylko na jednym obszarze kadru. W tym przypadku nie chcę, aby samochód widział wiele przedmiotów w otoczeniu. Chcę tylko, żeby samochód skupiał się na liniach pasa i ignorował wszystko inne. PS: układ współrzędnych (osie x i y) zaczyna się od lewego górnego rogu. Innymi słowy, punkt (0, 0) zaczyna się od lewego górnego rogu. oś y to wysokość, a oś x to szerokość. Poniższy kod wybiera interesujący obszar, aby skupić się tylko na dolnej połowie klatki.

def region_zainteresowania(krawędzie):

height, width = edge.shape # wyodrębnij wysokość i szerokość krawędzi maska ramki = np.zeros_like(edges) # utwórz pustą macierz o tych samych wymiarach krawędzi ramki # zaznacz tylko dolną połowę ekranu # określ współrzędne 4 punkty (lewy dolny, lewy górny, prawy górny, prawy dolny) polygon = np.array(

Ta funkcja przyjmie ramkę z krawędziami jako parametr i narysuje wielokąt z 4 wstępnie ustawionymi punktami. Skoncentruje się tylko na tym, co znajduje się wewnątrz wielokąta, i zignoruje wszystko poza nim. Ramka mojego obszaru zainteresowania jest pokazana powyżej.

Wykryj segmenty linii:

Transformacja Hough służy do wykrywania segmentów linii z ramki o krawędziach. Transformacja Hough to technika wykrywania dowolnego kształtu w formie matematycznej. Może wykryć prawie każdy obiekt, nawet jeśli jest zniekształcony zgodnie z pewną liczbą głosów. tutaj pokazano świetne odniesienie do przekształcenia Hougha. W tej aplikacji funkcja cv2. HoughLinesP() służy do wykrywania linii w każdej ramce. Ważnymi parametrami, jakie przyjmuje ta funkcja, są:

cv2. HoughLinesP(ramka, rho, theta, min_threshold, minLineLength, maxLineGap)

  • Ramka: to ramka, w której chcemy wykryć linie.
  • rho: Jest to dokładność odległości w pikselach (zwykle = 1)
  • theta: precyzja kątowa w radianach (zawsze = np.pi/180 ~ 1 stopień)
  • min_threshold: minimalny głos, jaki powinien otrzymać, aby został uznany za linię
  • minLineLength: minimalna długość linii w pikselach. Każda linia krótsza niż ta liczba nie jest uważana za linię.
  • maxLineGap: maksymalna przerwa w pikselach między 2 liniami, która ma być traktowana jako 1 linia. (Nie jest używany w moim przypadku, ponieważ linie pasa, których używam, nie mają żadnych przerw).

Ta funkcja zwraca punkty końcowe linii. Poniższa funkcja jest wywoływana z mojej głównej pętli, aby wykryć linie za pomocą przekształcenia Hough:

def detect_line_segments(przycięte_krawędzie):

rho = 1 theta = np.pi / 180 min_threshold = 10 line_segments = cv2. HoughLinesP(cropped_edges, rho, theta, min_threshold, np.array(), minLineLength=5, maxLineGap=0) return line_segments

Średnie nachylenie i przecięcie (m, b):

Przypomnijmy, że równanie linii jest dane przez y = mx + b. Gdzie m to nachylenie prostej, a b to punkt przecięcia z osią Y. W tej części obliczona zostanie średnia nachylenia i przecięcia odcinków linii wykrytych za pomocą przekształcenia Hougha. Zanim to zrobimy, spójrzmy na oryginalne zdjęcie ramki pokazane powyżej. Wydaje się, że lewy pas jedzie w górę, więc ma nachylenie ujemne (pamiętasz punkt początkowy układu współrzędnych?). Innymi słowy, linia lewego pasa ma x1 < x2 i y2 x1 i y2 > y1, co da dodatnie nachylenie. Tak więc wszystkie linie o dodatnim nachyleniu są uważane za punkty prawego pasa ruchu. W przypadku linii pionowych (x1 = x2) nachylenie będzie nieskończone. W takim przypadku pominiemy wszystkie pionowe linie, aby zapobiec wystąpieniu błędu. Aby zwiększyć dokładność tego wykrywania, każda ramka jest podzielona na dwa regiony (prawy i lewy) przez 2 linie graniczne. Wszystkie punkty szerokości (punkty osi x) większe niż prawa linia graniczna są powiązane z obliczeniami prawego pasa ruchu. A jeśli wszystkie punkty szerokości są mniejsze niż lewa linia graniczna, są one skojarzone z obliczeniami lewego pasa. Poniższa funkcja pobiera ramkę w trakcie przetwarzania i segmenty pasa ruchu wykryte za pomocą przekształcenia Hougha i zwraca średnie nachylenie i przecięcie dwóch linii pasa.

def przecięcie_średniego_slope(ramka, segmenty_linii):

lane_lines = jeśli line_segments to None: print("nie wykryto segmentu linii") return lane_lines wysokość, szerokość, _ = frame.shape left_fit = right_fit = border = left_region_boundary = szerokość * (1 - border) right_region_boundary = szerokość * granica dla segmentu_linii w segmentach_linii: dla x1, y1, x2, y2 w segmencie_linii: if x1 == x2: print("pomijanie linii pionowych (nachylenie = nieskończoność)") continue fit = np.polyfit((x1, x2), (y1, y2), 1) nachylenie = (y2 - y1) / (x2 - x1) punkt przecięcia = y1 - (nachylenie * x1), jeśli nachylenie < 0: jeśli x1 < lewa_granica_regionu i x2 prawa_granica_regionu i x2> prawy_obwiednia_regionu: prawy_obwiednia. append((nachylenie, przecięcie)) lewe_dopasowanie_średnia = np. średnia(lewe_dopasowanie, oś=0) if len(lewe_dopasowanie) > 0: linie_pasu.append(make_points(ramka, lewe_dopasowanie_średnia)) prawy_dopasowanie_średnia = np.średnia(prawe_dopasowanie, oś=0) if len(right_fit) > 0: lane_lines.append(make_points(frame, right_fit_average)) # lane_lines to dwuwymiarowa tablica zawierająca współrzędne linii lewego i prawego pasa # na przykład: lan e_lines =

make_points() to funkcja pomocnicza dla funkcji Average_slope_intercept(), która zwróci ograniczone współrzędne linii pasa (od dołu do środka ramki).

def make_points(ramka, linia):

wysokość, szerokość, _ = frame.shape nachylenie, przecięcie = linia y1 = wysokość # dół ramki y2 = int(y1 / 2) # wykonaj punkty od środka ramki w dół, jeśli nachylenie == 0: nachylenie = 0,1 x1 = int((y1 - punkt przecięcia) / nachylenie) x2 = int((y2 - punkt przecięcia) / nachylenie) return

Aby zapobiec dzieleniu przez 0, prezentowany jest warunek. Jeśli nachylenie = 0, co oznacza y1 = y2 (linia pozioma), nadaj nachyleniu wartość bliską 0. Nie wpłynie to na wydajność algorytmu, jak również zapobiegnie niemożliwym przypadkom (podzieleniu przez 0).

Aby wyświetlić linie pasów na ramkach, używana jest następująca funkcja:

def display_lines(ramka, linie, kolor_linii=(0, 255, 0), szerokość_linii=6): # kolor linii (B, G, R)

line_image = np.zeros_like(frame) jeśli linie nie są Brak: dla linii w liniach: dla x1, y1, x2, y2 w linii: cv2.line(line_image, (x1, y1), (x2, y2), line_color, line_width) line_image = cv2.addWeighted(ramka, 0.8, line_image, 1, 1) return line_image

Funkcja cv2.addWeighted() przyjmuje następujące parametry i służy do łączenia dwóch obrazów, ale z nadaniem każdemu z nich wagi.

cv2.addWeighted(obraz1, alfa, obraz2, beta, gamma)

I oblicza obraz wyjściowy za pomocą następującego równania:

wyjście = alfa * image1 + beta * image2 + gamma

Więcej informacji o funkcji cv2.addWeighted() można znaleźć tutaj.

Oblicz i wyświetl linię nagłówka:

To ostatni krok, zanim zastosujemy prędkości do naszych silników. Linia kursu jest odpowiedzialna za nadanie silnikowi sterowemu kierunku, w którym powinien się on obracać, oraz nadanie silnikom dławiącym prędkości, z jaką będą działać. Obliczanie linii kursu to czysta trygonometria, używane są funkcje trygonometryczne tan i atan (tan^-1). W skrajnych przypadkach kamera wykrywa tylko jedną linię pasa lub gdy nie wykrywa żadnej linii. Wszystkie te przypadki są pokazane w następującej funkcji:

def get_steering_angle(rama, linie_pasu):

height, width, _ = frame.shape if len(lane_lines) == 2: # jeśli wykryto dwie linie toru _, _, left_x2, _ = lane_lines[0][0] # wyodrębnij lewe x2 z tablicy lane_lines array _, _, right_x2, _ = linie_pasu[1][0] # wyodrębnij prawy x2 z tablicy linie_pasu mid = int(szerokość / 2) x_offset = (left_x2 + right_x2) / 2 - mid y_offset = int(height / 2) elif len(lane_lines) == 1: # jeśli wykryta zostanie tylko jedna linia x1, _, x2, _ = lane_lines[0][0] x_offset = x2 - x1 y_offset = int(height / 2) elif len(lane_lines) == 0: # jeśli nie zostanie wykryta żadna linia x_offset = 0 y_offset = int(height / 2) angle_to_mid_radian = math.atan(x_offset / y_offset) angle_to_mid_deg = int(angle_to_mid_radian * 180.0 / math.pi) Steering_angle = angle_to_mid_return_deg +

x_offset w pierwszym przypadku to jak bardzo średnia ((prawy x2 + lewy x2) / 2) różni się od środka ekranu. y_offset jest zawsze przyjmowany jako wysokość / 2. Ostatni obrazek powyżej pokazuje przykład linii nagłówka. angle_to_mid_radians to to samo co "theta" pokazane na ostatnim obrazku powyżej. Jeśli Steering_angle = 90, oznacza to, że samochód ma linię kursu prostopadłą do linii „wysokość/2” i samochód będzie jechał do przodu bez kierowania. Jeśli kąt_sterowania > 90, samochód powinien skręcić w prawo, w przeciwnym razie powinien skręcić w lewo. Do wyświetlenia linii nagłówka służy następująca funkcja:

def linia_nagłówka_wyświetlania(ramka, kąt_sterowania, kolor_linii=(0, 0, 255), szerokość_linii=5)

obraz_nagłówka = np.zeros_like(ramka) wysokość, szerokość, _ = ramka.kształt kąt_sterowania_radian = kąt_sterowania / 180,0 * math.pi x1 = int(szerokość / 2) y1 = wysokość x2 = int(x1 - wysokość / 2 / math.tan (promień_kąta_sterowania)) y2 = int(wysokość / 2) cv2.line(obraz_nagłówka, (x1, y1), (x2, y2), kolor_linii, szerokość_linii) obraz_nagłówka = cv2.addWeighted(ramka, 0.8, obraz_nagłówka, 1, 1) zwróć obraz_główny

Powyższa funkcja przyjmuje jako dane wejściowe ramkę, na której zostanie narysowana linia kursu i kąt skrętu. Zwraca obraz linii nagłówka. Ramka linii nagłówka wykonana w moim przypadku jest pokazana na powyższym obrazku.

Łączenie całego kodu razem:

Kod jest teraz gotowy do złożenia. Poniższy kod pokazuje główną pętlę programu wywołującą każdą funkcję:

importuj cv2

importuj numpy jako np video = cv2. VideoCapture(0) video.set(cv2. CAP_PROP_FRAME_WIDTH, 320) video.set(cv2. CAP_PROP_FRAME_HEIGHT, 240) while True: ret, frame = video.read() frame = cv2.flip(frame, -1) #Wywołanie funkcji hsv = konwertuj_na_HSV(ramkę) krawędzie = wykryj_krawędzie(hsv) roi = region_zainteresowania(krawędzie) line_segments = detect_line_segments(roi) lane_lines = średnia_slope_intercept(frame, line_segments) lane_lines_lines_image_setting_line_lines = get_steering_angle(frame, lane_lines) header_image = display_heading_line(lane_lines_image, Steering_angle) key = cv2.waitKey(1) if key == 27: break video.release() cv2.destroyAllWindows()

Krok 6: Stosowanie kontroli wyładowań niezupełnych

Stosowanie kontroli wyładowań niezupełnych
Stosowanie kontroli wyładowań niezupełnych

Teraz mamy gotowy kąt skrętu do podania do silników. Jak wspomniano wcześniej, jeśli kąt skrętu jest większy niż 90, samochód powinien skręcić w prawo, w przeciwnym razie powinien skręcić w lewo. Zastosowałem prosty kod, który skręca kierownicę w prawo, jeśli kąt jest większy niż 90 i skręca w lewo, jeśli kąt skrętu jest mniejszy niż 90 przy stałej prędkości dławienia (10% PWM), ale mam dużo błędów. Główny błąd, który otrzymałem, polega na tym, że gdy samochód zbliża się do dowolnego zakrętu, silnik sterujący działa bezpośrednio, ale silniki dławiące się zacinają. Próbowałem zwiększyć prędkość dławienia do (20% PWM) na zakrętach, ale skończyłem, gdy robot zszedł z pasów. Potrzebowałem czegoś, co znacznie zwiększa prędkość dławienia, jeśli kąt skrętu jest bardzo duży i zwiększa prędkość, jeśli kąt skrętu nie jest zbyt duży, a następnie zmniejsza prędkość do wartości początkowej, gdy samochód zbliża się do 90 stopni (jazda na wprost). Rozwiązaniem było zastosowanie kontrolera PD.

Regulator PID oznacza regulator proporcjonalny, całkujący i różniczkujący. Ten typ kontrolerów liniowych jest szeroko stosowany w aplikacjach robotyki. Powyższy rysunek przedstawia typową pętlę sterowania sprzężeniem zwrotnym PID. Celem tego sterownika jest osiągnięcie „wartości zadanej” w najbardziej efektywny sposób, w przeciwieństwie do sterowników „włącz-wyłącz”, które włączają lub wyłączają instalację w zależności od pewnych warunków. Niektóre słowa kluczowe powinny być znane:

  • Wartość zadana: jest pożądaną wartością, którą system ma osiągnąć.
  • Wartość rzeczywista: to rzeczywista wartość odczytana przez czujnik.
  • Błąd: to różnica między wartością zadaną a wartością rzeczywistą (błąd = wartość zadana - wartość rzeczywista).
  • Zmienna kontrolowana: od nazwy zmienna, którą chcesz kontrolować.
  • Kp: Stała proporcjonalności.
  • Ki: stała całkowa.
  • Kd: stała pochodnej.

W skrócie pętla systemu regulacji PID działa w następujący sposób:

  • Użytkownik określa nastawę potrzebną do osiągnięcia przez system.
  • Obliczany jest błąd (błąd = wartość zadana - aktualna).
  • Regulator P generuje akcję proporcjonalną do wartości błędu. (wzrost błędu, akcja P również wzrasta)
  • Kontroler zintegruje błąd w czasie, co eliminuje błąd stanu ustalonego systemu, ale zwiększa jego przeregulowanie.
  • Kontroler D jest po prostu pochodną czasu błędu. Innymi słowy, jest to nachylenie błędu. Wykonuje akcję proporcjonalną do pochodnej błędu. Ten kontroler zwiększa stabilność systemu.
  • Wyjście kontrolera będzie sumą trzech kontrolerów. Wyjście kontrolera stanie się 0, jeśli błąd stanie się 0.

Świetne wyjaśnienie działania regulatora PID można znaleźć tutaj.

Wracając do samochodu utrzymującego pas ruchu, moją kontrolowaną zmienną była prędkość przepustnicy (ponieważ układ kierowniczy ma tylko dwa stany, prawy lub lewy). Do tego celu używany jest kontroler PD, ponieważ akcja D znacznie zwiększa prędkość dławienia, jeśli zmiana błędu jest bardzo duża (tj. duże odchylenie) i spowalnia samochód, jeśli ta zmiana błędu zbliża się do 0. Wykonałem następujące kroki, aby zaimplementować PD kontroler:

  • Ustaw wartość zadaną na 90 stopni (zawsze chcę, aby samochód jechał prosto)
  • Obliczono kąt odchylenia od środka
  • Odchylenie daje dwie informacje: jak duży jest błąd (wielkość odchylenia) i w jakim kierunku ma podążać silnik sterujący (oznaczenie odchylenia). Jeśli odchylenie jest dodatnie, samochód powinien skręcić w prawo, w przeciwnym razie powinien skręcić w lewo.
  • Ponieważ odchylenie jest ujemne lub dodatnie, zdefiniowana jest zmienna „błąd”, która zawsze jest równa wartości bezwzględnej odchylenia.
  • Błąd jest mnożony przez stałą Kp.
  • Błąd podlega zróżnicowaniu czasowemu i jest mnożony przez stałą Kd.
  • Prędkość silników jest aktualizowana i pętla zaczyna się od nowa.

Poniższy kod jest używany w pętli głównej do sterowania prędkością silników dławiących:

prędkość = 10 # prędkość robocza w % PWM

#Zmienne do aktualizacji w każdej pętli lastTime = 0 lastError = 0 # stałe PD Kp = 0,4 Kd = Kp * 0,65 While True: now = time.time() # bieżąca zmienna czasu dt = now - lastTime odchylenie = kąt_sterowania - 90 # odpowiednik to angle_to_mid_deg zmienna error = abs(odchylenie) jeśli odchylenie -5: # nie steruj jeśli występuje 10-stopniowy zakres błędu odchylenie = 0 error = 0 GPIO.output(in1, GPIO. LOW) GPIO.output(in2, GPIO. LOW) Steering.stop() elif odchylenie > 5: # skręć w prawo jeśli odchylenie jest dodatnie GPIO.output(in1, GPIO. LOW) GPIO.output(in2, GPIO. HIGH) Steering.start(100) elif odchylenie < -5: # skręć w lewo jeśli odchylenie jest ujemne GPIO.output(in1, GPIO. HIGH) GPIO.output(in2, GPIO. LOW) Steering.start(100) pochodna = kd * (błąd - lastError) / dt proporcjonalny = kp * błąd PD = int(prędkość + pochodna + proporcjonalna) spd = abs(PD) jeśli spd> 25: spd = 25 throttle.start(spd) lastError = błąd lastTime = time.time()

Jeśli błąd jest bardzo duży (odchylenie od środka jest duże), akcje proporcjonalne i różniczkujące są duże, co skutkuje dużą prędkością dławienia. Gdy błąd zbliża się do 0 (odchylenie od środka jest małe), akcja różniczkowania działa odwrotnie (nachylenie jest ujemne), a prędkość dławienia zmniejsza się, aby utrzymać stabilność systemu. Pełny kod znajduje się poniżej.

Krok 7: Wyniki

Powyższe filmy przedstawiają uzyskane przeze mnie wyniki. Potrzebuje więcej dostrojenia i dalszych regulacji. Podłączałem raspberry pi do ekranu LCD, ponieważ przesyłanie strumieniowe wideo przez moją sieć miało duże opóźnienia i było bardzo frustrujące, dlatego w filmie są podłączone przewody do raspberry pi. Do narysowania toru użyłem płyt piankowych.

Czekam na Wasze rekomendacje, aby ulepszyć ten projekt! Ponieważ mam nadzieję, że ta instrukcja była wystarczająco dobra, aby dostarczyć ci nowych informacji.

Zalecana: