Table des matières:

Voiture de maintien de voie autonome utilisant Raspberry Pi et OpenCV : 7 étapes (avec photos)
Voiture de maintien de voie autonome utilisant Raspberry Pi et OpenCV : 7 étapes (avec photos)

Vidéo: Voiture de maintien de voie autonome utilisant Raspberry Pi et OpenCV : 7 étapes (avec photos)

Vidéo: Voiture de maintien de voie autonome utilisant Raspberry Pi et OpenCV : 7 étapes (avec photos)
Vidéo: Vision industrielle + IA : Tout se met en place ! 2024, Novembre
Anonim
Voiture de maintien de voie autonome utilisant Raspberry Pi et OpenCV
Voiture de maintien de voie autonome utilisant Raspberry Pi et OpenCV

Dans ce instructables, un robot autonome de maintien de voie sera mis en œuvre et passera par les étapes suivantes:

  • Rassembler des pièces
  • Installation des prérequis logiciels
  • Assemblage du matériel
  • Premier test
  • Détection des lignes de voie et affichage de la ligne de guidage à l'aide d'openCV
  • Implémentation d'un contrôleur PD
  • Résultats

Étape 1: Rassembler les composants

Rassembler des composants
Rassembler des composants
Rassembler des composants
Rassembler des composants
Rassembler des composants
Rassembler des composants
Rassembler des composants
Rassembler des composants

Les images ci-dessus montrent tous les composants utilisés dans ce projet:

  • Voiture RC: j'ai eu la mienne dans un magasin local de mon pays. Il est équipé de 3 moteurs (2 pour l'étranglement et 1 pour la direction). Le principal inconvénient de cette voiture est que la direction est limitée entre "pas de direction" et "direction complète". En d'autres termes, il ne peut pas diriger à un angle spécifique, contrairement aux voitures RC à servodirection. Vous pouvez trouver un kit de voiture similaire spécialement conçu pour Raspberry Pi à partir d'ici.
  • Raspberry pi 3 modèle b+: c'est le cerveau de la voiture qui va gérer de nombreuses étapes de traitement. Il repose sur un processeur quad core 64 bits cadencé à 1,4 GHz. J'ai le mien d'ici.
  • Module de caméra Raspberry pi 5 mp: il prend en charge l'enregistrement 1080p @ 30 fps, 720p @ 60 fps et 640x480p 60/90. Il prend également en charge l'interface série qui peut être branchée directement sur le raspberry pi. Ce n'est pas la meilleure option pour les applications de traitement d'images, mais c'est suffisant pour ce projet et c'est très bon marché. J'ai le mien d'ici.
  • Pilote de moteur: est utilisé pour contrôler les directions et les vitesses des moteurs à courant continu. Il prend en charge le contrôle de 2 moteurs à courant continu sur 1 carte et peut supporter 1,5 A.
  • Banque d'alimentation (facultatif): j'ai utilisé une banque d'alimentation (évaluée à 5 V, 3 A) pour alimenter le Raspberry Pi séparément. Un convertisseur abaisseur (convertisseur buck: courant de sortie 3A) doit être utilisé afin d'alimenter le raspberry pi à partir d'une source.
  • Batterie LiPo 3s (12 V): Les batteries Lithium Polymère sont connues pour leurs excellentes performances dans le domaine de la robotique. Il est utilisé pour alimenter le pilote du moteur. J'ai acheté le mien d'ici.
  • Fils de liaison mâle à mâle et femelle à femelle.
  • Ruban adhésif double face: utilisé pour monter les composants sur la voiture RC.
  • Ruban bleu: C'est un élément très important de ce projet, il est utilisé pour faire les lignes à deux voies entre lesquelles la voiture circulera. Vous pouvez choisir la couleur de votre choix, mais je vous recommande de choisir des couleurs différentes de celles de l'environnement.
  • Attaches zippées et barres de bois.
  • Tournevis.

Étape 2: Installation d'OpenCV sur Raspberry Pi et configuration de l'affichage à distance

Installation d'OpenCV sur Raspberry Pi et configuration de l'affichage à distance
Installation d'OpenCV sur Raspberry Pi et configuration de l'affichage à distance

Cette étape est un peu ennuyeuse et prendra un certain temps.

OpenCV (Open source Computer Vision) est une bibliothèque logicielle open source de vision par ordinateur et d'apprentissage automatique. La bibliothèque compte plus de 2500 algorithmes optimisés. Suivez CE guide très simple pour installer l'openCV sur votre raspberry pi ainsi que pour installer le système d'exploitation raspberry pi (si vous ne l'avez toujours pas fait). Veuillez noter que le processus de construction de l'openCV peut prendre environ une heure et demie dans une pièce bien refroidie (puisque la température du processeur deviendra très élevée !), alors prenez un thé et attendez patiemment:D.

Pour l'affichage à distance, suivez également CE guide pour configurer l'accès à distance à votre raspberry pi à partir de votre appareil Windows/Mac.

Étape 3: Connecter les pièces ensemble

Connecter des pièces ensemble
Connecter des pièces ensemble
Connecter des pièces ensemble
Connecter des pièces ensemble
Connecter des pièces ensemble
Connecter des pièces ensemble

Les images ci-dessus montrent les connexions entre raspberry pi, module de caméra et pilote de moteur. Veuillez noter que les moteurs que j'ai utilisés absorbent 0,35 A à 9 V chacun, ce qui permet au conducteur de moteur de faire fonctionner 3 moteurs en même temps en toute sécurité. Et comme je veux contrôler la vitesse des 2 moteurs d'étranglement (1 à l'arrière et 1 à l'avant) exactement de la même manière, je les ai connectés au même port. J'ai monté le pilote du moteur sur le côté droit de la voiture en utilisant du ruban adhésif double. En ce qui concerne le module de caméra, j'ai inséré une attache zippée entre les trous de vis comme le montre l'image ci-dessus. Ensuite, j'installe la caméra sur une barre de bois afin de pouvoir ajuster la position de la caméra comme je le souhaite. Essayez d'installer la caméra au milieu de la voiture autant que possible. Je recommande de placer la caméra à au moins 20 cm au-dessus du sol afin que le champ de vision devant la voiture s'améliore. Le schéma de Fritzing est joint ci-dessous.

Étape 4: Premier test

Premier test
Premier test
Premier test
Premier test

Test de la caméra:

Une fois la caméra installée et la bibliothèque openCV construite, il est temps de tester notre première image ! Nous prendrons une photo de pi cam et l'enregistrerons sous le nom "original.jpg". Cela peut se faire de 2 manières:

1. Utilisation des commandes du terminal:

Ouvrez une nouvelle fenêtre de terminal et tapez la commande suivante:

raspistill -o original.jpg

Cela prendra une image fixe et l'enregistrera dans le répertoire "/pi/original.jpg".

2. En utilisant n'importe quel IDE python (j'utilise IDLE):

Ouvrez une nouvelle esquisse et écrivez le code suivant:

importer cv2

video = cv2. VideoCapture(0) alors que True: ret, frame = video.read() frame = cv2.flip(frame, -1) # utilisé pour retourner l'image verticalement cv2.imshow('original', frame) cv2. imwrite('original.jpg', frame) key = cv2.waitKey(1) if key == 27: break video.release() cv2.destroyAllWindows()

Voyons ce qui s'est passé dans ce code. La première ligne importe notre bibliothèque openCV pour utiliser toutes ses fonctions. la fonction VideoCapture(0) commence à diffuser une vidéo en direct à partir de la source déterminée par cette fonction, dans ce cas c'est 0 qui signifie caméra raspi. si vous avez plusieurs caméras, des numéros différents doivent être placés. video.read() lira chaque image provenant de la caméra et l'enregistrera dans une variable appelée "frame". La fonction flip () retournera l'image par rapport à l'axe des y (verticalement) puisque je monte mon appareil photo à l'envers. imshow() affichera nos cadres précédés du mot "original" et imwrite() enregistrera notre photo sous le nom original.jpg. waitKey(1) attendra 1 ms qu'un bouton quelconque du clavier soit enfoncé et renvoie son code ASCII. si le bouton d'échappement (esc) est enfoncé, une valeur décimale de 27 est renvoyée et interrompra la boucle en conséquence. video.release() arrêtera l'enregistrement et destroyAllWindows() fermera chaque image ouverte par la fonction imshow().

Je vous recommande de tester votre photo avec la deuxième méthode pour vous familiariser avec les fonctions d'openCV. L'image est enregistrée dans le répertoire "/pi/original.jpg". La photo originale que mon appareil photo a prise est montrée ci-dessus.

Tester les moteurs:

Cette étape est indispensable pour déterminer le sens de rotation de chaque moteur. Tout d'abord, faisons une brève introduction sur le principe de fonctionnement d'un pilote de moteur. L'image ci-dessus montre le brochage du pilote de moteur. La validation A, l'entrée 1 et l'entrée 2 sont associées à la commande du moteur A. Enable B, Input 3 et Input 4 sont associés à la commande du moteur B. Le contrôle de direction est établi par la partie "Entrée" et le contrôle de vitesse est établi par la partie "Activation". Pour contrôler le sens du moteur A par exemple, réglez l'entrée 1 sur HIGH (3,3 V dans ce cas puisque nous utilisons un raspberry pi) et réglez l'entrée 2 sur LOW, le moteur tournera dans un sens spécifique et en réglant les valeurs opposées à l'entrée 1 et à l'entrée 2, le moteur tournera dans le sens opposé. Si Entrée 1 = Entrée 2 = (HIGH ou LOW), le moteur ne tournera pas. Les broches d'activation prennent un signal d'entrée de modulation de largeur d'impulsion (PWM) de la framboise (0 à 3,3 V) et font fonctionner les moteurs en conséquence. Par exemple, un signal 100 % PWM signifie que nous travaillons sur la vitesse maximale et un signal 0 % PWM signifie que le moteur ne tourne pas. Le code suivant est utilisé pour déterminer les directions des moteurs et tester leurs vitesses.

heure d'importation

importer RPi. GPIO en tant que GPIO GPIO.setwarnings(False) # Broches du moteur de direction Steering_enable = 22 # Broche physique 15 in1 = 17 # Broche physique 11 in2 = 27 # Broche physique 13 # Broches des moteurs d'accélérateurthrottle_enable = 25 # Broche physique 22 in3 = 23 # Broche physique 16 in4 = 24 # Broche physique 18 GPIO.setmode(GPIO. BCM) # Utilisez la numérotation GPIO au lieu de la numérotation physique 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) # Contrôle du moteur de direction GPIO.output(in1, GPIO. HIGH) GPIO.output(in2, GPIO. LOW) direction = GPIO. PWM(steering_enable, 1000) # régler la fréquence de commutation sur 1000 Hz Steering.stop() # Commande des moteurs d'accélérateur GPIO.output(in3, GPIO. HIGH) GPIO.output(in4, GPIO. LOW) accélérateur = GPIO. PWM(throttle_enable, 1000) # règle la fréquence de commutation sur 1000 Hz étrangleur.stop() time.sleep(1) accélérateur.start(25) # démarre le moteur à 25 % signal PWM-> (0,25 * tension de la batterie) - conducteur loss Steering.start(100) # démarre le moteur à 100 % du signal PWM-> (1 * Tension de la batterie) - temps de perte du conducteur. Sleep(3) Throttle.stop() Steering.stop()

Ce code fera fonctionner les moteurs d'étranglement et le moteur de direction pendant 3 secondes, puis les arrêtera. La (perte du conducteur) peut être déterminée à l'aide d'un voltmètre. Par exemple, nous savons qu'un signal 100 % PWM devrait donner la pleine tension de la batterie à la borne du moteur. Mais, en réglant PWM à 100%, j'ai constaté que le pilote provoquait une chute de 3 V et que le moteur recevait 9 V au lieu de 12 V (exactement ce dont j'ai besoin !). La perte n'est pas linéaire, c'est-à-dire que la perte à 100% est très différente de la perte à 25%. Après avoir exécuté le code ci-dessus, mes résultats étaient les suivants:

Résultats de l'étranglement: si in3 = HAUT et in4 = BAS, les moteurs d'étranglement auront une rotation dans le sens horaire (CW), c'est-à-dire que la voiture avancera. Sinon, la voiture reculera.

Résultats de la direction: si in1 = HAUT et in2 = BAS, le moteur de direction tournera au maximum à gauche, c'est-à-dire que la voiture tournera à gauche. Sinon, la voiture tournera à droite. Après quelques expériences, j'ai découvert que le moteur de direction ne tournerait pas si le signal PWM n'était pas à 100% (c'est-à-dire que le moteur se dirigera complètement vers la droite ou complètement vers la gauche).

Étape 5: Détection des lignes de voie et calcul de la ligne de cap

Détection des lignes de voie et calcul de la ligne de cap
Détection des lignes de voie et calcul de la ligne de cap
Détection des lignes de voie et calcul de la ligne de cap
Détection des lignes de voie et calcul de la ligne de cap
Détection des lignes de voie et calcul de la ligne de cap
Détection des lignes de voie et calcul de la ligne de cap

Dans cette étape, l'algorithme qui contrôlera le mouvement de la voiture sera expliqué. La première image montre l'ensemble du processus. L'entrée du système est des images, la sortie est thêta (angle de braquage en degrés). Notez que, le traitement se fait sur 1 image et sera répété sur toutes les images.

Caméra:

La caméra commencera à enregistrer une vidéo avec une résolution (320 x 240). Je recommande de réduire la résolution afin d'obtenir une meilleure fréquence d'images (fps) car une chute de fps se produira après l'application de techniques de traitement à chaque image. Le code ci-dessous sera la boucle principale du programme et ajoutera chaque étape sur ce code.

importer cv2

importer numpy as np video = cv2. VideoCapture(0) video.set(cv2. CAP_PROP_FRAME_WIDTH, 320) # définir la largeur à 320 p video.set(cv2. CAP_PROP_FRAME_HEIGHT, 240) # définir la hauteur à 240 p # La boucle pendant Vrai: 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()

Le code ici montrera l'image originale obtenue à l'étape 4 et est montré dans les images ci-dessus.

Convertir en espace colorimétrique HSV:

Maintenant, après avoir pris l'enregistrement vidéo sous forme d'images à partir de la caméra, l'étape suivante consiste à convertir chaque image en espace colorimétrique Teinte, Saturation et Valeur (HSV). Le principal avantage de le faire est de pouvoir différencier les couleurs par leur niveau de luminance. Et voici une bonne explication de l'espace colorimétrique HSV. La conversion en HSV se fait via la fonction suivante:

def convert_to_HSV(frame):

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

Cette fonction sera appelée depuis la boucle principale et renverra la trame dans l'espace colorimétrique HSV. Le cadre obtenu par moi dans l'espace colorimétrique HSV est montré ci-dessus.

Détecter la couleur bleue et les bords:

Après avoir converti l'image en espace colorimétrique HSV, il est temps de détecter uniquement la couleur qui nous intéresse (c'est-à-dire la couleur bleue puisque c'est la couleur des lignes de voie). Pour extraire la couleur bleue d'une trame HSV, une plage de teinte, de saturation et de valeur doit être spécifiée. référez-vous ici pour avoir une meilleure idée sur les valeurs HSV. Après quelques expériences, les limites supérieure et inférieure de la couleur bleue sont indiquées dans le code ci-dessous. Et pour réduire la distorsion globale dans chaque image, les bords sont détectés uniquement à l'aide d'un détecteur de bord astucieux. Pour en savoir plus sur Canny Edge, cliquez ici. Une règle de base est de sélectionner les paramètres de la fonction Canny() avec un rapport de 1:2 ou 1:3.

def detect_edges(frame):

lower_blue = np.array([90, 120, 0], dtype = "uint8") # limite inférieure de la couleur bleue upper_blue = np.array([150, 255, 255], dtype="uint8") # limite supérieure de blue color mask = cv2.inRange(hsv, lower_blue, upper_blue) # ce masque filtrera tout sauf le bleu # détecter les bords bords = cv2. Canny(mask, 50, 100) cv2.imshow("bords", bords) retourner les bords

Cette fonction sera également appelée depuis la boucle principale qui prend en paramètre le cadre de l'espace couleur HSV et renvoie le cadre bordé. Le cadre bordé que j'ai obtenu se trouve ci-dessus.

Sélectionnez la région d'intérêt (ROI):

La sélection de la région d'intérêt est cruciale pour se concentrer uniquement sur 1 région de la trame. Dans ce cas, je ne veux pas que la voiture voit beaucoup d'éléments dans l'environnement. Je veux juste que la voiture se concentre sur les lignes de voie et ignore tout le reste. P. S: le système de coordonnées (axes x et y) part du coin supérieur gauche. En d'autres termes, le point (0, 0) part du coin supérieur gauche. l'axe y étant la hauteur et l'axe x étant la largeur. Le code ci-dessous sélectionne la région d'intérêt pour se concentrer uniquement sur la moitié inférieure du cadre.

def region_of_interest(edges):

height, width = edge.shape # extrait la hauteur et la largeur des bords frame mask = np.zeros_like(edges) # crée une matrice vide avec les mêmes dimensions que les bords frame # se concentre uniquement sur la moitié inférieure de l'écran # spécifie les coordonnées de 4 points (en bas à gauche, en haut à gauche, en haut à droite, en bas à droite) polygone = np.array(

Cette fonction prendra le cadre bordé comme paramètre et dessine un polygone avec 4 points prédéfinis. Il se concentrera uniquement sur ce qui se trouve à l'intérieur du polygone et ignorera tout ce qui se trouve à l'extérieur. Le cadre de ma région d'intérêt est indiqué ci-dessus.

Détecter les segments de ligne:

La transformation de Hough est utilisée pour détecter des segments de ligne à partir d'une trame bordée. La transformation de Hough est une technique permettant de détecter n'importe quelle forme sous forme mathématique. Il peut détecter presque n'importe quel objet même s'il est déformé en fonction d'un certain nombre de votes. une excellente référence pour la transformation de Hough est montrée ici. Pour cette application, la fonction cv2. HoughLinesP() est utilisée pour détecter les lignes dans chaque trame. Les paramètres importants que cette fonction prend sont:

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

  • Cadre: est le cadre dans lequel nous voulons détecter les lignes.
  • rho: C'est la précision de la distance en pixels (généralement c'est = 1)
  • theta: précision angulaire en radians (toujours = np.pi/180 ~ 1 degré)
  • min_threshold: vote minimum qu'il devrait obtenir pour qu'il soit considéré comme une ligne
  • minLineLength: longueur minimale de la ligne en pixels. Toute ligne plus courte que ce nombre n'est pas considérée comme une ligne.
  • maxLineGap: écart maximum en pixels entre 2 lignes à traiter comme 1 ligne. (Il n'est pas utilisé dans mon cas car les lignes de voie que j'utilise n'ont pas d'espace).

Cette fonction renvoie les extrémités d'une ligne. La fonction suivante est appelée depuis ma boucle principale pour détecter les lignes à l'aide de la transformation de 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

Pente moyenne et Interception (m, b):

rappelons que l'équation de la droite est donnée par y = mx + b. Où m est la pente de la ligne et b est l'ordonnée à l'origine. Dans cette partie, la moyenne des pentes et des interceptions des segments de ligne détectés à l'aide de la transformée de Hough sera calculée. Avant de le faire, jetons un coup d'œil à la photo du cadre d'origine ci-dessus. La voie de gauche semble aller vers le haut, elle a donc une pente négative (vous vous souvenez du point de départ du système de coordonnées ?). En d'autres termes, la ligne de la voie de gauche a x1 < x2 et y2 x1 et y2 > y1 ce qui donnera une pente positive. Ainsi, toutes les lignes avec une pente positive sont considérées comme des points de voie de droite. En cas de lignes verticales (x1 = x2), la pente sera l'infini. Dans ce cas, nous sauterons toutes les lignes verticales pour éviter d'avoir une erreur. Pour ajouter plus de précision à cette détection, chaque trame est divisée en deux régions (droite et gauche) à travers 2 lignes de délimitation. Tous les points de largeur (points de l'axe X) supérieurs à la ligne de démarcation droite sont associés au calcul de la voie de droite. Et si tous les points de largeur sont inférieurs à la ligne de délimitation gauche, ils sont associés au calcul de la voie de gauche. La fonction suivante prend la trame en cours de traitement et les segments de voie détectés à l'aide de la transformation de Hough et renvoie la pente moyenne et l'interception de deux lignes de voie.

def average_slope_intercept(frame, line_segments):

lane_lines = si line_segments est None: print("aucun segment de ligne détecté") return lane_lines height, width, _ = frame.shape left_fit = right_fit = boundary = left_region_boundary = width * (1 - boundary) right_region_boundary = largeur * limite pour line_segment dans line_segments: pour x1, y1, x2, y2 dans line_segment: if x1 == x2: print("saut des lignes verticales (pente = infini)") continue fit = np.polyfit((x1, x2), (y1, y2), 1) pente = (y2 - y1) / (x2 - x1) intercept = y1 - (pente * x1) si pente < 0: si x1 < left_region_boundary et x2 right_region_boundary et x2 > right_region_boundary: right_fit. append((pente, intercept)) left_fit_average = np.average(left_fit, axis=0) si 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 est un tableau 2D composé des coordonnées des lignes de droite et de gauche # par exemple: lan e_lines =

make_points() est une fonction d'assistance pour la fonction average_slope_intercept() qui renverra les coordonnées bornées des lignes de voie (du bas au milieu du cadre).

def make_points(frame, line):

hauteur, largeur, _ = frame.shape pente, intercept = ligne y1 = hauteur # bas du cadre y2 = int(y1 / 2) # fait des points du milieu du cadre vers le bas si pente == 0: pente = 0,1 x1 = int((y1 - intercept) / pente) x2 = int((y2 - intercept) / pente) return

Pour éviter de diviser par 0, une condition est présentée. Si pente = 0, ce qui signifie y1 = y2 (ligne horizontale), donnez à la pente une valeur proche de 0. Cela n'affectera pas les performances de l'algorithme et évitera les cas impossibles (diviser par 0).

Pour afficher les lignes de voie sur les cadres, la fonction suivante est utilisée:

def display_lines(frame, lines, line_color=(0, 255, 0), line_width=6): # couleur de la ligne (B, G, R)

line_image = np.zeros_like(frame) si lines n'est pas None: pour la ligne dans les lignes: pour x1, y1, x2, y2 dans la ligne: 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 fonction cv2.addWeighted() prend les paramètres suivants et est utilisée pour combiner deux images mais en leur donnant un poids.

cv2.addWeighted(image1, alpha, image2, bêta, gamma)

Et calcule l'image de sortie en utilisant l'équation suivante:

sortie = alpha * image1 + bêta * image2 + gamma

Plus d'informations sur la fonction cv2.addWeighted() sont dérivées ici.

Calculer et afficher la ligne d'en-tête:

C'est la dernière étape avant d'appliquer des vitesses à nos moteurs. La ligne de cap est chargée de donner au moteur de direction la direction dans laquelle il doit tourner et de donner aux moteurs d'étranglement la vitesse à laquelle ils fonctionneront. Le calcul de la ligne de cap est de la trigonométrie pure, les fonctions trigonométriques tan et atan (tan^-1) sont utilisées. Certains cas extrêmes sont lorsque la caméra détecte une seule ligne de voie ou lorsqu'elle ne détecte aucune ligne. Tous ces cas sont représentés dans la fonction suivante:

def get_steering_angle(frame, lane_lines):

hauteur, largeur, _ = frame.shape if len(lane_lines) == 2: # si deux lignes de voie sont détectées _, _, left_x2, _ = lane_lines[0][0] # extraire left x2 du tableau lane_lines _, _, right_x2, _ = lane_lines[1][0] # extraire la droite x2 du tableau lane_lines mid = int(width / 2) x_offset = (left_x2 + right_x2) / 2 - mid y_offset = int(height / 2) elif len(lane_lines) == 1: # si une seule ligne est détectée x1, _, x2, _ = lane_lines[0][0] x_offset = x2 - x1 y_offset = int(height / 2) elif len(lane_lines) == 0: # si aucune ligne n'est détectée 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_deg + 90 return Steering_angle

x_offset dans le premier cas est de combien la moyenne ((droite x2 + gauche x2) / 2) diffère du milieu de l'écran. y_offset est toujours considéré comme étant la hauteur / 2. La dernière image ci-dessus montre un exemple de ligne de cap. angle_to_mid_radians est le même que "theta" montré dans la dernière image ci-dessus. Si Steering_angle = 90, cela signifie que la voiture a une ligne de cap perpendiculaire à la ligne "hauteur / 2" et la voiture avancera sans direction. Si Steering_angle > 90, la voiture doit braquer à droite sinon elle doit braquer à gauche. Pour afficher la ligne de titre, la fonction suivante est utilisée:

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

head_image = np.zeros_like(frame) height, width, _ = frame.shape Steering_angle_radian = Steering_angle / 180.0 * math.pi x1 = int(width / 2) y1 = height x2 = int(x1 - height / 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) retour en-tête_image

La fonction ci-dessus prend le cadre dans lequel la ligne de cap sera tracée et l'angle de braquage comme entrée. Il renvoie l'image de la ligne de cap. Le cadre de ligne de cap pris dans mon cas est montré dans l'image ci-dessus.

Combiner tous les codes ensemble:

Le code est maintenant prêt à être assemblé. Le code suivant montre la boucle principale du programme appelant chaque fonction:

importer cv2

importer numpy comme np video = cv2. VideoCapture(0) video.set(cv2. CAP_PROP_FRAME_WIDTH, 320) video.set(cv2. CAP_PROP_FRAME_HEIGHT, 240) tandis que True: ret, frame = video.read() frame = cv2.flip(frame, -1) #Appeler les fonctions hsv = convert_to_HSV(frame) edge = detect_edges(hsv) roi = region_of_interest(edges) line_segments = detect_line_segments(roi) lane_lines = average_slope_intercept(frame, line_segments) lane_lines_image_lane_lines_angle(frame) = 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()

Étape 6: Application du contrôle PD

Application du contrôle PD
Application du contrôle PD

Maintenant, nous avons notre angle de braquage prêt à être transmis aux moteurs. Comme mentionné précédemment, si l'angle de braquage est supérieur à 90, la voiture doit tourner à droite sinon elle doit tourner à gauche. J'ai appliqué un code simple qui fait tourner le moteur de direction à droite si l'angle est supérieur à 90 et le tourne à gauche si l'angle de braquage est inférieur à 90 à une vitesse d'étranglement constante de (10% PWM) mais j'ai eu beaucoup d'erreurs. La principale erreur que j'ai eue est que lorsque la voiture s'approche d'un virage, le moteur de direction agit directement mais les moteurs d'étranglement se bloquent. J'ai essayé d'augmenter la vitesse d'étranglement à (20 % PWM) dans les virages, mais j'ai terminé avec le robot quittant les voies. J'avais besoin de quelque chose qui augmente beaucoup la vitesse d'étranglement si l'angle de braquage est très grand et augmente un peu la vitesse si l'angle de braquage n'est pas si grand, puis diminue la vitesse à une valeur initiale à mesure que la voiture approche de 90 degrés (en se déplaçant tout droit). La solution consistait à utiliser un contrôleur PD.

Le contrôleur PID signifie contrôleur proportionnel, intégral et dérivé. Ce type de contrôleurs linéaires est largement utilisé dans les applications robotiques. L'image ci-dessus montre la boucle de contrôle de rétroaction PID typique. Le but de ce contrôleur est d'atteindre le "point de consigne" de la manière la plus efficace contrairement aux contrôleurs "marche-arrêt" qui allument ou éteignent l'installation selon certaines conditions. Certains mots-clés doivent être connus:

  • Point de consigne: est la valeur souhaitée que votre système doit atteindre.
  • Valeur réelle: est la valeur réelle détectée par le capteur.
  • Erreur: est la différence entre la valeur de consigne et la valeur réelle (erreur = Consigne - Valeur réelle).
  • Variable contrôlée: à partir de son nom, la variable que vous souhaitez contrôler.
  • Kp: Constante proportionnelle.
  • Ki: constante intégrale.
  • Kd: constante dérivée.

En bref, la boucle du système de contrôle PID fonctionne comme suit:

  • L'utilisateur définit le point de consigne que le système doit atteindre.
  • L'erreur est calculée (erreur = consigne - réelle).
  • Le contrôleur P génère une action proportionnelle à la valeur de l'erreur. (l'erreur augmente, l'action P augmente également)
  • Le contrôleur I intégrera l'erreur au fil du temps, ce qui élimine l'erreur d'état stable du système mais augmente son dépassement.
  • Le contrôleur D est simplement la dérivée temporelle de l'erreur. En d'autres termes, c'est la pente de l'erreur. Il effectue une action proportionnelle à la dérivée de l'erreur. Ce contrôleur augmente la stabilité du système.
  • La sortie du contrôleur sera la somme des trois contrôleurs. La sortie du contrôleur deviendra 0 si l'erreur devient 0.

Une excellente explication du contrôleur PID peut être trouvée ici.

Pour en revenir à la voiture de maintien de voie, ma variable contrôlée était la vitesse d'étranglement (puisque la direction n'a que deux états, à droite ou à gauche). Un contrôleur PD est utilisé à cette fin car l'action D augmente beaucoup la vitesse d'étranglement si le changement d'erreur est très important (c'est-à-dire un grand écart) et ralentit la voiture si ce changement d'erreur approche 0. J'ai suivi les étapes suivantes pour implémenter un PD manette:

  • Réglez le point de consigne à 90 degrés (je veux toujours que la voiture se déplace tout droit)
  • Calcul de l'angle de déviation par rapport au milieu
  • La déviation donne deux informations: la taille de l'erreur (amplitude de la déviation) et la direction que le moteur de direction doit prendre (signe de la déviation). Si la déviation est positive, la voiture doit virer à droite sinon elle doit virer à gauche.
  • L'écart étant soit négatif, soit positif, une variable "erreur" est définie et toujours égale à la valeur absolue de l'écart.
  • L'erreur est multipliée par une constante Kp.
  • L'erreur subit une différenciation temporelle et est multipliée par une constante Kd.
  • La vitesse des moteurs est mise à jour et la boucle recommence.

Le code suivant est utilisé dans la boucle principale pour contrôler la vitesse des moteurs d'étranglement:

vitesse = 10 # vitesse de fonctionnement en % PWM

#Variables à mettre à jour à chaque boucle lastTime = 0 lastError = 0 # Constantes PD Kp = 0.4 Kd = Kp * 0.65 While True: now = time.time() # variable d'heure actuelle dt = now - lastTime déviation = Steering_angle - 90 # équivalent to angle_to_mid_deg variable error = abs(déviation) if déviation -5: # ne pas diriger s'il y a une erreur de plage de 10 degrés déviation = 0 erreur = 0 GPIO.output(in1, GPIO. LOW) GPIO.output(in2, GPIO. LOW) Steering.stop() elif déviation > 5: # braquer à droite si l'écart est positif GPIO.output(in1, GPIO. LOW) GPIO.output(in2, GPIO. HIGH) Steering.start(100) elif déviation < -5: # braquer à gauche si l'écart est négatif GPIO.output(in1, GPIO. HIGH) GPIO.output(in2, GPIO. LOW) Steering.start(100) dérivée = kd * (error - lastError) / dt proportionnel = kp * erreur PD = int(vitesse + dérivée + proportionnelle) spd = abs(PD) si vitesse > 25: vitesse = 25 papillon.start(spd) lastError = erreur lastTime = time.time()

Si l'erreur est très importante (l'écart par rapport au milieu est élevé), les actions proportionnelles et dérivées sont élevées, ce qui entraîne une vitesse d'étranglement élevée. Lorsque l'erreur approche de 0 (l'écart par rapport au milieu est faible), l'action dérivée agit en sens inverse (la pente est négative) et la vitesse d'étranglement devient faible pour maintenir la stabilité du système. Le code complet est joint ci-dessous.

Étape 7: Résultats

Les vidéos ci-dessus montrent les résultats que j'ai obtenus. Il a besoin de plus de réglages et d'ajustements supplémentaires. Je connectais le raspberry pi à mon écran LCD car le streaming vidéo sur mon réseau avait une latence élevée et était très frustrant de travailler avec, c'est pourquoi il y a des fils connectés au raspberry pi dans la vidéo. J'ai utilisé des planches de mousse pour dessiner la piste.

J'attends vos recommandations pour améliorer ce projet! Comme j'espère que cette instructables était assez bonne pour vous donner de nouvelles informations.

Conseillé: