Taula de continguts:

Cotxe autònom que manté carrils amb Raspberry Pi i OpenCV: 7 passos (amb imatges)
Cotxe autònom que manté carrils amb Raspberry Pi i OpenCV: 7 passos (amb imatges)

Vídeo: Cotxe autònom que manté carrils amb Raspberry Pi i OpenCV: 7 passos (amb imatges)

Vídeo: Cotxe autònom que manté carrils amb Raspberry Pi i OpenCV: 7 passos (amb imatges)
Vídeo: ПОЧЕМУ Я ЖДУ L4D3 2024, Juliol
Anonim
Cotxe autònom que manté carrils amb Raspberry Pi i OpenCV
Cotxe autònom que manté carrils amb Raspberry Pi i OpenCV

En aquest instructable, s’implementarà un robot autònom de manteniment del carril que passarà pels següents passos:

  • Recollida de peces
  • Instal·lació dels requisits previs del programari
  • Muntatge de maquinari
  • Primera prova
  • Detectar línies de carril i mostrar la línia de guia mitjançant openCV
  • Implementació d’un controlador PD
  • Resultats

Pas 1: Recopilació de components

Recopilació de components
Recopilació de components
Recopilació de components
Recopilació de components
Recopilació de components
Recopilació de components
Recopilació de components
Recopilació de components

Les imatges anteriors mostren tots els components utilitzats en aquest projecte:

  • Cotxe RC: vaig obtenir el meu d'una botiga local del meu país. Està equipat amb 3 motors (2 per a la regulació i 1 per a la direcció). El principal desavantatge d'aquest cotxe és que la direcció està limitada entre "sense direcció" i "direcció completa". Dit d'una altra manera, no pot dirigir-se en un angle específic, a diferència dels cotxes de servo-direcció RC. Aquí podeu trobar un kit de cotxes similar dissenyat especialment per a raspberry pi.
  • Raspberry pi 3 model b +: aquest és el cervell del cotxe que gestionarà moltes etapes de processament. Es basa en un processador de 64 bits de quatre nuclis a 1,4 GHz. Tinc el meu d’aquí.
  • Mòdul de càmera Raspberry pi de 5 mp: admet enregistraments a 1080p @ 30 fps, 720p @ 60 fps i 640x480p 60/90. També és compatible amb la interfície sèrie que es pot connectar directament al raspberry pi. No és la millor opció per a aplicacions de processament d'imatges, però és suficient per a aquest projecte, ja que és molt barat. Tinc el meu d’aquí.
  • Motor Driver: s’utilitza per controlar les direccions i velocitats dels motors de corrent continu. Admet el control de motors de 2 cc en 1 placa i pot suportar 1,5 A.
  • Power Bank (opcional): he utilitzat un power bank (de 5V, 3A) per encendre el raspberry pi per separat. S’ha d’utilitzar un convertidor de descens (convertidor Buck: corrent de sortida de 3A) per encendre el raspberry pi des d’una font.
  • Bateria LiPo de 3 s (12 V): les bateries de polímer de liti són conegudes pel seu excel·lent rendiment en el camp de la robòtica. S'utilitza per alimentar el conductor del motor. Vaig comprar la meva d’aquí.
  • Filferos de pont masculí a masculí i femella a femella.
  • Cinta de doble cara: s'utilitza per muntar els components al cotxe RC.
  • Cinta blava: és un component molt important d’aquest projecte, s’utilitza per fer les línies de dos carrils per on circularà el cotxe. Podeu triar qualsevol color que vulgueu, però us recomano triar colors diferents dels de l’entorn.
  • Tirants i barres de fusta.
  • Tornavís.

Pas 2: Instal·leu OpenCV a Raspberry Pi i configureu la pantalla remota

Instal·lació d’OpenCV a Raspberry Pi i configuració de la pantalla remota
Instal·lació d’OpenCV a Raspberry Pi i configuració de la pantalla remota

Aquest pas és una mica molest i trigarà una estona.

OpenCV (Open source Computer Vision) és una biblioteca de programari de visió per ordinador i aprenentatge automàtic de codi obert. La biblioteca té més de 2500 algorismes optimitzats. Seguiu AQUESTA guia molt senzilla per instal·lar openCV al vostre raspberry pi i instal·lar el SO de raspberry pi (si encara no ho heu fet). Tingueu en compte que el procés de creació de l’OpenCV pot trigar aproximadament 1,5 hores en una habitació ben refrigerada (ja que la temperatura del processador augmentarà molt!), Així que preneu-vos un te i espereu pacientment: D.

Per a la visualització remota, també seguiu AQUESTA guia per configurar l'accés remot al vostre raspberry pi des del dispositiu Windows / Mac.

Pas 3: connectar peces juntes

Connexió de peces juntes
Connexió de peces juntes
Connexió de peces juntes
Connexió de peces juntes
Connexió de peces juntes
Connexió de peces juntes

Les imatges anteriors mostren les connexions entre el raspberry pi, el mòdul de càmera i el controlador del motor. Tingueu en compte que els motors que he utilitzat absorbeixen 0,35 A a 9 V cadascun, cosa que fa que el conductor del motor faci funcionar 3 motors al mateix temps. I com que vull controlar la velocitat dels 2 motors d’estrangulació (1 posterior i 1 frontal) exactament de la mateixa manera, els vaig connectar al mateix port. Vaig muntar el conductor del motor al costat dret del cotxe amb cinta doble. Pel que fa al mòdul de la càmera, he introduït una corbata de cremallera entre els forats del cargol, tal com mostra la imatge superior. A continuació, ajusto la càmera a una barra de fusta per poder ajustar la posició de la càmera com vulgui. Intenteu instal·lar la càmera al centre del cotxe tant com sigui possible. Recomano col·locar la càmera a almenys 20 cm per sobre del terra perquè el camp de visió davant del cotxe millorarà. A continuació s’adjunta l’esquema de Fritzing.

Pas 4: Primera prova

Primera prova
Primera prova
Primera prova
Primera prova

Prova de càmera:

Un cop instal·lada la càmera i creada la biblioteca openCV, és hora de provar la nostra primera imatge. Farem una foto de pi cam i la guardarem com a "original.jpg". Es pot fer de dues maneres:

1. Ús d'ordres de terminal:

Obriu una nova finestra de terminal i escriviu l'ordre següent:

raspistill -o original.jpg

Això agafarà una imatge fixa i la desarà al directori "/pi/original.jpg".

2. Utilitzant qualsevol IDE de python (jo faig servir IDLE):

Obriu un esbós nou i escriviu el codi següent:

importar cv2

video = cv2. VideoCapture (0) mentre és True: ret, frame = video.read () frame = cv2.flip (frame, -1) # utilitzat per capgirar la imatge verticalment cv2.imshow ('original', frame) cv2. imwrite ('original.jpg', marc) key = cv2.waitKey (1) if key == 27: break video.release () cv2.destroyAllWindows ()

Vegem què ha passat en aquest codi. La primera línia és importar la nostra biblioteca openCV per utilitzar totes les seves funcions. la funció VideoCapture (0) comença a transmetre un vídeo en directe des de la font determinada per aquesta funció, en aquest cas és 0 que significa càmera raspi. si teniu diverses càmeres, hauríeu de col·locar números diferents. video.read () llegirà cada fotograma que prové de la càmera i el desarà en una variable anomenada "fotograma". La funció flip () capgirarà la imatge respecte a l'eix y (verticalment), ja que estic muntant la càmera de manera inversa. imshow () mostrarà els nostres marcs encapçalats per la paraula "original" i imwrite () desarà la nostra foto com a original.jpg. waitKey (1) esperarà 1 ms per prémer qualsevol botó de teclat i retorna el seu codi ASCII. si es prem el botó escape (esc), es torna un valor decimal de 27 i trencarà el bucle en conseqüència. video.release () deixarà de gravar i destroyAllWindows () tancarà totes les imatges obertes per la funció imshow ().

Us recomano provar la vostra foto amb el segon mètode per familiaritzar-vos amb les funcions openCV. La imatge es desa al directori "/pi/original.jpg". La foto original que va fer la meva càmera es mostra a la part superior.

Prova de motors:

Aquest pas és essencial per determinar el sentit de gir de cada motor. En primer lloc, fem una breu introducció sobre el principi de funcionament d’un conductor de motor. La imatge superior mostra el pin-out del controlador del motor. L'activació A, l'entrada 1 i l'entrada 2 estan associades al control del motor A. L'activació B, l'entrada 3 i l'entrada 4 estan associades al control del motor B. El control de direcció s'estableix mitjançant la part "Entrada" i el control de velocitat s'estableix amb la part "Habilita". Per controlar la direcció del motor A, p. a l'entrada 1 i a l'entrada 2, el motor girarà en la direcció oposada. Si Entrada 1 = Entrada 2 = (ALTA o BAIXA), el motor no girarà. Els pins habilitats prenen un senyal d’entrada de modulació d’amplitud de pols (PWM) del gerd (0 a 3,3 V) i fan funcionar els motors en conseqüència. Per exemple, un senyal 100% PWM significa que estem treballant en la velocitat màxima i un 0% senyal PWM significa que el motor no gira. El següent codi s’utilitza per determinar les direccions dels motors i provar-ne la velocitat.

temps d'importació

importar RPi. GPIO com a GPIO GPIO.setwarnings (fals) # Pins del motor de direcció steering_enable = 22 # Pin físic 15 in1 = 17 # Pin físic 11 in2 = 27 # Pin físic 13 # Pins del motor d’accelerador throttle_enable = 25 # Pin físic 22 in3 = 23 # Pin físic 16 in4 = 24 # Pin físic 18 GPIO.setmode (GPIO. BCM) # Utilitzeu la numeració GPIO en lloc de la numeració física 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) # Control del motor de direcció GPIO.output (in1, GPIO. ALTA) sortida GPIO.out (in2, GPIO. LOW) direcció = GPIO. PWM (steering_enable, 1000) # estableix la freqüència de commutació a 1000 Hz steering.stop () # Control de motors d’accelerador GPIO.output (in3, GPIO. HIGH) GPIO.output (in4, GPIO. LOW) throttle = GPIO. PWM (throttle_enable, 1000) # estableix la freqüència de commutació a 1000 Hz throttle.stop () time.sleep (1) throttle.start (25) # arrenca el motor a 25 % De senyal PWM-> (tensió de la bateria 0,25 *): del conductor loss steering.start (100) # arrenca el motor amb un senyal 100% PWM-> (1 * Voltatge de la bateria): temps de pèrdua del conductor.sleep (3) throttle.stop () steering.stop ()

Aquest codi farà funcionar els motors de regulació i el motor de direcció durant 3 segons i després els detindrà. La (pèrdua del conductor) es pot determinar mitjançant un voltímetre. Per exemple, sabem que un senyal 100% PWM hauria de donar tota la tensió de la bateria al terminal del motor. Però, establint PWM al 100%, vaig trobar que el controlador provoca una caiguda de 3 V i el motor rep 9 V en lloc de 12 V (exactament el que necessito). La pèrdua no és lineal, és a dir, la pèrdua al 100% és molt diferent de la pèrdua al 25%. Després d'executar el codi anterior, els meus resultats van ser els següents:

Resultats d’acceleració: si in3 = HIGH i in4 = LOW, els motors d’acceleració tindran una rotació Clock-Wise (CW), és a dir, el cotxe avançarà. En cas contrari, el cotxe es desplaçarà cap enrere.

Resultats de la direcció: si in1 = HIGH i in2 = LOW, el motor de la direcció girarà al màxim a l’esquerra, és a dir, el cotxe dirigirà cap a l’esquerra. En cas contrari, el cotxe girarà cap a la dreta. Després d’alguns experiments, vaig trobar que el motor de direcció no giraria si el senyal PWM no era del 100% (és a dir, el motor es dirigirà totalment cap a la dreta o completament cap a l’esquerra).

Pas 5: detectar línies de carrils i calcular la línia de capçalera

Detecció de línies de carrils i càlcul de línies de capçalera
Detecció de línies de carrils i càlcul de línies de capçalera
Detecció de línies de carrils i càlcul de línies de capçalera
Detecció de línies de carrils i càlcul de línies de capçalera
Detecció de línies de carrils i càlcul de línies de capçalera
Detecció de línies de carrils i càlcul de línies de capçalera

En aquest pas, s’explicarà l’algoritme que controlarà el moviment del cotxe. La primera imatge mostra tot el procés. L’entrada del sistema són imatges, la sortida és theta (angle de direcció en graus). Tingueu en compte que el processament es fa en 1 imatge i es repetirà en tots els fotogrames.

Càmera:

La càmera començarà a gravar un vídeo amb resolució (320 x 240). Us recomano reduir la resolució per obtenir una millor velocitat de fotogrames (fps), ja que es produirà una caiguda de fps després d’aplicar tècniques de processament a cada fotograma. El codi següent serà el bucle principal del programa i afegirà cada pas sobre aquest codi.

importar cv2

import numpy as np video = cv2. VideoCapture (0) video.set (cv2. CAP_PROP_FRAME_WIDTH, 320) # estableix l’amplada a 320 p video.set (cv2. CAP_PROP_FRAME_HEIGHT, 240) # estableix l’alçada a 240 p # El bucle mentre Cert: ret, frame = video.read () frame = cv2.flip (frame, -1) cv2.imshow ("original", frame) key = cv2.waitKey (1) if key == 27: break video.release () cv2.destroyAllWindows ()

El codi aquí mostrarà la imatge original obtinguda al pas 4 i es mostra a les imatges anteriors.

Converteix a espai de color HSV:

Ara, després de prendre la gravació de vídeo com a fotogrames de la càmera, el següent pas és convertir cada fotograma en espai de color Hue, Saturation i Value (HSV). El principal avantatge de fer-ho és poder diferenciar els colors pel seu nivell de lluminositat. I aquí teniu una bona explicació de l’espai de color HSV. La conversió a HSV es realitza mitjançant la següent funció:

def convert_to_HSV (marc):

hsv = cv2.cvtColor (marc, cv2. COLOR_BGR2HSV) cv2.imshow ("HSV", hsv) retorna hsv

Aquesta funció es cridarà des del bucle principal i retornarà el marc a l'espai de color HSV. El marc obtingut per mi en l’espai de color HSV es mostra a la part superior.

Detecta el color blau i les vores:

Després de convertir la imatge en espai de color HSV, és hora de detectar només el color que ens interessa (és a dir, el color blau, ja que és el color de les línies del carril). Per extreure el color blau d’un marc HSV, s’ha d’especificar un rang de tonalitat, saturació i valor. consulteu aquí per tenir una millor idea dels valors HSV. Després d'alguns experiments, els límits superior i inferior del color blau es mostren al codi següent. I per reduir la distorsió general de cada quadre, les vores només es detecten mitjançant un detector de vores canyós. Trobareu més informació sobre el cany edge aquí. Una regla general és seleccionar els paràmetres de la funció Canny () amb una proporció d’1: 2 o 1: 3.

def detect_edges (frame):

lower_blue = np.array ([90, 120, 0], dtype = "uint8") # límit inferior de color blau upper_blue = np.array ([150, 255, 255], dtype = "uint8") # límit superior de color blau màscara = cv2.inRange (hsv, lower_blue, upper_blue) # aquesta màscara filtrarà tot menys blau # detectar vores arestes = cv2. Canny (màscara, 50, 100) cv2.imshow ("arestes", arestes) retornar vores

Aquesta funció també es cridarà des del bucle principal que pren com a paràmetre el marc de l’espai de color HSV i retorna el marc vorejat. El marc vorejat que he obtingut es troba més amunt.

Seleccioneu una regió d'interès (ROI):

La selecció de la regió d’interès és crucial per centrar-se només en una regió del marc. En aquest cas, no vull que el cotxe vegi molts elements a l’entorn. Només vull que el cotxe se centri en les línies del carril i ignori qualsevol altra cosa. P. S: el sistema de coordenades (eixos xy) comença des de la cantonada superior esquerra. En altres paraules, el punt (0, 0) comença des de la cantonada superior esquerra. l'eix y és l'altura i l'eix x l'amplada. El codi següent selecciona la regió d'interès per centrar-se només a la meitat inferior del marc.

def regió_de_interès (vores):

altura, amplada = arestes. forma # extreure l'alçada i l'amplada de les vores màscara del marc = np.zeros_like (vores) # fer una matriu buida amb les mateixes dimensions del marc de vores # només enfocar la meitat inferior de la pantalla # especificar les coordenades de 4 punts (inferior esquerre, superior esquerre, superior dret, inferior dret) polígon = np.array (

Aquesta funció agafarà el marc vorejat com a paràmetre i dibuixarà un polígon amb 4 punts predefinits. Només se centrarà en el que hi ha dins del polígon i ignorarà tot el que hi ha fora. El marc de la meva regió d’interès es mostra a la part superior.

Detecta segments de línia:

La transformació Hough s’utilitza per detectar segments de línia des d’un marc vorejat. La transformada de Hough és una tècnica per detectar qualsevol forma en forma matemàtica. Pot detectar gairebé qualsevol objecte, fins i tot si es distorsiona segons un nombre de vots. aquí es mostra una gran referència per a la transformada de Hough. Per a aquesta aplicació, s’utilitza la funció cv2. HoughLinesP () per detectar línies a cada fotograma. Els paràmetres importants que adopta aquesta funció són:

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

  • Marc: és el marc en el qual volem detectar línies.
  • rho: és la distància de precisió en píxels (normalment és = 1)
  • theta: precisió angular en radians (sempre = np.pi / 180 ~ 1 grau)
  • min_threshold: vot mínim que hauria d'obtenir perquè es considerés com una línia
  • minLineLength: longitud mínima de línia en píxels. Qualsevol línia més curta que aquest número no es considera una línia.
  • maxLineGap: espai màxim en píxels entre 2 línies que es tractarà com a 1 línia. (No s'utilitza en el meu cas, ja que les línies de carrils que faig servir no tenen cap desnivell).

Aquesta funció retorna els punts finals d'una línia. Des del meu bucle principal es crida la següent funció per detectar línies mitjançant la transformada de Hough:

def detect_line_segments (cropped_edges):

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

Pendent mitjà i intercepció (m, b):

recordem que l’equació de recta ve donada per y = mx + b. On m és el pendent de la línia i b és la intersecció en y. En aquesta part, es calcularà la mitjana de pendents i interseccions de segments de línia detectats mitjançant la transformada de Hough. Abans de fer-ho, fem una ullada a la foto del marc original que es mostra més amunt. Sembla que el carril esquerre puja cap amunt, de manera que té un pendent negatiu (recordeu el punt d’inici del sistema de coordenades?). En altres paraules, la línia del carril esquerre té x1 <x2 i y2 x1 i y2> y1, la qual cosa donarà un pendent positiu. Per tant, totes les línies amb pendent positiu es consideren punts de carril dret. En cas de línies verticals (x1 = x2), el pendent serà infinit. En aquest cas, saltarem totes les línies verticals per evitar un error. Per afegir més precisió a aquesta detecció, cada fotograma es divideix en dues regions (dreta i esquerra) a través de dues línies límit. Tots els punts d’amplada (punts de l’eix x) superiors a la línia límit dreta s’associen al càlcul del carril dret. I si tots els punts d’amplada són inferiors a la línia límit esquerra, s’associen al càlcul del carril esquerre. La següent funció pren el quadre en procés i els segments de carril detectats mitjançant la transformada de Hough i retorna el pendent i la intercepció mitjana de dues línies de carril.

def average_slope_intercept (frame, line_segments):

línies_de_línia = si els segments_de_línia és Cap: imprimeix ("no s'ha detectat cap segment de línia") retorna alçada de línies, amplada, _ = frame.shape left_fit = right_fit = límit = left_region_boundary = ample * (1 - límit) right_region_boundary = ample * límit per al segment de línia en els segments de línia: per a x1, y1, x2, y2 en el segment de línia: si x1 == x2: print ("saltant línies verticals (pendent = infinit)") continue fit = np.polyfit ((x1, x2), (y1, y2), 1) pendent = (y2 - y1) / (x2 - x1) intercepció = y1 - (pendent * x1) si pendent <0: si x1 <límit_regió_esquerra i x2 límit_regió_dret i x2> límit_regió_dret: ajust_dret. append ((pendent, intercepció)) 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 és una matriu 2-D que consta de les coordenades de les línies de carril dret i esquerre # per exemple: lan e_lines =

make_points () és una funció auxiliar per a la funció average_slope_intercept () que retornarà les coordenades delimitades de les línies de carrils (des de la part inferior fins a la meitat del quadre).

def make_points (marc, línia):

alçada, amplada, _ = pendent de la forma del marc, intercepció = línia y1 = alçada # inferior del marc y2 = int (y1 / 2) # fer punts des de la meitat del marc cap avall si pendent == 0: pendent = 0,1 x1 = int ((y1 - intercepció) / pendent) x2 = int ((y2 - intercepció) / pendent) retornar

Per evitar la divisió per 0, es presenta una condició. Si pendent = 0 que significa y1 = y2 (línia horitzontal), doneu a la pendent un valor proper a 0. Això no afectarà el rendiment de l'algorisme, ja que evitarà casos impossibles (dividir per 0).

Per mostrar les línies de carrils als marcs, s'utilitza la funció següent:

def display_lines (frame, lines, line_color = (0, 255, 0), line_width = 6): # color de línia (B, G, R)

image_line = np.zeros_like (frame) si les línies no són Cap: per a línies en línies: per a x1, y1, x2, y2 en línia: cv2.line (line_image, (x1, y1), (x2, y2), line_color, amplada_línia) imatge_línia = cv2.addWeighted (marc, 0,8, imatge_línia, 1, 1) retorna imatge_línia

La funció cv2.addWeighted () pren els següents paràmetres i s’utilitza per combinar dues imatges però donant un pes a cadascuna d’elles.

cv2.addWeighted (imatge1, alfa, imatge2, beta, gamma)

I calcula la imatge de sortida mitjançant la següent equació:

sortida = alfa * imatge1 + beta * imatge2 + gamma

Aquí es deriva més informació sobre la funció cv2.addWeighted ().

Calcular i mostrar la línia de capçalera:

Aquest és l'últim pas abans d'aplicar velocitats als nostres motors. La línia de rumb és responsable de donar al motor de direcció la direcció en què hauria de girar i de donar als motors d’acceleració la velocitat a la qual funcionaran. El càlcul de la línia d’encapçalament és trigonometria pura, s’utilitzen funcions trigonomètriques tan i atan (tan ^ -1). Alguns casos extrems són quan la càmera detecta només una línia de carril o quan no detecta cap línia. Tots aquests casos es mostren a la funció següent:

def get_steering_angle (marc, línies_carril):

height, width, _ = frame.shape if len (lane_lines) == 2: # si es detecten dues línies de carrils _, _, left_x2, _ = lane_lines [0] [0] # extract left x2 from lane_lines array _, _, right_x2, _ = lane_lines [1] [0] # extract right x2 from lane_lines array mid = int (width / 2) x_offset = (left_x2 + right_x2) / 2 - mid y_offset = int (height / 2) elif len (lane_lines) == 1: # si només es detecta una línia x1, _, x2, _ = lane_lines [0] [0] x_offset = x2 - x1 y_offset = int (height / 2) elif len (lane_lines) == 0: # si no es detecta cap línia x_offset = 0 y_offset = int (altura / 2) angle_to_mid_radian = math.atan (x_offset / y_offset) angle_to_mid_deg = int (angle_to_mid_radian * 180.0 / math.pi) direction_angle = angle_to_mid_deg + 90 return

x_offset en el primer cas és quant difereix la mitjana ((x2 dreta + x2 esquerra) / 2) de la meitat de la pantalla. y_offset sempre es considera altura / 2. La darrera imatge superior mostra un exemple de línia de capçalera. angle_to_mid_radians és el mateix que "theta" que es mostra a la darrera imatge superior. Si angle de direcció = 90, vol dir que el cotxe té una línia de direcció perpendicular a la línia "alçada / 2" i el cotxe avançarà sense direcció. Si angle de direcció> 90, el cotxe hauria de girar cap a la dreta, en cas contrari hauria de girar cap a l’esquerra. Per mostrar la línia de capçalera, s'utilitza la funció següent:

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

heading_image = np.zeros_like (frame) height, width, _ = frame.shape steering_angle_radian = direction_angle / 180.0 * math.pi x1 = int (width / 2) y1 = height x2 = int (x1 - height / 2 / math.tan (direcció_angle_radian)) y2 = int (alçada / 2) cv2.line (imatge_encapçalament, (x1, y1), (x2, y2), color_línia, amplada_línia) imatge_encapçalament = cv2.add Pesat (marc, 0.8, imatge_encapçalament, 1, 1) torna imatge_títol

La funció anterior pren el marc en què es dibuixarà la línia de capçalera i l’angle de direcció com a entrada. Retorna la imatge de la línia de capçalera. El marc de la línia de capçalera presa en el meu cas es mostra a la imatge superior.

Combinació de tots els codis:

El codi ara està preparat per muntar-se. El següent codi mostra el bucle principal del programa que crida a cada funció:

importar cv2

importa numpy com a np video = cv2. VideoCapture (0) video.set (cv2. CAP_PROP_FRAME_WIDTH, 320) video.set (cv2. CAP_PROP_FRAME_HEIGHT, 240) mentre és True: ret, frame = video.read () frame = cv2.flip (frame, -1) #Calling the functions hsv = convert_to_HSV (frame) edge = detect_edges (hsv) roi = region_of_interest (edge) line_segments = detect_line_segments (roi) lane_lines = average_slope_intercept (frame, line_segments) lane_lines_image = display_lines (frame, lane_lines) = get_steering_angle (frame, lane_lines) heading_image = display_heading_line (lane_lines_image, direction_angle) key = cv2.waitKey (1) if key == 27: break video.release () cv2.destroyAllWindows ()

Pas 6: Aplicació del control PD

Aplicació del control PD
Aplicació del control PD

Ara tenim el nostre angle de direcció a punt per ser alimentat als motors. Com s'ha esmentat anteriorment, si l'angle de direcció és superior a 90, el cotxe hauria de girar a la dreta en cas contrari, hauria de girar a l'esquerra. He aplicat un codi senzill que fa girar el motor de direcció cap a la dreta si l’angle és superior a 90 i el gira cap a l’esquerra si l’angle de direcció és inferior a 90 a una velocitat d’acceleració constant de (10% PWM), però tinc molts errors. El principal error que he tingut és que quan el cotxe s’acosta a qualsevol gir, el motor de la direcció actua directament, però els motors d’acceleració s’encallen. Vaig intentar augmentar la velocitat de regulació (20% PWM) per torns però vaig acabar amb el robot que sortia dels carrils. Necessitava alguna cosa que augmentés molt la velocitat de regulació si l'angle de direcció és molt gran i augmenta una mica la velocitat si l'angle de direcció no és tan gran, aleshores disminueix la velocitat fins a un valor inicial a mesura que el cotxe s'aproxima a 90 graus (movent-se recte). La solució era utilitzar un controlador PD.

El controlador PID significa controlador proporcional, integral i derivat. Aquest tipus de controladors lineals s’utilitzen àmpliament en aplicacions de robòtica. La imatge superior mostra el típic bucle de control de retroalimentació PID. L'objectiu d'aquest controlador és arribar al "punt de consigna" amb la forma més eficient a diferència dels controladors "on-off" que activen o apaguen la planta segons algunes condicions. Cal conèixer algunes paraules clau:

  • Punt de consigna: és el valor desitjat que voleu que assoleixi el vostre sistema.
  • Valor real: és el valor real detectat pel sensor.
  • Error: és la diferència entre el valor de consigna i el valor real (error = Valor de consigna - Valor real).
  • Variable controlada: des del seu nom, la variable que voleu controlar.
  • Kp: constant proporcional.
  • Ki: constant integral.
  • Kd: constant derivada.

En resum, el bucle del sistema de control PID funciona de la següent manera:

  • L'usuari defineix el punt de referència necessari per assolir el sistema.
  • L'error es calcula (error = setpoint - real).
  • El controlador P genera una acció proporcional al valor de l'error. (augmenta l'error, també augmenta l'acció P)
  • El controlador I integrarà l'error amb el pas del temps, que elimina l'error d'estat estacionari del sistema, però augmenta la seva superació.
  • El controlador D és simplement la derivada del temps de l'error. En altres paraules, és el pendent de l'error. Fa una acció proporcional a la derivada de l'error. Aquest controlador augmenta l'estabilitat del sistema.
  • La sortida del controlador serà la suma dels tres controladors. La sortida del controlador es convertirà en 0 si l’error esdevé 0.

Podeu trobar una gran explicació del controlador PID aquí.

Tornant al cotxe que mantenia el carril, la meva variable controlada era la velocitat d’estrangulació (ja que la direcció només té dos estats a la dreta o a l’esquerra). S'utilitza un controlador PD per a aquest propòsit, ja que l'acció D augmenta molt la velocitat d'acceleració si el canvi d'error és molt gran (és a dir, una gran desviació) i alenteix el cotxe si aquest canvi d'error s'aproxima a 0. Vaig fer els següents passos per implementar un PD controlador:

  • Estableix el punt de consigna a 90 graus (sempre vull que el cotxe es mogui recte)
  • Calculat l’angle de desviació respecte al mig
  • La desviació dóna dues dades: quant de gran és l’error (magnitud de la desviació) i quina direcció ha de prendre el motor de direcció (signe de desviació). Si la desviació és positiva, el cotxe hauria de girar cap a la dreta, en cas contrari hauria de girar cap a l’esquerra.
  • Com que la desviació és negativa o positiva, es defineix una variable "error" i sempre igual al valor absolut de la desviació.
  • L'error es multiplica per una constant Kp.
  • L’error experimenta una diferenciació temporal i es multiplica per una constant Kd.
  • La velocitat dels motors s’actualitza i el bucle torna a començar.

El següent codi s’utilitza al bucle principal per controlar la velocitat dels motors d’estrangulació:

velocitat = 10 # velocitat de funcionament en% PWM

#Variables que s’actualitzaran cada bucle lastTime = 0 lastError = 0 # constants PD Kp = 0,4 Kd = Kp * 0,65 Mentre és cert: ara = time.time () # variable d’hora actual dt = ara - lastTime desviació = angle de direcció - 90 # equivalent a angle_to_mid_deg error variable = abs (desviació) si desviació -5: # no dirigeixi si hi ha una desviació del rang d'error de 10 graus = 0 error = 0 GPIO.output (in1, GPIO. LOW) GPIO.output (in2, GPIO. LOW) steering.stop () elif deviation> 5: # steer right if the deviation is positive GPIO.output (in1, GPIO. LOW) GPIO.output (in2, GPIO. HIGH) steering.start (100) elif deviation < -5: # gireu a l'esquerra si la desviació és negativa GPIO.output (in1, GPIO. HIGH) GPIO.output (in2, GPIO. LOW) steering.start (100) derivative = kd * (error - lastError) / dt proporcional = kp * error PD = int (velocitat + derivada + proporcional) spd = abs (PD) si spd> 25: spd = 25 throttle.start (spd) lastError = error lastTime = time.time ()

Si l’error és molt gran (la desviació del mig és elevada), les accions proporcionals i derivades són elevades, donant lloc a una velocitat d’estrangulació elevada. Quan l’error s’acosta a 0 (la desviació del mig és baixa), l’acció derivada actua de forma inversa (el pendent és negatiu) i la velocitat d’estrangulació és baixa per mantenir l’estabilitat del sistema. A continuació s’adjunta el codi complet.

Pas 7: Resultats

Els vídeos anteriors mostren els resultats que he obtingut. Necessita més afinació i més ajustos. Connectava el raspberry pi a la pantalla LCD perquè el vídeo que emetia a la xarxa tenia una latència elevada i era molt frustrant treballar, per això hi ha cables connectats al raspberry pi al vídeo. Vaig utilitzar taules d’escuma per dibuixar la pista.

Estic esperant de conèixer les vostres recomanacions per millorar aquest projecte. Com espero que aquest instructable sigui prou bo per proporcionar-vos informació nova.

Recomanat: