Tabla de contenido:

Automóvil autónomo que se mantiene en el carril con Raspberry Pi y OpenCV: 7 pasos (con imágenes)
Automóvil autónomo que se mantiene en el carril con Raspberry Pi y OpenCV: 7 pasos (con imágenes)

Video: Automóvil autónomo que se mantiene en el carril con Raspberry Pi y OpenCV: 7 pasos (con imágenes)

Video: Automóvil autónomo que se mantiene en el carril con Raspberry Pi y OpenCV: 7 pasos (con imágenes)
Video: Robot Autónomo con Raspberry Pi y Arduino - #NEXTPCB 2024, Mes de julio
Anonim
Coche autónomo que se mantiene en el carril con Raspberry Pi y OpenCV
Coche autónomo que se mantiene en el carril con Raspberry Pi y OpenCV

En este instructables, se implementará un robot de mantenimiento de carril autónomo y pasará por los siguientes pasos:

  • Recopilación de piezas
  • Requisitos previos de la instalación de software
  • Montaje de hardware
  • Primer examen
  • Detectar líneas de carril y mostrar la línea de guía usando openCV
  • Implementación de un controlador de DP
  • Resultados

Paso 1: Recopilación de componentes

Recopilación de componentes
Recopilación de componentes
Recopilación de componentes
Recopilación de componentes
Recopilación de componentes
Recopilación de componentes
Recopilación de componentes
Recopilación de componentes

Las imágenes de arriba muestran todos los componentes utilizados en este proyecto:

  • Coche RC: el mío lo compré en una tienda local de mi país. Está equipado con 3 motores (2 para estrangular y 1 para dirección). La principal desventaja de este coche es que la dirección está limitada entre "sin dirección" y "dirección completa". En otras palabras, no puede conducir en un ángulo específico, a diferencia de los coches RC con servodirección. Puede encontrar un kit de coche similar diseñado especialmente para raspberry pi desde aquí.
  • Raspberry pi 3 modelo b +: este es el cerebro del automóvil que manejará muchas etapas de procesamiento. Se basa en un procesador de cuatro núcleos de 64 bits con una frecuencia de 1,4 GHz. Yo saqué el mío de aquí.
  • Módulo de cámara Raspberry pi de 5 mp: admite grabación de 1080p a 30 fps, 720p a 60 fps y 640x480p 60/90. También es compatible con la interfaz en serie que se puede conectar directamente a la Raspberry Pi. No es la mejor opción para aplicaciones de procesamiento de imágenes pero es suficiente para este proyecto y además es muy económico. Yo saqué el mío de aquí.
  • Controlador de motor: se utiliza para controlar las direcciones y velocidades de los motores de CC. Admite el control de 2 motores de CC en 1 placa y puede soportar 1,5 A.
  • Banco de energía (opcional): utilicé un banco de energía (nominal de 5 V, 3 A) para encender la frambuesa pi por separado. Se debe usar un convertidor reductor (convertidor reductor: corriente de salida de 3 A) para encender la frambuesa pi desde 1 fuente.
  • Batería LiPo de 3 s (12 V): las baterías de polímero de litio son conocidas por su excelente rendimiento en el campo de la robótica. Se utiliza para alimentar el controlador del motor. Compré el mío de aquí.
  • Cables de puente macho a macho y hembra a hembra.
  • Cinta de doble cara: se utiliza para montar los componentes en el coche RC.
  • Cinta azul: Este es un componente muy importante de este proyecto, se utiliza para hacer las dos líneas de carriles entre las que circulará el coche. Puede elegir el color que desee, pero le recomiendo que elija colores diferentes a los del entorno.
  • Bridas y barras de madera.
  • Destornillador.

Paso 2: instalar OpenCV en Raspberry Pi y configurar la pantalla remota

Instalación de OpenCV en Raspberry Pi y configuración de pantalla remota
Instalación de OpenCV en Raspberry Pi y configuración de pantalla remota

Este paso es un poco molesto y llevará algún tiempo.

OpenCV (Open source Computer Vision) es una biblioteca de software de aprendizaje automático y visión por computadora de código abierto. La biblioteca tiene más de 2500 algoritmos optimizados. Siga ESTA guía muy sencilla para instalar openCV en su raspberry pi, así como para instalar el sistema operativo raspberry pi (si aún no lo hizo). Tenga en cuenta que el proceso de construcción del openCV puede llevar alrededor de 1,5 horas en una habitación bien refrigerada (¡ya que la temperatura del procesador será muy alta!), Así que tome un poco de té y espere pacientemente: D.

Para la pantalla remota, también siga ESTA guía para configurar el acceso remoto a su raspberry pi desde su dispositivo Windows / Mac.

Paso 3: Conexión de piezas juntas

Conexión de piezas juntas
Conexión de piezas juntas
Conexión de piezas juntas
Conexión de piezas juntas
Conexión de piezas juntas
Conexión de piezas juntas

Las imágenes de arriba muestran las conexiones entre raspberry pi, módulo de cámara y controlador de motor. Tenga en cuenta que los motores que utilicé absorben 0,35 A a 9 V cada uno, lo que hace que sea seguro para el controlador del motor hacer funcionar 3 motores al mismo tiempo. Y como quiero controlar la velocidad de los 2 motores de aceleración (1 trasero y 1 delantero) exactamente de la misma manera, los conecté al mismo puerto. Monté el controlador del motor en el lado derecho del automóvil con cinta adhesiva doble. En cuanto al módulo de la cámara, inserté una brida entre los orificios de los tornillos como muestra la imagen de arriba. Luego, coloco la cámara en una barra de madera para poder ajustar la posición de la cámara como quiera. Intente instalar la cámara en el medio del automóvil tanto como sea posible. Recomiendo colocar la cámara al menos a 20 cm del suelo para mejorar el campo de visión delante del coche. El esquema de Fritzing se adjunta a continuación.

Paso 4: Primera prueba

Primer examen
Primer examen
Primer examen
Primer examen

Prueba de cámara:

Una vez que la cámara está instalada y la biblioteca openCV está construida, ¡es hora de probar nuestra primera imagen! Tomaremos una foto de pi cam y la guardaremos como "original.jpg". Se puede realizar de 2 formas:

1. Uso de comandos de terminal:

Abra una nueva ventana de terminal y escriba el siguiente comando:

raspistill -o original.jpg

Esto tomará una imagen fija y la guardará en el directorio "/pi/original.jpg".

2. Usando cualquier Python IDE (yo uso IDLE):

Abra un nuevo boceto y escriba el siguiente código:

importar cv2

video = cv2. VideoCapture (0) while True: ret, frame = video.read () frame = cv2.flip (frame, -1) # utilizado para voltear la imagen verticalmente cv2.imshow ('original', frame) cv2. imwrite ('original.jpg', frame) key = cv2.waitKey (1) if key == 27: break video.release () cv2.destroyAllWindows ()

Veamos qué pasó en este código. La primera línea es importar nuestra biblioteca openCV para usar todas sus funciones. la función VideoCapture (0) comienza a transmitir un video en vivo desde la fuente determinada por esta función, en este caso es 0, lo que significa cámara raspi. si tiene varias cámaras, se deben colocar números diferentes. video.read () leerá cada fotograma procedente de la cámara y lo guardará en una variable llamada "fotograma". La función flip () volteará la imagen con respecto al eje y (verticalmente) ya que estoy montando mi cámara a la inversa. imshow () mostrará nuestros marcos encabezados por la palabra "original" e imwrite () guardará nuestra foto como original.jpg. waitKey (1) esperará 1 ms para que se presione cualquier botón del teclado y devolverá su código ASCII. si se presiona el botón de escape (esc), se devuelve un valor decimal de 27 y se romperá el ciclo en consecuencia. video.release () detendrá la grabación y destroyAllWindows () cerrará todas las imágenes abiertas por la función imshow ().

Recomiendo probar su foto con el segundo método para familiarizarse con las funciones de openCV. La imagen se guarda en el directorio "/pi/original.jpg". La foto original que tomó mi cámara se muestra arriba.

Prueba de motores:

Este paso es fundamental para determinar el sentido de giro de cada motor. Primero, hagamos una breve introducción sobre el principio de funcionamiento de un controlador de motor. La imagen de arriba muestra el pin del controlador del motor. La habilitación A, la entrada 1 y la entrada 2 están asociadas con el control del motor A. La habilitación B, la entrada 3 y la entrada 4 están asociadas con el control del motor B. El control de dirección se establece mediante la parte "Entrada" y el control de velocidad se establece mediante la parte "Habilitar". Para controlar la dirección del motor A, por ejemplo, configure la Entrada 1 en ALTA (3.3 V en este caso, ya que estamos usando una frambuesa pi) y configure la Entrada 2 en BAJA, el motor girará en una dirección específica y configurando los valores opuestos a la Entrada 1 y la Entrada 2, el motor girará en la dirección opuesta. Si Entrada 1 = Entrada 2 = (ALTA o BAJA), el motor no gira. Los pines de habilitación toman una señal de entrada de modulación de ancho de pulso (PWM) de la frambuesa (0 a 3.3 V) y hacen funcionar los motores en consecuencia. Por ejemplo, una señal 100% PWM significa que estamos trabajando en la velocidad máxima y una señal 0% PWM significa que el motor no está girando. El siguiente código se utiliza para determinar las direcciones de los motores y probar sus velocidades.

tiempo de importación

import RPi. GPIO como GPIO GPIO.setwarnings (Falso) # Pines del motor de dirección steering_enable = 22 # Pin físico 15 in1 = 17 # Pin físico 11 in2 = 27 # Pin físico 13 # Pines de motores del acelerador throttle_enable = 25 # Pin físico 22 in3 = 23 # Pin físico 16 in4 = 24 # Pin físico 18 GPIO.setmode (GPIO. BCM) # Use la numeración GPIO en lugar de la numeración 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ón Salida GPIO (in1, GPIO. HIGH) GPIO.output (in2, GPIO. LOW) dirección = GPIO. PWM (dirección_enable, 1000) # establezca la frecuencia de conmutación en 1000 Hz dirección.stop () # Control de motores del acelerador Salida GPIO (in3, GPIO. HIGH) GPIO.output (in4, GPIO. LOW) throttle = GPIO. PWM (throttle_enable, 1000) # establece la frecuencia de conmutación en 1000 Hz throttle.stop () time.sleep (1) throttle.start (25) # arranca el motor a 25 % Señal PWM-> (0,25 * voltaje de la batería) - del conductor pérdida de dirección.start (100) # arranca el motor al 100% de la señal PWM-> (1 * voltaje de la batería) - tiempo de pérdida del conductor.sueño (3) acelerador.parada () dirección.parada ()

Este código hará funcionar los motores de aceleración y el motor de dirección durante 3 segundos y luego los detendrá. La (pérdida del conductor) se puede determinar con un voltímetro. Por ejemplo, sabemos que una señal 100% PWM debería dar el voltaje completo de la batería en el terminal del motor. Pero, al configurar PWM al 100%, descubrí que el controlador está causando una caída de 3 V y el motor está obteniendo 9 V en lugar de 12 V (¡exactamente lo que necesito!). La pérdida no es lineal, es decir, la pérdida al 100% es muy diferente de la pérdida al 25%. Después de ejecutar el código anterior, mis resultados fueron los siguientes:

Resultados de estrangulamiento: si in3 = HIGH e in4 = LOW, los motores de estrangulamiento tendrán una rotación en sentido horario (CW), es decir, el automóvil avanzará. De lo contrario, el automóvil se moverá hacia atrás.

Resultados de la dirección: si in1 = HIGH e in2 = LOW, el motor de la dirección girará al máximo a la izquierda, es decir, el automóvil se dirigirá hacia la izquierda. De lo contrario, el automóvil se dirigirá a la derecha. Después de algunos experimentos, descubrí que el motor de dirección no gira si la señal PWM no es del 100% (es decir, el motor se dirigirá completamente hacia la derecha o hacia la izquierda).

Paso 5: detección de líneas de carril y cálculo de la línea de rumbo

Detección de líneas de carril y cálculo de la línea de rumbo
Detección de líneas de carril y cálculo de la línea de rumbo
Detección de líneas de carril y cálculo de la línea de rumbo
Detección de líneas de carril y cálculo de la línea de rumbo
Detección de líneas de carril y cálculo de la línea de rumbo
Detección de líneas de carril y cálculo de la línea de rumbo

En este paso se explicará el algoritmo que controlará el movimiento del automóvil. La primera imagen muestra todo el proceso. La entrada del sistema son imágenes, la salida es theta (ángulo de dirección en grados). Tenga en cuenta que el procesamiento se realiza en 1 imagen y se repetirá en todos los fotogramas.

Cámara:

La cámara comenzará a grabar un video con resolución (320 x 240). Recomiendo reducir la resolución para que pueda obtener una mejor velocidad de fotogramas (fps), ya que se producirá una caída de fps después de aplicar técnicas de procesamiento a cada fotograma. El siguiente código será el bucle principal del programa y agregará cada paso sobre este código.

importar cv2

import numpy as np video = cv2. VideoCapture (0) video.set (cv2. CAP_PROP_FRAME_WIDTH, 320) # establece el ancho en 320 p video.set (cv2. CAP_PROP_FRAME_HEIGHT, 240) # establece la altura en 240 p # El bucle mientras Verdadero: 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 código aquí mostrará la imagen original obtenida en el paso 4 y se muestra en las imágenes de arriba.

Convertir a espacio de color HSV:

Ahora, después de tomar la grabación de video como fotogramas de la cámara, el siguiente paso es convertir cada fotograma en un espacio de color de Tono, Saturación y Valor (HSV). La principal ventaja de hacerlo es poder diferenciar los colores por su nivel de luminancia. Y aquí hay una buena explicación del espacio de color HSV. La conversión a HSV se realiza mediante la siguiente función:

def convert_to_HSV (marco):

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

Esta función se llamará desde el bucle principal y devolverá el fotograma en el espacio de color HSV. El fotograma obtenido por mí en el espacio de color HSV se muestra arriba.

Detecta bordes y colores azules:

Después de convertir la imagen en el espacio de color HSV, es hora de detectar solo el color que nos interesa (es decir, el color azul, ya que es el color de las líneas del carril). Para extraer el color azul de un marco HSV, se debe especificar un rango de matiz, saturación y valor. consulte aquí para tener una mejor idea de los valores de HSV. Después de algunos experimentos, los límites superior e inferior del color azul se muestran en el siguiente código. Y para reducir la distorsión general en cada fotograma, los bordes se detectan solo mediante un astuto detector de bordes. Aquí encontrará más información sobre Canny Edge. Una regla general es seleccionar los parámetros de la función Canny () con una proporción de 1: 2 o 1: 3.

def detect_edges (marco):

lower_blue = np.array ([90, 120, 0], dtype = "uint8") # límite inferior del color azul upper_blue = np.array ([150, 255, 255], dtype = "uint8") # límite superior de máscara de color azul = cv2.inRange (hsv, lower_blue, upper_blue) # esta máscara filtrará todo menos azul # detectar bordes bordes = cv2. Canny (máscara, 50, 100) cv2.imshow ("bordes", bordes) devolver bordes

Esta función también se llamará desde el bucle principal que toma como parámetro el marco del espacio de color HSV y devuelve el marco con bordes. El marco con bordes que obtuve se encuentra arriba.

Seleccione la región de interés (ROI):

La selección de la región de interés es crucial para enfocarse solo en una región del marco. En este caso, no quiero que el automóvil vea muchos elementos en el entorno. Solo quiero que el auto se concentre en las líneas del carril e ignore cualquier otra cosa. P. S: el sistema de coordenadas (ejes xey) comienza desde la esquina superior izquierda. En otras palabras, el punto (0, 0) comienza en la esquina superior izquierda. siendo el eje y la altura y el eje x el ancho. El siguiente código selecciona la región de interés para enfocarse solo en la mitad inferior del marco.

def region_of_interest (bordes):

altura, ancho = bordes. 4 puntos (abajo a la izquierda, arriba a la izquierda, arriba a la derecha, abajo a la derecha) polígono = np.array (

Esta función tomará el marco con bordes como parámetro y dibujará un polígono con 4 puntos preestablecidos. Solo se enfocará en lo que está dentro del polígono e ignorará todo lo que esté fuera de él. El marco de mi región de interés se muestra arriba.

Detectar segmentos de línea:

La transformación Hough se utiliza para detectar segmentos de línea de un marco con bordes. La transformada de Hough es una técnica para detectar cualquier forma en forma matemática. Puede detectar casi cualquier objeto incluso si está distorsionado de acuerdo con un cierto número de votos. aquí se muestra una gran referencia para la transformada de Hough. Para esta aplicación, la función cv2. HoughLinesP () se usa para detectar líneas en cada cuadro. Los parámetros importantes que toma esta función son:

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

  • Marco: es el marco en el que queremos detectar las líneas.
  • rho: es la precisión de la distancia en píxeles (normalmente es = 1)
  • theta: precisión angular en radianes (siempre = np.pi / 180 ~ 1 grado)
  • min_threshold: voto mínimo que debe obtener para que se considere una línea
  • minLineLength: longitud mínima de la línea en píxeles. Cualquier línea más corta que este número no se considera una línea.
  • maxLineGap: espacio máximo en píxeles entre 2 líneas que se tratarán como 1 línea. (No se usa en mi caso ya que las líneas de carril que estoy usando no tienen ningún espacio).

Esta función devuelve los puntos finales de una línea. La siguiente función es llamada desde mi bucle principal para detectar líneas usando la transformación de Hough:

def detect_line_segments (bordes_cortados):

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

Pendiente e intersección promedio (m, b):

recuerde que la ecuación de la recta viene dada por y = mx + b. Donde m es la pendiente de la recta y b es la intersección con el eje y. En esta parte, se calculará el promedio de pendientes e intersecciones de segmentos de línea detectados usando la transformada de Hough. Antes de hacerlo, echemos un vistazo a la foto del marco original que se muestra arriba. El carril izquierdo parece ir hacia arriba, por lo que tiene una pendiente negativa (¿recuerda el punto de inicio del sistema de coordenadas?). En otras palabras, la línea del carril izquierdo tiene x1 <x2 e y2 x1 e y2> y1, lo que dará una pendiente positiva. Entonces, todas las líneas con pendiente positiva se consideran puntos del carril derecho. En el caso de líneas verticales (x1 = x2), la pendiente será infinita. En este caso, omitiremos todas las líneas verticales para evitar errores. Para agregar más precisión a esta detección, cada cuadro se divide en dos regiones (derecha e izquierda) a través de 2 líneas de límite. Todos los puntos de ancho (puntos del eje x) mayores que la línea de límite derecha están asociados con el cálculo del carril derecho. Y si todos los puntos de ancho son menores que la línea de límite izquierda, están asociados con el cálculo del carril izquierdo. La siguiente función toma la trama en proceso y los segmentos de carril detectados mediante la transformada de Hough y devuelve la pendiente media y la intersección de dos líneas de carril.

def media_interceptación_de_línea (marco, segmentos_de_línea):

lane_lines = si line_segments es None: print ("ningún segmento de línea detectado") return lane_lines height, width, _ = frame.shape left_fit = right_fit = boundary = left_region_boundary = width * (1 - boundary) right_region_boundary = ancho * límite para segmento_de_línea en segmentos_de_línea: para x1, y1, x2, y2 en segmento_de_línea: si x1 == x2: print ("saltando líneas verticales (pendiente = infinito)") continuar ajuste = np.polyfit ((x1, x2), (y1, y2), 1) pendiente = (y2 - y1) / (x2 - x1) intersección = y1 - (pendiente * x1) if pendiente <0: si x1 <left_region_boundary y x2 right_region_boundary y x2> right_region_boundary: right_fit. append ((pendiente, intersección)) 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 es una matriz 2-D que consta de las coordenadas de las líneas de carril derecha e izquierda # por ejemplo: lan e_lines =

make_points () es una función auxiliar para la función average_slope_intercept () que devolverá las coordenadas limitadas de las líneas de carril (desde la parte inferior hasta la mitad del marco).

def make_points (marco, línea):

altura, ancho, _ = marco.forma pendiente, intersección = línea y1 = altura # parte inferior del marco y2 = int (y1 / 2) # hacer puntos desde el medio del marco hacia abajo si pendiente == 0: pendiente = 0.1 x1 = int ((y1 - intersección) / pendiente) x2 = int ((y2 - intersección) / pendiente) return

Para evitar dividir por 0, se presenta una condición. Si pendiente = 0, lo que significa y1 = y2 (línea horizontal), dé a la pendiente un valor cercano a 0. Esto no afectará el rendimiento del algoritmo y evitará un caso imposible (dividir por 0).

Para mostrar las líneas de carril en los marcos, se utiliza la siguiente función:

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

line_image = np.zeros_like (frame) si líneas no es None: para línea en líneas: para x1, y1, x2, y2 en línea: 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 función cv2.addWeighted () toma los siguientes parámetros y se usa para combinar dos imágenes pero dando a cada una un peso.

cv2.addWeighted (imagen1, alfa, imagen2, beta, gamma)

Y calcula la imagen de salida usando la siguiente ecuación:

salida = alfa * imagen1 + beta * imagen2 + gamma

Aquí se obtiene más información sobre la función cv2.addWeighted ().

Calcular y mostrar la línea de rumbo:

Este es el paso final antes de aplicar velocidades a nuestros motores. La línea de rumbo es responsable de dar al motor de dirección la dirección en la que debe girar y dar a los motores de aceleración la velocidad a la que operarán. El cálculo de la línea de rumbo es pura trigonometría, se utilizan funciones trigonométricas tan y atan (tan ^ -1). Algunos casos extremos son cuando la cámara detecta solo una línea de carril o cuando no detecta ninguna línea. Todos estos casos se muestran en la siguiente función:

def get_steering_angle (frame, lane_lines):

height, width, _ = frame.shape if len (lane_lines) == 2: # si se detectan dos líneas de carril _, _, left_x2, _ = lane_lines [0] [0] # extraer left x2 from lane_lines array _, _, right_x2, _ = lane_lines [1] [0] # extraer derecha x2 de 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 solo se detecta una línea x1, _, x2, _ = lane_lines [0] [0] x_offset = x2 - x1 y_offset = int (altura / 2) elif len (lane_lines) == 0: # si no se detecta ninguna línea 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) ángulo de dirección = ángulo_a_de_medio + 90 return ángulo de dirección

x_offset en el primer caso es cuánto difiere el promedio ((derecha x2 + izquierda x2) / 2) del centro de la pantalla. y_offset siempre se toma como altura / 2. La última imagen de arriba muestra un ejemplo de línea de encabezado. angle_to_mid_radians es lo mismo que "theta" que se muestra en la última imagen de arriba. Si direction_angle = 90, significa que el automóvil tiene una línea de rumbo perpendicular a la línea "altura / 2" y el automóvil avanzará sin dirección. Si direction_angle> 90, el automóvil debe girar a la derecha; de lo contrario, debe girar a la izquierda. Para mostrar la línea de rumbo, se utiliza la siguiente función:

def display_heading_line (marco, ángulo de dirección, color_línea = (0, 0, 255), ancho_línea = 5)

header_image = np.zeros_like (frame) height, width, _ = frame.shape direction_angle_radian = direction_angle / 180.0 * math.pi x1 = int (width / 2) y1 = height x2 = int (x1 - height / 2 / math.tan (direction_angle_radian)) y2 = int (altura / 2) cv2.line (cabecera_imagen, (x1, y1), (x2, y2), line_color, line_width) cabecera_imagen = cv2.addWeighted (marco, 0.8, cabecera_imagen, 1, 1) return header_image

La función anterior toma el marco en el que se dibujará la línea de rumbo y el ángulo de dirección como entrada. Devuelve la imagen de la línea de encabezado. El marco de la línea de encabezado tomado en mi caso se muestra en la imagen de arriba.

Combinando todo el código en conjunto:

El código ahora está listo para ensamblarse. El siguiente código muestra el bucle principal del programa que llama a cada función:

importar cv2

importar numpy como 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) #Calling the functions hsv = convert_to_HSV (frame) frames = detect_edges (hsv) roi = region_of_interest (bordes) line_segments = detect_line_segments (roi) lane_lines = average_slope_intercept (frame, line_segments) lane_lines_image_frame_lines (líneas de dirección) = get_steering_angle (frame, lane_lines) header_image = display_heading_line (lane_lines_image, direction_angle) key = cv2.waitKey (1) if key == 27: break video.release () cv2.destroyAllWindows ()

Paso 6: Aplicación del control de DP

Aplicación de control de DP
Aplicación de control de DP

Ahora tenemos nuestro ángulo de dirección listo para alimentar a los motores. Como se mencionó anteriormente, si el ángulo de dirección es mayor de 90, el automóvil debe girar a la derecha; de lo contrario, debe girar a la izquierda. Apliqué un código simple que gira el motor de dirección a la derecha si el ángulo es superior a 90 y lo gira a la izquierda si el ángulo de dirección es inferior a 90 a una velocidad de aceleración constante de (10% PWM) pero obtuve muchos errores. El principal error que obtuve es que cuando el automóvil se acerca a cualquier giro, el motor de dirección actúa directamente pero los motores de aceleración se atascan. Traté de aumentar la velocidad de aceleración a (20% PWM) en las curvas, pero terminé con el robot saliendo de los carriles. Necesitaba algo que aumentara mucho la velocidad de aceleración si el ángulo de dirección es muy grande y aumenta un poco la velocidad si el ángulo de dirección no es tan grande, luego disminuye la velocidad a un valor inicial a medida que el automóvil se acerca a 90 grados (en línea recta). La solución fue utilizar un controlador PD.

El controlador PID significa controlador proporcional, integral y derivativo. Este tipo de controladores lineales se usa ampliamente en aplicaciones de robótica. La imagen de arriba muestra el bucle de control de retroalimentación PID típico. El objetivo de este controlador es alcanzar el "setpoint" de la manera más eficiente a diferencia de los controladores "on-off" que encienden o apagan la planta según algunas condiciones. Se deben conocer algunas palabras clave:

  • Punto de ajuste: es el valor deseado que desea que alcance su sistema.
  • Valor real: es el valor real detectado por el sensor.
  • Error: es la diferencia entre el punto de ajuste y el valor real (error = Punto de ajuste - Valor real).
  • Variable controlada: desde su nombre, la variable que desea controlar.
  • Kp: constante proporcional.
  • Ki: constante integral.
  • Kd: constante derivada.

En resumen, el bucle del sistema de control PID funciona de la siguiente manera:

  • El usuario define el punto de ajuste necesario para que alcance el sistema.
  • El error se calcula (error = setpoint - real).
  • El controlador P genera una acción proporcional al valor del error. (el error aumenta, la acción P también aumenta)
  • El controlador integrará el error a lo largo del tiempo, lo que elimina el error de estado estable del sistema pero aumenta su sobreimpulso.
  • El controlador D es simplemente la derivada del error en el tiempo. En otras palabras, es la pendiente del error. Realiza una acción proporcional a la derivada del error. Este controlador aumenta la estabilidad del sistema.
  • La salida del controlador será la suma de los tres controladores. La salida del controlador se convertirá en 0 si el error se convierte en 0.

Puede encontrar una gran explicación del controlador PID aquí.

Volviendo al auto de mantenimiento de carril, mi variable controlada era la velocidad de aceleración (ya que la dirección tiene solo dos estados, ya sea a la derecha o a la izquierda). Se utiliza un controlador PD para este propósito, ya que la acción D aumenta mucho la velocidad de aceleración si el cambio de error es muy grande (es decir, una gran desviación) y ralentiza el automóvil si este cambio de error se acerca a 0. Hice los siguientes pasos para implementar un PD controlador:

  • Establezca el punto de ajuste en 90 grados (siempre quiero que el automóvil se mueva en línea recta)
  • Calculó el ángulo de desviación desde el medio
  • La desviación proporciona dos datos: qué tan grande es el error (magnitud de la desviación) y qué dirección debe tomar el motor de dirección (signo de desviación). Si la desviación es positiva, el automóvil debe girar a la derecha; de lo contrario, debe girar a la izquierda.
  • Dado que la desviación es negativa o positiva, se define una variable de "error" y siempre es igual al valor absoluto de la desviación.
  • El error se multiplica por una constante Kp.
  • El error sufre diferenciación temporal y se multiplica por una constante Kd.
  • La velocidad de los motores se actualiza y el bucle comienza de nuevo.

El siguiente código se utiliza en el bucle principal para controlar la velocidad de los motores de estrangulamiento:

velocidad = 10 # velocidad de funcionamiento en% PWM

#Variables que se actualizarán en cada bucle lastTime = 0 lastError = 0 # PD constantes Kp = 0.4 Kd = Kp * 0.65 Mientras que es verdadero: ahora = tiempo.tiempo () # variable de tiempo actual dt = ahora - último Desviación de tiempo = ángulo de dirección - 90 # equivalente a angle_to_mid_deg variable error = abs (desviación) si la desviación -5: # no gire si hay una desviación del rango de error de 10 grados = 0 error = 0 GPIO.output (in1, GPIO. LOW) GPIO.output (in2, GPIO. LOW) dirección.parada () elif desviación> 5: # gire a la derecha si la desviación es positiva GPIO.output (in1, GPIO. LOW) GPIO.output (in2, GPIO. HIGH) steering.start (100) elif desviación < -5: # girar a la izquierda si la desviación es negativa GPIO.output (in1, GPIO. HIGH) GPIO.output (in2, GPIO. LOW) steering.start (100) derivative = kd * (error - lastError) / dt proporcional = kp * error PD = int (velocidad + derivada + proporcional) spd = abs (PD) si spd> 25: spd = 25 throttle.start (spd) lastError = error lastTime = time.time ()

Si el error es muy grande (la desviación del medio es alta), las acciones proporcionales y derivadas son altas, lo que da como resultado una alta velocidad de estrangulamiento. Cuando el error se acerca a 0 (la desviación del medio es baja), la acción derivada actúa a la inversa (la pendiente es negativa) y la velocidad de estrangulamiento se reduce para mantener la estabilidad del sistema. El código completo se adjunta a continuación.

Paso 7: resultados

Los videos de arriba muestran los resultados que obtuve. Necesita más ajustes y ajustes. Estaba conectando la raspberry pi a mi pantalla LCD porque la transmisión de video a través de mi red tenía una latencia alta y era muy frustrante trabajar con ella, por eso hay cables conectados a la raspberry pi en el video. Usé tablas de espuma para dibujar la pista.

¡Estoy esperando escuchar tus recomendaciones para mejorar este proyecto! Como espero que estos instructivos hayan sido lo suficientemente buenos para brindarles información nueva.

Recomendado: