Sommario:

Auto autonoma che mantiene la corsia utilizzando Raspberry Pi e OpenCV: 7 passaggi (con immagini)
Auto autonoma che mantiene la corsia utilizzando Raspberry Pi e OpenCV: 7 passaggi (con immagini)

Video: Auto autonoma che mantiene la corsia utilizzando Raspberry Pi e OpenCV: 7 passaggi (con immagini)

Video: Auto autonoma che mantiene la corsia utilizzando Raspberry Pi e OpenCV: 7 passaggi (con immagini)
Video: Tesla ci salva la vita! sistemi di sicurezza in tesla model 3, autopilot e cambio corsia automatico! 2024, Luglio
Anonim
Auto autonoma per il mantenimento della corsia utilizzando Raspberry Pi e OpenCV
Auto autonoma per il mantenimento della corsia utilizzando Raspberry Pi e OpenCV

In questo istruibile, verrà implementato un robot autonomo per il mantenimento della corsia e passerà attraverso i seguenti passaggi:

  • Raccolta di parti
  • Installazione dei prerequisiti del software
  • Assemblaggio hardware
  • Primo test
  • Rilevamento delle linee di corsia e visualizzazione della linea guida utilizzando openCV
  • Implementazione di un controller PD
  • Risultati

Passaggio 1: raccolta dei componenti

Raccolta di componenti
Raccolta di componenti
Raccolta di componenti
Raccolta di componenti
Raccolta di componenti
Raccolta di componenti
Raccolta di componenti
Raccolta di componenti

Le immagini sopra mostrano tutti i componenti utilizzati in questo progetto:

  • Auto RC: ho preso la mia da un negozio locale nel mio paese. È dotato di 3 motori (2 per il throttling e 1 per lo sterzo). Lo svantaggio principale di questa vettura è che lo sterzo è limitato tra "nessuna sterzata" e "sterzo completo". In altre parole, non può sterzare con un angolo specifico, a differenza delle auto RC servosterzanti. Puoi trovare kit per auto simili progettati appositamente per lampone pi da qui.
  • Raspberry pi 3 modello b+: questo è il cervello dell'auto che gestirà molte fasi di lavorazione. Si basa su un processore quad core a 64 bit con clock a 1,4 GHz. Ho preso il mio da qui.
  • Modulo fotocamera Raspberry pi 5 mp: supporta la registrazione 1080p @ 30 fps, 720p @ 60 fps e 640x480p 60/90. Supporta anche l'interfaccia seriale che può essere collegata direttamente al Raspberry Pi. Non è l'opzione migliore per le applicazioni di elaborazione delle immagini, ma è sufficiente per questo progetto ed è molto economica. Ho preso il mio da qui.
  • Motor Driver: viene utilizzato per controllare le direzioni e le velocità dei motori CC. Supporta il controllo di 2 motori cc in 1 scheda e può sopportare 1,5 A.
  • Power Bank (opzionale): ho usato un power bank (valutato a 5 V, 3 A) per accendere il Raspberry Pi separatamente. È necessario utilizzare un convertitore step-down (convertitore buck: corrente di uscita 3A) per alimentare il Raspberry Pi da 1 sorgente.
  • Batteria LiPo 3s (12 V): le batterie ai polimeri di litio sono note per le loro eccellenti prestazioni nel campo della robotica. Viene utilizzato per alimentare il driver del motore. Ho comprato il mio da qui.
  • Cavi jumper maschio-maschio e femmina-femmina.
  • Nastro biadesivo: utilizzato per montare i componenti sull'auto RC.
  • Nastro blu: questo è un componente molto importante di questo progetto, viene utilizzato per creare le due linee di corsia in cui passerà l'auto. Puoi scegliere qualsiasi colore tu voglia ma ti consiglio di scegliere colori diversi da quelli dell'ambiente intorno.
  • Fascette e barre di legno.
  • Cacciavite.

Passaggio 2: installazione di OpenCV su Raspberry Pi e configurazione del display remoto

Installazione di OpenCV su Raspberry Pi e configurazione del display remoto
Installazione di OpenCV su Raspberry Pi e configurazione del display remoto

Questo passaggio è un po' fastidioso e richiederà del tempo.

OpenCV (Open source Computer Vision) è una libreria di software di visione artificiale e machine learning open source. La libreria dispone di oltre 2500 algoritmi ottimizzati. Segui QUESTA guida molto semplice per installare openCV sul tuo raspberry pi e installare il sistema operativo raspberry pi (se ancora non l'hai fatto). Si prega di notare che il processo di creazione dell'openCV può richiedere circa 1,5 ore in una stanza ben raffreddata (poiché la temperatura del processore sarà molto alta!), quindi prendi un po' di tè e aspetta pazientemente:D.

Per il display remoto, segui anche QUESTA guida per configurare l'accesso remoto al tuo raspberry pi dal tuo dispositivo Windows/Mac.

Passaggio 3: collegare le parti insieme

Collegamento delle parti insieme
Collegamento delle parti insieme
Collegamento delle parti insieme
Collegamento delle parti insieme
Collegamento delle parti insieme
Collegamento delle parti insieme

Le immagini sopra mostrano le connessioni tra raspberry pi, modulo fotocamera e driver del motore. Si prega di notare che i motori che ho usato assorbono 0,35 A a 9 V ciascuno, il che rende sicuro per il driver del motore far funzionare 3 motori contemporaneamente. E poiché voglio controllare la velocità dei 2 motori di strozzamento (1 posteriore e 1 anteriore) esattamente allo stesso modo, li ho collegati alla stessa porta. Ho montato il driver del motore sul lato destro dell'auto usando del doppio nastro. Per quanto riguarda il modulo della fotocamera, ho inserito una fascetta tra i fori delle viti come mostra l'immagine sopra. Quindi, monto la telecamera su una barra di legno in modo da poter regolare la posizione della telecamera come voglio. Cerca di installare la telecamera il più possibile al centro dell'auto. Consiglio di posizionare la telecamera ad almeno 20 cm da terra in modo che il campo visivo davanti all'auto migliori. Lo schema di Fritzing è allegato di seguito.

Passaggio 4: primo test

Primo test
Primo test
Primo test
Primo test

Test della fotocamera:

Una volta installata la fotocamera e creata la libreria openCV, è il momento di testare la nostra prima immagine! Scatteremo una foto da pi cam e la salveremo come "original.jpg". Si può fare in 2 modi:

1. Utilizzo dei comandi del terminale:

Apri una nuova finestra di terminale e digita il seguente comando:

raspistill -o original.jpg

Questo prenderà un'immagine fissa e la salverà nella directory "/pi/original.jpg".

2. Usando qualsiasi IDE Python (io uso IDLE):

Apri un nuovo sketch e scrivi il seguente codice:

importa cv2

video = cv2. VideoCapture(0) while True: ret, frame = video.read() frame = cv2.flip(frame, -1) # usato per capovolgere l'immagine verticalmente cv2.imshow('original', frame) cv2. imwrite('original.jpg', frame) key = cv2.waitKey(1) if key == 27: break video.release() cv2.destroyAllWindows()

Vediamo cosa è successo in questo codice. La prima riga è l'importazione della nostra libreria openCV per utilizzare tutte le sue funzioni. la funzione VideoCapture(0) avvia lo streaming di un video live dalla sorgente determinata da questa funzione, in questo caso è 0 che significa telecamera raspi. se si dispone di più fotocamere, è necessario inserire numeri diversi. video.read() leggerà ogni fotogramma proveniente dalla telecamera e lo salverà in una variabile chiamata "frame". La funzione flip() capovolgerà l'immagine rispetto all'asse y (verticalmente) poiché sto montando la mia fotocamera al contrario. imshow() visualizzerà i nostri frame con la parola "originale" e imwrite() salverà la nostra foto come original.jpg. waitKey(1) attenderà 1 ms per la pressione di qualsiasi pulsante della tastiera e restituirà il suo codice ASCII. se viene premuto il pulsante di escape (esc), viene restituito un valore decimale di 27 e interromperà il ciclo di conseguenza. video.release() interromperà la registrazione e destroyAllWindows() chiuderà ogni immagine aperta dalla funzione imshow().

Consiglio di testare la tua foto con il secondo metodo per familiarizzare con le funzioni di openCV. L'immagine viene salvata nella directory "/pi/original.jpg". La foto originale scattata dalla mia fotocamera è mostrata sopra.

Motori di prova:

Questo passaggio è essenziale per determinare il senso di rotazione di ciascun motore. Innanzitutto, facciamo una breve introduzione sul principio di funzionamento di un driver per motori. L'immagine sopra mostra il pin-out del driver del motore. Enable A, Input 1 e Input 2 sono associati al controllo del motore A. Enable B, Input 3 e Input 4 sono associati al controllo del motore B. Il controllo della direzione è stabilito dalla parte "Ingresso" e il controllo della velocità è stabilito dalla parte "Abilita". Per controllare la direzione del motore A, ad esempio, impostare l'ingresso 1 su ALTO (3,3 V in questo caso poiché stiamo utilizzando un pi lampone) e impostare l'ingresso 2 su BASSO, il motore girerà in una direzione specifica e impostando i valori opposti a Input 1 e Input 2, il motore girerà nella direzione opposta. Se Input 1 = Input 2 = (HIGH o LOW), il motore non gira. I pin di abilitazione prendono un segnale di ingresso Pulse Width Modulation (PWM) dal lampone (da 0 a 3,3 V) e azionano i motori di conseguenza. Ad esempio, un segnale PWM 100% significa che stiamo lavorando alla velocità massima e un segnale PWM 0% significa che il motore non sta ruotando. Il codice seguente viene utilizzato per determinare le direzioni dei motori e testare le loro velocità.

tempo di importazione

import RPi. GPIO as GPIO GPIO.setwarnings(False) # Pin del motore dello sterzo Steering_enable = 22 # Pin fisico 15 in1 = 17 # Pin fisico 11 in2 = 27 # Pin fisico 13 # Pin motori dell'acceleratore Throttle_enable = 25 # Pin fisico 22 in3 = 23 # Pin fisico 16 in4 = 24 # Pin fisico 18 GPIO.setmode(GPIO. BCM) # Usa la numerazione GPIO invece della numerazione fisica 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) # Controllo motore dello sterzo GPIO.output(in1, GPIO. HIGH) GPIO.output(in2, GPIO. LOW) sterzo = GPIO. PWM(steering_enable, 1000) # imposta la frequenza di commutazione su 1000 Hz sterzo.stop() # Controllo motori a farfalla GPIO.output(in3, GPIO. HIGH) GPIO.output(in4, GPIO. LOW) Throttle = GPIO. PWM(throttle_enable, 1000) # imposta la frequenza di commutazione a 1000 Hz Throttle.stop() time.sleep(1) Throttle.start(25) # avvia il motore a 25 % segnale PWM-> (0,25 * tensione batteria) - driver loss sterzo.start(100) # avvia il motore al 100% del segnale PWM-> (1 * voltaggio batteria) - tempo di perdita del conducente.sleep(3) throttle.stop() sterzo.stop()

Questo codice farà funzionare i motori di strozzamento e il motore dello sterzo per 3 secondi e poi li fermerà. La (perdita del conducente) può essere determinata utilizzando un voltmetro. Ad esempio, sappiamo che un segnale PWM al 100% dovrebbe fornire la piena tensione della batteria al terminale del motore. Ma, impostando PWM al 100%, ho scoperto che il driver sta causando una caduta di 3 V e il motore riceve 9 V invece di 12 V (esattamente quello di cui ho bisogno!). La perdita non è lineare, cioè la perdita al 100% è molto diversa dalla perdita al 25%. Dopo aver eseguito il codice sopra, i miei risultati sono stati i seguenti:

Risultati del throttling: se in3 = HIGH e in4 = LOW, i motori di throttling avranno una rotazione in senso orario (CW), cioè l'auto andrà avanti. In caso contrario, l'auto si muoverà all'indietro.

Risultati dello sterzo: se in1 = ALTO e in2 = BASSO, il motore dello sterzo girerà alla sua massima sinistra, cioè l'auto sterzerà a sinistra. In caso contrario, l'auto sterzerà a destra. Dopo alcuni esperimenti, ho scoperto che il motore dello sterzo non gira se il segnale PWM non è al 100% (cioè il motore sterza completamente a destra o completamente a sinistra).

Passaggio 5: rilevamento delle linee di corsia e calcolo della linea di prua

Rilevamento delle linee di corsia e calcolo della linea di prua
Rilevamento delle linee di corsia e calcolo della linea di prua
Rilevamento delle linee di corsia e calcolo della linea di prua
Rilevamento delle linee di corsia e calcolo della linea di prua
Rilevamento delle linee di corsia e calcolo della linea di prua
Rilevamento delle linee di corsia e calcolo della linea di prua

In questo passaggio verrà spiegato l'algoritmo che controllerà il movimento della vettura. La prima immagine mostra l'intero processo. L'input del sistema è immagini, l'output è theta (angolo di sterzata in gradi). Si noti che l'elaborazione viene eseguita su 1 immagine e verrà ripetuta su tutti i fotogrammi.

Telecamera:

La fotocamera inizierà a registrare un video con una risoluzione (320 x 240). Consiglio di abbassare la risoluzione in modo da ottenere una migliore frequenza fotogrammi (fps) poiché si verificherà un calo di fps dopo l'applicazione di tecniche di elaborazione a ciascun fotogramma. Il codice seguente sarà il ciclo principale del programma e aggiungerà ogni passaggio a questo codice.

importa cv2

import numpy as np video = cv2. VideoCapture(0) video.set(cv2. CAP_PROP_FRAME_WIDTH, 320) # imposta la larghezza a 320 p video.set(cv2. CAP_PROP_FRAME_HEIGHT, 240) # imposta l'altezza a 240 p # Il ciclo mentre Vero: ret, frame = video.read() frame = cv2.flip(frame, -1) cv2.imshow("originale", frame) key = cv2.waitKey(1) if key == 27: break video.release () cv2.destroyAllWindows()

Il codice qui mostrerà l'immagine originale ottenuta nel passaggio 4 ed è mostrato nelle immagini sopra.

Converti in spazio colore HSV:

Ora, dopo aver acquisito la registrazione video come fotogrammi dalla fotocamera, il passaggio successivo consiste nel convertire ogni fotogramma nello spazio colore Tonalità, Saturazione e Valore (HSV). Il vantaggio principale di farlo è quello di poter distinguere i colori in base al loro livello di luminanza. Ed ecco una buona spiegazione dello spazio colore HSV. La conversione in HSV viene eseguita tramite la seguente funzione:

def convert_to_HSV(frame):

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

Questa funzione verrà chiamata dal ciclo principale e restituirà il frame nello spazio colore HSV. Il fotogramma ottenuto da me nello spazio colore HSV è mostrato sopra.

Rileva colore blu e bordi:

Dopo aver convertito l'immagine nello spazio colore HSV, è il momento di rilevare solo il colore che ci interessa (cioè il colore blu poiché è il colore delle linee di corsia). Per estrarre il colore blu da un fotogramma HSV, è necessario specificare un intervallo di tonalità, saturazione e valore. fare riferimento qui per avere un'idea migliore sui valori HSV. Dopo alcuni esperimenti, i limiti superiore e inferiore del colore blu sono mostrati nel codice sottostante. E per ridurre la distorsione complessiva in ogni fotogramma, i bordi vengono rilevati solo utilizzando il rilevatore di bordi canny. Maggiori informazioni su Canny Edge si trovano qui. Una regola pratica è selezionare i parametri della funzione Canny() con un rapporto di 1:2 o 1:3.

def detect_edges(frame):

lower_blue = np.array([90, 120, 0], dtype = "uint8") # limite inferiore del colore blu upper_blue = np.array([150, 255, 255], dtype="uint8") # limite superiore di blue color mask = cv2.inRange(hsv, lower_blue, upper_blue) # questa maschera filtrerà tutto tranne il blu # rileva i bordi bordi = cv2. Canny(maschera, 50, 100) cv2.imshow("bordi", bordi) restituisce bordi

Questa funzione verrà chiamata anche dal ciclo principale che prende come parametro il frame dello spazio colore HSV e restituisce il frame con bordi. La cornice bordata che ho ottenuto si trova sopra.

Seleziona regione di interesse (ROI):

La selezione della regione di interesse è fondamentale per concentrarsi solo su 1 regione del fotogramma. In questo caso, non voglio che l'auto veda molti oggetti nell'ambiente. Voglio solo che l'auto si concentri sulle linee della corsia e ignori qualsiasi altra cosa. P. S: il sistema di coordinate (assi x e y) inizia dall'angolo in alto a sinistra. In altre parole, il punto (0, 0) inizia dall'angolo in alto a sinistra. l'asse y è l'altezza e l'asse x è la larghezza. Il codice seguente seleziona la regione di interesse per mettere a fuoco solo la metà inferiore del fotogramma.

def regione_di_interesse (bordi):

altezza, larghezza = bordi.forma # estrae l'altezza e la larghezza dei bordi frame mask = np.zeros_like(edges) # crea una matrice vuota con le stesse dimensioni dei bordi frame # focalizza solo la metà inferiore dello schermo # specifica le coordinate di 4 punti (in basso a sinistra, in alto a sinistra, in alto a destra, in basso a destra) poligono = np.array(

Questa funzione prenderà come parametro la cornice con bordi e disegnerà un poligono con 4 punti preimpostati. Si concentrerà solo su ciò che è all'interno del poligono e ignorerà tutto ciò che è al di fuori di esso. Il riquadro della mia regione di interesse è mostrato sopra.

Rileva segmenti di linea:

La trasformata di Hough viene utilizzata per rilevare i segmenti di linea da una cornice con bordi. La trasformata di Hough è una tecnica per rilevare qualsiasi forma in forma matematica. Può rilevare quasi tutti gli oggetti anche se distorti in base a un certo numero di voti. un ottimo riferimento per la trasformata di Hough è mostrato qui. Per questa applicazione, la funzione cv2. HoughLinesP() viene utilizzata per rilevare le linee in ogni frame. I parametri importanti che questa funzione assume sono:

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

  • Frame: è il frame in cui vogliamo rilevare le linee.
  • rho: è la precisione della distanza in pixel (di solito è = 1)
  • theta: precisione angolare in radianti (sempre = np.pi/180 ~ 1 grado)
  • min_threshold: voto minimo che dovrebbe ottenere per essere considerato come una linea
  • minLineLength: lunghezza minima della linea in pixel. Qualsiasi riga più corta di questo numero non è considerata una riga.
  • maxLineGap: distanza massima in pixel tra 2 righe da trattare come 1 riga. (Non viene utilizzato nel mio caso poiché le linee di corsia che sto utilizzando non hanno spazi vuoti).

Questa funzione restituisce gli estremi di una linea. La seguente funzione viene chiamata dal mio ciclo principale per rilevare le linee usando la trasformata di Hough:

def detect_line_segments(cropped_edges):

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

Pendenza media e intercetta (m, b):

ricordiamo che l'equazione della retta è data da y = mx + b. Dove m è la pendenza della linea e b è l'intercetta y. In questa parte verrà calcolata la media delle pendenze e delle intercettazioni dei segmenti di linea rilevati mediante la trasformata di Hough. Prima di farlo, diamo un'occhiata alla foto della cornice originale mostrata sopra. La corsia di sinistra sembra andare verso l'alto, quindi ha una pendenza negativa (ricordate il punto di partenza del sistema di coordinate?). In altre parole, la linea della corsia di sinistra ha x1 < x2 e y2 x1 e y2 > y1 che darà una pendenza positiva. Quindi, tutte le linee con pendenza positiva sono considerate punti della corsia di destra. In caso di linee verticali (x1 = x2), la pendenza sarà infinita. In questo caso, salteremo tutte le linee verticali per evitare di ricevere un errore. Per aggiungere maggiore precisione a questo rilevamento, ogni fotogramma è diviso in due regioni (destra e sinistra) attraverso 2 linee di confine. Tutti i punti di larghezza (punti dell'asse x) maggiori della linea di confine destra, sono associati al calcolo della corsia destra. E se tutti i punti di larghezza sono inferiori alla linea di confine sinistra, sono associati al calcolo della corsia sinistra. La seguente funzione prende il frame in elaborazione e i segmenti di corsia rilevati utilizzando la trasformata di Hough e restituisce la pendenza media e l'intercetta di due linee di corsia.

def medium_slope_intercept(frame, line_segments):

lane_lines = if line_segments è None: print("nessun segmento di linea rilevato") return lane_lines height, width, _ = frame.shape left_fit = right_fit = confine = left_region_boundary = larghezza * (1 - confine) right_region_boundary = larghezza * confine per segmento_riga in segmenti_riga: for x1, y1, x2, y2 in segmento_riga: if x1 == x2: print("salta linee verticali (pendenza = infinito)") continue fit = np.polyfit((x1, x2), (y1, y2), 1) pendenza = (y2 - y1) / (x2 - x1) intercetta = y1 - (pendenza * x1) se pendenza < 0: se x1 < confine_area_sinistra e x2 confine_area_destra e x2 > confine_area_destra: adattamento_destro. append((slope, intercept)) left_fit_average = np.average(left_fit, axis=0) if len(left_fit) > 0: lane_lines.append(make_points(frame, left_fit_average)) right_fit_average = np.average(right_fit, axis=0) if len(right_fit) > 0: lane_lines.append(make_points(frame, right_fit_average)) # lane_lines è un array 2-D composto dalle coordinate delle linee della corsia destra e sinistra # per esempio: lan e_lines =

make_points() è una funzione di supporto per la funzione medium_slope_intercept() che restituirà le coordinate delimitate delle linee della corsia (dal basso al centro del frame).

def make_points(frame, line):

altezza, larghezza, _ = cornice.forma pendenza, intercetta = linea y1 = altezza # parte inferiore della cornice y2 = int(y1 / 2) # crea punti dal centro della cornice verso il basso se pendenza == 0: pendenza = 0,1 x1 = int((y1 - intercetta) / pendenza) x2 = int((y2 - intercetta) / pendenza) return

Per evitare la divisione per 0, viene presentata una condizione. Se pendenza = 0 che significa y1 = y2 (linea orizzontale), assegna alla pendenza un valore vicino a 0. Ciò non influirà sulle prestazioni dell'algoritmo e preverrà il caso impossibile (divisione per 0).

Per visualizzare le linee di corsia sui telai, viene utilizzata la seguente funzione:

def display_lines(frame, lines, line_color=(0, 255, 0), line_width=6): # line color (B, G, R)

line_image = np.zeros_like(frame) if lines non è None: for line in lines: for x1, y1, x2, y2 in line: cv2.line(line_image, (x1, y1), (x2, y2), line_color, line_width) line_image = cv2.addWeighted(frame, 0.8, line_image, 1, 1) return line_image

La funzione cv2.addWeighted() accetta i seguenti parametri e viene utilizzata per combinare due immagini ma dando a ciascuna un peso.

cv2.addWeighted(image1, alpha, image2, beta, gamma)

E calcola l'immagine di output utilizzando la seguente equazione:

output = alfa * immagine1 + beta * immagine2 + gamma

Maggiori informazioni sulla funzione cv2.addWeighted() sono derivate qui.

Calcola e visualizza la riga di intestazione:

Questo è il passaggio finale prima di applicare le velocità ai nostri motori. La linea di prua è responsabile di dare al motore dello sterzo la direzione in cui dovrebbe ruotare e dare ai motori di strozzamento la velocità alla quale opereranno. Il calcolo della linea di prua è pura trigonometria, vengono utilizzate le funzioni trigonometriche tan e atan (tan^-1). Alcuni casi estremi si verificano quando la telecamera rileva solo una linea di corsia o quando non rileva alcuna linea. Tutti questi casi sono mostrati nella seguente funzione:

def get_steering_angle(frame, lane_lines):

altezza, larghezza, _ = frame.shape if len(lane_lines) == 2: # se vengono rilevate due linee di corsia _, _, left_x2, _ = lane_lines[0][0] # estrae x2 a sinistra dall'array lane_lines _, _, right_x2, _ = lane_lines[1][0] # estrae right x2 dall'array lane_lines mid = int(width / 2) x_offset = (sinistra_x2 + right_x2) / 2 - mid y_offset = int(height / 2) elif len(lane_lines) == 1: # se viene rilevata solo una riga x1, _, x2, _ = lane_lines[0][0] x_offset = x2 - x1 y_offset = int(height / 2) elif len(lane_lines) == 0: # se non viene rilevata alcuna linea 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) sterzo_angle = angolo_a_mid_grado + 90 return

x_offset nel primo caso è quanto la media ((destra x2 + sinistra x2) / 2) differisce dal centro dello schermo. y_offset viene sempre considerato altezza / 2. L'ultima immagine sopra mostra un esempio di linea di prua. angle_to_mid_radians è lo stesso di "theta" mostrato nell'ultima immagine sopra. Se sterzo_angolo = 90, significa che l'auto ha una linea di prua perpendicolare alla linea "altezza/2" e l'auto si muoverà in avanti senza sterzare. Se sterzo_angolo > 90, l'auto dovrebbe sterzare a destra altrimenti dovrebbe sterzare a sinistra. Per visualizzare la riga di intestazione, viene utilizzata la seguente funzione:

def display_heading_line(frame, sterzo_angolo, line_color=(0, 0, 255), line_width=5)

intestazione_immagine = np.zeros_like(frame) altezza, larghezza, _ = frame.shape sterzo_angolo_radiante = angolo_sterzo / 180.0 * math.pi x1 = int(larghezza / 2) y1 = altezza x2 = int(x1 - altezza / 2 / math.tan (steering_angle_radian)) y2 = int(height / 2) cv2.line(heading_image, (x1, y1), (x2, y2), line_color, line_width) head_image = cv2.addWeighted(frame, 0.8, head_image, 1, 1) return header_image

La funzione sopra prende il fotogramma in cui verrà disegnata la linea di prua e l'angolo di sterzata come input. Restituisce l'immagine della linea di intestazione. La cornice della linea di intestazione presa nel mio caso è mostrata nell'immagine sopra.

Combinando tutto il codice insieme:

Il codice ora è pronto per essere assemblato. Il codice seguente mostra il ciclo principale del programma che chiama ciascuna funzione:

importa cv2

import numpy as 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) #Chiamata delle funzioni hsv = convert_to_HSV(frame) edge = detect_edges(hsv) roi = region_of_interest(edges) line_segments = detect_line_segments(roi) lane_lines = medium_slope_intercept(frame, line_segments) lane_linesangles_frames_image, display_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()

Passaggio 6: applicazione del controllo PD

Applicazione del controllo PD
Applicazione del controllo PD

Ora abbiamo il nostro angolo di sterzata pronto per essere alimentato ai motori. Come accennato in precedenza, se l'angolo di sterzata è maggiore di 90, l'auto dovrebbe svoltare a destra altrimenti dovrebbe svoltare a sinistra. Ho applicato un semplice codice che fa girare il motore dello sterzo a destra se l'angolo è superiore a 90 e lo gira a sinistra se l'angolo di sterzo è inferiore a 90 a una velocità di strozzatura costante (10% PWM) ma ho riscontrato molti errori. L'errore principale che ho riscontrato è che quando l'auto si avvicina a qualsiasi svolta, il motore dello sterzo agisce direttamente ma i motori di strozzamento si inceppano. Ho provato ad aumentare la velocità di throttling per essere (20% PWM) alle curve, ma ho finito con il robot che usciva dalle corsie. Avevo bisogno di qualcosa che aumentasse molto la velocità di strozzamento se l'angolo di sterzata è molto grande e aumenta un po' la velocità se l'angolo di sterzata non è così grande, quindi riduce la velocità a un valore iniziale quando l'auto si avvicina a 90 gradi (in rettilineo). La soluzione era usare un controller PD.

Controller PID sta per controller proporzionale, integrale e derivativo. Questo tipo di controllori lineari è ampiamente utilizzato nelle applicazioni di robotica. L'immagine sopra mostra il tipico circuito di controllo del feedback PID. L'obiettivo di questo controllore è quello di raggiungere il "setpoint" nel modo più efficiente a differenza dei controllori "on - off" che accendono o spengono l'impianto in base ad alcune condizioni. Alcune parole chiave dovrebbero essere conosciute:

  • Setpoint: è il valore che vuoi che il tuo impianto raggiunga.
  • Valore effettivo: è il valore effettivo rilevato dal sensore.
  • Errore: è la differenza tra setpoint e valore effettivo (errore = Setpoint - Valore effettivo).
  • Variabile controllata: dal suo nome, la variabile che si desidera controllare.
  • Kp: costante proporzionale.
  • Ki: Costante integrale.
  • Kd: costante derivata.

In breve, il loop del sistema di controllo PID funziona come segue:

  • L'utente definisce il setpoint necessario affinché il sistema raggiunga.
  • L'errore viene calcolato (errore = setpoint - effettivo).
  • Il controller P genera un'azione proporzionale al valore dell'errore. (l'errore aumenta, aumenta anche l'azione P)
  • Il controller integrerà l'errore nel tempo che elimina l'errore di stato stazionario del sistema ma ne aumenta il superamento.
  • Il controller D è semplicemente la derivata temporale dell'errore. In altre parole, è la pendenza dell'errore. Fa un'azione proporzionale alla derivata dell'errore. Questo controller aumenta la stabilità del sistema.
  • L'output del controller sarà la somma dei tre controller. L'uscita del controller diventerà 0 se l'errore diventa 0.

Un'ottima spiegazione del controller PID può essere trovata qui.

Tornando all'auto che mantiene la corsia, la mia variabile controllata era la velocità di strozzamento (dato che lo sterzo ha solo due stati, a destra oa sinistra). A questo scopo viene utilizzato un controller PD poiché l'azione D aumenta molto la velocità di strozzamento se la modifica dell'errore è molto grande (cioè una grande deviazione) e rallenta l'auto se questa modifica dell'errore si avvicina a 0. Ho eseguito i seguenti passaggi per implementare un PD controllore:

  • Imposta il setpoint a 90 gradi (voglio sempre che l'auto si muova dritta)
  • Calcolato l'angolo di deviazione dal centro
  • La deviazione fornisce due informazioni: quanto è grande l'errore (entità della deviazione) e quale direzione deve prendere il motore dello sterzo (segno di deviazione). Se la deviazione è positiva, l'auto dovrebbe sterzare a destra altrimenti dovrebbe sterzare a sinistra.
  • Poiché la deviazione è negativa o positiva, viene definita una variabile di "errore" sempre uguale al valore assoluto della deviazione.
  • L'errore viene moltiplicato per un Kp costante.
  • L'errore subisce una differenziazione temporale e viene moltiplicato per una costante Kd.
  • La velocità dei motori viene aggiornata e il ciclo ricomincia.

Il seguente codice viene utilizzato nel loop principale per controllare la velocità dei motori di strozzamento:

velocità = 10 # velocità operativa in % PWM

#Variabili da aggiornare ad ogni ciclo lastTime = 0 lastError = 0 # costanti PD Kp = 0.4 Kd = Kp * 0.65 While True: now = time.time() # variabile dell'ora corrente dt = now - lastTime deviazione = angolo_sterzo - 90 # equivalente ad angolo_a_metà_gradi variabile errore = abs(deviazione) se deviazione -5: # non sterza se c'è un intervallo di errore di 10 gradi deviazione = 0 errore = 0 GPIO.output(in1, GPIO. LOW) GPIO.output(in2, GPIO. LOW) sterzo.stop() deviazione elif > 5: # sterzo a destra se la deviazione è positiva GPIO.output(in1, GPIO. LOW) GPIO.output(in2, GPIO. HIGH) sterzo.start(100) deviazione elif < -5: # sterza a sinistra se la deviazione è negativa GPIO.output(in1, GPIO. HIGH) GPIO.output(in2, GPIO. LOW) sterzo.start(100) derivata = kd * (errore - ultimo errore) / dt proporzionale = kp * errore PD = int(velocità + derivata + proporzionale) spd = abs(PD) se spd> 25: spd = 25 throttle.start(spd) lastError = errore lastTime = time.time()

Se l'errore è molto grande (la deviazione dal centro è elevata), le azioni proporzionali e derivate sono elevate con conseguente elevata velocità di strozzamento. Quando l'errore si avvicina a 0 (la deviazione dal centro è bassa), l'azione derivativa agisce in modo inverso (la pendenza è negativa) e la velocità di strozzamento si riduce per mantenere la stabilità del sistema. Il codice completo è allegato di seguito.

Passaggio 7: risultati

I video sopra mostrano i risultati che ho ottenuto. Ha bisogno di più messa a punto e ulteriori regolazioni. Stavo collegando il raspberry pi al mio schermo LCD perché lo streaming video sulla mia rete aveva un'elevata latenza ed era molto frustrante lavorarci, ecco perché ci sono fili collegati a raspberry pi nel video. Ho usato delle tavole di gommapiuma per disegnare il binario.

Aspetto i vostri consigli per migliorare questo progetto! Poiché spero che questo tutorial sia stato abbastanza buono da darti alcune nuove informazioni.

Consigliato: