• Coche RC autónomo (VIII) - Usando threads para enviar y recibir datos del sensor ultrasónico e imágenes desde la Raspberry Pi

    Como ya expliqué en el post Coche RC autónomo (VII) - Configurando el sensor ultrasónico HC-SR04 para detectar objetos, el siguiente paso era enviar esa información desde nuestro cliente (Raspberry Pi) a nuestro servidor, para luego poder procesar esos datos de distancia a objetos junto con las imágenes de las videocámaras (que ya éramos capaces de enviar, como se explicó en el post Coche RC autónomo (VI) - Probando la cámara usando Python, OpenCV y streaming).

    Imágenes de la videocámara y datos del sensor ultrasónico

    La idea es, por tanto, actualizar nuestros scripts Python 3 server.py (a ejecutar en nuestro ordenador durante el desarrollo, y luego exclusivamente desde la Raspberry Pi 3 cuando esté montada en el vehículo teledirigido) y client.py (a ejecutar en la Raspberry Pi 3 siempre). A tener en cuenta:

    • Vamos a crear en el cliente un thread (hilo de ejecución) que lea datos del sensor ultrasónico con la distancia a objetos enfrente de dicho sensor y la envíe al servidor por un puerto definido (el 8001 en nuestro caso), y otro thread que obtenga imágenes de la videocámara y las envíe mediante streaming a otro puerto definido (el 8000)
    • En el servidor crearemos igualmente dos threads, uno para gestionar las imágenes recibidas de la cámara y otro para obtener los datos de distancia obtenidos del sensor ultrasónico.

    En el servidor, el thread principal será el que procese imágenes de la videocámara, ya que desde él podremos leer los datos recibidos del sensor ultrasónico y podremos mostrar mensajes en la propia imagen que visualicemos. Desde este thread, como veremos en futuros posts, iremos llamando a diferentes funciones para controlar el vehículo (redes neuronales que procesen las imágenes, y lógica de si el coche debe girar, frenar, acelerar, etcétera)

    En el ejemplo detallado en este post, a la vez que recibimos dichos datos (imágenes y distancia a obstáculos) vamos a ir preparando esta lógica: Vamos a mostrar en la imagen si tenemos obstáculo delante o no, escribiendo un texto.

    El código del cliente (Raspberry Pi 3) es el siguiente (client.py). Como hemos comentado, la configuración del los circuitos y código del sensor de ultrasonidos y videocámara los puedes encontrar en los correspondientes posts, mientras que el código lo puedes encontrar actualizado en Github - jorgecasas/autonomous-rc-car:

    # Importamos librerias RPi.GPIO (entradas/salidas GPIO de Raspberry Pi) y time (para sleeps, etc...)
    # Requiere previamente instalarla (pip install RPi.GPIO)
    import RPi.GPIO as GPIO
    import time
    import io
    import socket
    import struct
    import picamera
    import threading
    
    # Configure Raspberry Pi GPIO in BCM mode
    GPIO.setmode(GPIO.BCM) 
    
    
    # Config vars. These IP and ports must be available in server firewall
    log_enabled = False
    server_ip = '192.168.1.235'
    server_port_ultrasonic = 8001
    server_port_camera = 8000
    
    # Camera configuration
    image_width = 640
    image_height = 480
    image_fps = 10
    recording_time = 600
    
    # Definition of GPIO pins in Raspberry Pi 3 (GPIO pins schema needed!)
    #   18 - Trigger (output)
    #   24 - Echo (input)
    GPIO_ultrasonic_trigger = 18
    GPIO_ultrasonic_echo = 24
    
    # Class to handle the ultrasonic sensor stream in client
    class StreamClientUltrasonic():
    
        def measure(self):
            # Measure distance from ultrasonic sensor. Send a trigger pulse
            GPIO.output( GPIO_ultrasonic_trigger, True )
            time.sleep( 0.00001 )
            GPIO.output( GPIO_ultrasonic_trigger, False )
            
            # Get start time
            start = time.time()
    
            # Wait to receive any ultrasound in sensor (echo)
            while GPIO.input( GPIO_ultrasonic_echo ) == 0:
                start = time.time()
    
            # We have received the echo. Wait for its end, getting stop time
            while GPIO.input( GPIO_ultrasonic_echo ) == 1:
                stop = time.time()
    
            # Calculate time difference. Sound has gone from trigger to object and come back to sensor, so 
            # we have to divide between 2. Formula: Distance = ( Time elapsed * Sound Speed ) / 2 
            time_elapsed = stop-start
            distance = (time_elapsed * 34300) / 2
    
            return distance
    
      
        def __init__(self):
    
            # Connect a client socket to server_ip:server_port_ultrasonic
            print( '+ Trying to connect to ultrasonic streaming server in ' + str( server_ip ) + ':' + str( server_port_ultrasonic ) );
    
            # Configure GPIO pins (trigger as output, echo as input)
            GPIO.setup( GPIO_ultrasonic_trigger, GPIO.OUT )
            GPIO.setup( GPIO_ultrasonic_echo, GPIO.IN )
    
            # Set output GPIO pins to False
            GPIO.output( GPIO_ultrasonic_trigger, False ) 
    
            # Create socket and bind host
            client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            client_socket.connect( ( server_ip, server_port_ultrasonic ) )
    
            try:
                while True:
                    # Measure and send data to the host every 0.5 sec, 
                    # pausing for a while to no lock Raspberry Pi processors
                    distance = self.measure()
                    if log_enabled: print( "Ultrasonic sensor distance: %.1f cm" % distance )
                    client_socket.send( str( distance ).encode('utf-8') )
                    time.sleep( 0.5 )
    
            finally:
                # Ctrl + C to exit app (cleaning GPIO pins and closing socket connection)
                print( 'Ultrasonic sensor connection finished!' );
                client_socket.close()
                GPIO.cleanup()
    
    
    # Class to handle the jpeg video stream in client
    class StreamClientVideocamera():
      
        def __init__(self):
    
            # Connect a client socket to server_ip:server_port_camera
            print( '+ Trying to connect to videocamera streaming server in ' + str( server_ip ) + ':' + str( server_port_camera ) );
    
            # create socket and bind host
            client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            client_socket.connect((server_ip, server_port_camera))
            connection = client_socket.makefile('wb')
    
            try:
                with picamera.PiCamera() as camera:
                    camera.resolution = (image_width, image_height)
                    camera.framerate = image_fps
    
                    # Give 2 secs for camera to initilize
                    time.sleep(2)                       
                    start = time.time()
                    stream = io.BytesIO()
                    
                    # send jpeg format video stream
                    for foo in camera.capture_continuous(stream, 'jpeg', use_video_port = True):
                        connection.write(struct.pack('<L', stream.tell()))
                        connection.flush()
                        stream.seek(0)
                        connection.write(stream.read())
                        if time.time() - start > recording_time:
                            break
                        stream.seek(0)
                        stream.truncate()
                connection.write(struct.pack('<L', 0))
    
            finally:
                connection.close()
                client_socket.close()
                print( 'Videocamera stream connection finished!' );
    
     
    
    # Class to handle the different threads in client 
    class ThreadClient():
    
        # Client thread to handle the video
        def client_thread_camera(host, port):
            print( '+ Starting videocamera stream client connection to ' + str( host ) + ':' + str( port ) )
            StreamClientVideocamera()
    
        # Client thread to handle ultrasonic distances to objects
        def client_thread_ultrasonic(host, port):
            print( '+ Starting ultrasonic stream client connection to ' + str( host ) + ':' + str( port ) )
            StreamClientUltrasonic()
    
        print( '+ Starting client - Logs ' + ( log_enabled and 'enabled' or 'disabled'  ) )
        thread_ultrasonic = threading.Thread( name = 'thread_ultrasonic', target = client_thread_ultrasonic, args = ( server_ip, server_port_ultrasonic ) )
        thread_ultrasonic.start()
        
        thread_videocamera = threading.Thread( name = 'thread_videocamera', target = client_thread_camera, args = ( server_ip, server_port_camera ) )
        thread_videocamera.start()
    
    
    # Starting thread client handler
    if __name__ == '__main__':
        ThreadClient()
    

    El código del servidor es el siguiente (server.py):

    # Import libraries
    import threading
    import socketserver
    import socket
    import cv2
    import numpy as np
    import math
    
    # Config vars
    log_enabled = False
    server_ip = '192.168.1.235'
    server_port_camera = 8000
    server_port_ultrasonic = 8001
    
    # Video configuration
    image_gray_enabled = False
    image_fps = 24 
    image_width = 640
    image_height = 480
    image_height_half = int( image_height / 2 )
    
    # Global var (ultrasonic_data) to measure object distances (distance in cm)
    ultrasonic_sensor_distance = ' '
    ultrasonic_stop_distance = 25
    ultrasonic_text_position = ( 16, 16 )
    
    # Other vars
    color_blue = (211, 47, 47)
    color_yellow = (255, 238, 88)
    color_red = (48, 79, 254)
    color_green = (0, 168, 0)
    
    # Font used in opencv images
    image_font = cv2.FONT_HERSHEY_PLAIN
    image_font_size = 1.0
    image_font_stroke = 2
    
    
    # Datos para lineas de control visual. Array stroke_lines contiene 3 componentes:
    #   0: Punto inicial (x,y)
    #   1: Punto final (x,y)
    #   2: Color de linea
    #   3: Ancho de linea en px
    # Para activarlo/desactivarlo: stroke_enable = True|False
    stroke_enabled = True
    stroke_width = 4
    stroke_lines = [
       [ (0,image_height), ( int( image_width * 0.25 ), int( image_height/2 ) ), color_green, stroke_width ],
       [ (image_width,image_height), ( int( image_width * 0.75 ), int( image_height/2 ) ), color_green, stroke_width ]
    ];
    
    
    # Class to handle data obtained from ultrasonic sensor
    class StreamHandlerUltrasonic(socketserver.BaseRequestHandler):
    
        data = ' '
    
        def handle(self):
            global ultrasonic_sensor_distance
            distance_float = 0.0
    
            try:
                print( 'Ultrasonic sensor measure: Receiving data in server!' )
                while self.data:
                    self.data = self.request.recv(1024)
                    try:
                        distance_float = float( self.data )
                    except ValueError: 
                        # No es float... porque hemos recibido algo del tipo b'123.123456.456' (es decir, por lag de la red
                        # o sobrecarga de nuestro sistema hemos recibido dos valores antes de ser capaces de procesarlo)
                        distance_float = 1000.0
                    
                    ultrasonic_sensor_distance = round( distance_float, 1)
                    if log_enabled: print( 'Ultrasonic sensor measure received: ' + str( ultrasonic_sensor_distance ) + ' cm' )
     
            finally:
                print( 'Connection closed on ultrasonic thread' )
    
    
    # Class to handle the jpeg video stream received from client
    class StreamHandlerVideocamera(socketserver.StreamRequestHandler):
      
        def handle(self):
            stream_bytes = b' '
            global ultrasonic_sensor_distance
    
            # stream video frames one by one
            try:
                print( 'Videocamera: Receiving images in server!' )
                while True:
                    stream_bytes += self.rfile.read(1024)
    
                    first = stream_bytes.find(b'\xff\xd8')
                    last = stream_bytes.find(b'\xff\xd9')
                    if first != -1 and last != -1:
                        jpg = stream_bytes[first:last+2]
                        stream_bytes = stream_bytes[last+2:]
                        image = cv2.imdecode(np.fromstring(jpg, dtype=np.uint8), cv2.IMREAD_UNCHANGED)
    
                        # lower half of the image
                        if image_gray_enabled:
                            gray = cv2.imdecode(np.fromstring(jpg, dtype=np.uint8), cv2.IMREAD_GRAYSCALE)
                            half_gray = gray[image_height_half:image_height, :]
    
                        # Dibujamos lineas "control"
                        if stroke_enabled:
                            for stroke in stroke_lines:
                                cv2.line( image, stroke[0], stroke[1], stroke[2], stroke[3])
    
    
                        # Check ultrasonic sensor data (distance to objects in front of the car)
                        if ultrasonic_sensor_distance is not None and ultrasonic_sensor_distance < ultrasonic_stop_distance:
                            cv2.putText( image, 'OBSTACLE ' + str( ultrasonic_sensor_distance ) + 'cm', ultrasonic_text_position, image_font, image_font_size, color_red, image_font_stroke, cv2.LINE_AA)
                            if log_enabled: print( 'Stop, obstacle in front! >> Measure: ' + str( ultrasonic_sensor_distance ) + 'cm - Limit: '+ str(ultrasonic_stop_distance ) + 'cm' )
                        else:
                            cv2.putText( image, 'NO OBSTACLE ' + str( ultrasonic_sensor_distance ) + 'cm', ultrasonic_text_position, image_font, image_font_size, color_green, image_font_stroke, cv2.LINE_AA)
    
                        # Show images
                        cv2.imshow('image', image)
                        if image_gray_enabled:
                            cv2.imshow('mlp_image', half_gray)
        
                        if cv2.waitKey(1) & 0xFF == ord('q'):
                            break
    
            finally:
                cv2.destroyAllWindows()
                print( 'Connection closed on videostream thread' )
    
    
    # Class to handle the different threads 
    class ThreadServer( object ):
    
        # Server thread to handle the video
        def server_thread_camera(host, port):
            print( '+ Starting videocamera stream server in ' + str( host ) + ':' + str( port ) )
            server = socketserver.TCPServer((host, port), StreamHandlerVideocamera)
            server.serve_forever()
    
        # Server thread to handle ultrasonic distances to objects
        def server_thread_ultrasonic(host, port):
            print( '+ Starting ultrasonic stream server in ' + str( host ) + ':' + str( port ) )
            server = socketserver.TCPServer((host, port), StreamHandlerUltrasonic)
            server.serve_forever()
    
        print( '+ Starting server - Logs ' + ( log_enabled and 'enabled' or 'disabled'  ) )
        thread_ultrasonic = threading.Thread( name = 'thread_ultrasonic', target = server_thread_ultrasonic, args = ( server_ip, server_port_ultrasonic ) )
        thread_ultrasonic.start()
        
        thread_videocamera = threading.Thread( name = 'thread_videocamera', target = server_thread_camera, args = ( server_ip, server_port_camera ) )
        thread_videocamera.start()
    
    
    
    # Starting thread server handler
    if __name__ == '__main__':
        ThreadServer()
    
    

    En ambos scripts he ido incluendo variables para facilitar la configuración usada, como puede ser la activación / desactivación de logs, dimensiones de imágenes, etcétera, por lo que es recomendable revisar dicha configuración cuando estéis haciendo pruebas. Ahora sólo queda probarlo, arrancando primero el servidor y luego el cliente (accediendo previamente al entorno virtualizado python mediante workon cv):

    • En el servidor: python server.py
    • En el cliente: python client.py

    El resultado será similar al de la imagen que acompaña este post. En próximos capítulos empezaremos a detectar señales de STOP mediante redes neuronales. ¡Espero que os guste!

    Más información sobre el proyecto

  • Coche RC autónomo (VII) - Configurando el sensor ultrasónico HC-SR04 para detectar objetos

    Además de utilizar la cámara, los coches autónomos tendrán muchos otros sensores que obtendrán datos que podrán ser utilizados para garantizar la seguridad e integridad de los vehículos y todo lo que les rodea. Así, en este post voy a detallar cómo utilizar el sensor de ultrasonidos HC-SR04 para detectar obstáculos delante del vehículo.

    El mismo script y circuito que vamos a utilizar puede servir como sensor de aparcamiento, ya que nos permitirá detectar a qué distancia nos encontramos de un obstáculo o pared. También puede ser utilizado para medir cuán lleno está un pozo o un bidón de almacenamiento de líquidos.

    Sensor HC-SR04

    El mecanismo de este sensor es el siguiente:

    • Emite un pulso de sonido durante un pequeño período de tiempo. El pulso lo emitimos de forma periódica, y es como un disparo (trigger).
    • El sonido avanza hasta chocar con un objeto y rebota.
    • El sonido rebotado es el eco (echo), que se recibirá y detectará en el sensor receptor.
    • Conociendo la velocidad del sonido en el aire y calculando el tiempo transcurrido desde que se lanzó el pulso sónico y se recibe su eco, podemos conocer la distancia (teniendo en cuenta que el sonido habrá ido y habrá vuelto, por lo que tendremos que dividir la distancia entre 2).

    Así, en nuestro diseño definiremos una distancia límite. Si el vehículo se encuentra más cerca de un obstáculo que dicha distancia, se detendrá para no chocar.

    En las pruebas que hemos realizado (ver el script a continuación, que puede ser descargado de Código en Github - jorgecasas/autonomous-rc-car), vamos a encender un led cuando estemos a menos distancia y mostraremos un mensaje de alerta. De esta forma, y con un pequeño circuito, aprenderemos también a utilizar las entradas y salidas GPIO de la Raspberry Pi.

    En primer lugar, accedemos a la Raspberry Pi y accedemos al entorno virtual de Python, instalando con pip la librería requerida para poder gestionar las entradas y salidas GPIO:

    workon cv
    pip install RPi.GPIO
    

    El script sensor-ultrasonidos-HC-SR04.py es el siguiente:

    # Importamos librerias RPi.GPIO (entradas/salidas GPIO de Raspberry Pi) y time (para sleeps, etc...)
    # Reqiuere previamente instalarla (pip install RPi.GPIO)
    import RPi.GPIO as GPIO
    import time
    
    # Ponemos la placa en modo BCM
    GPIO.setmode(GPIO.BCM) 
    
    # Definimos los pines GPIO de la Raspberry Pi 3 (segun esquema)
    #   12 - Led (output)
    #   18 - Trigger (output)
    #   24 - Echo (input)
    GPIO_LED = 12
    GPIO_TRIGGER = 18
    GPIO_ECHO = 24
    
    # Configuramos los pines como salidas (trigger y led) o entradas (detector de eco)
    GPIO.setup(GPIO_TRIGGER,GPIO.OUT) #Configuramos Trigger como salida
    GPIO.setup(GPIO_ECHO,GPIO.IN) #Configuramos Echo como entrada
    GPIO.setup( GPIO_LED, GPIO.OUT ) # Pin de led
    
    # Inicializamos los pines de salida (apagados)
    GPIO.output( GPIO_TRIGGER, False ) 
    GPIO.output( GPIO_LED, False )
    
    # Distancia en cm
    distance_limit = 15
    
    try:
        print( 'Sensor ultrasonico HC-SR04' )
    
        # Iniciamos un loop infinito
        while True: 
    
            # Enviamos un pulso de ultrasonidos durante un poco de tiempo
            GPIO.output(GPIO_TRIGGER,True)
            time.sleep(0.00001)
            GPIO.output(GPIO_TRIGGER,False)
            
            # Desde que dejamos de enviar el pulso, obtenemos tiempo actual
            start = time.time()
    
            # Si el sensor no recibe sonido, mantenemos el tipo actual actualizado
            while GPIO.input(GPIO_ECHO)==0:
                start = time.time()
    
            # Si el sensor recibe sonido, obtenemos el tiempo fin
            while GPIO.input(GPIO_ECHO)==1:
                stop = time.time()
    
            # Obtenemos el tiempo transcurrido. 
            # La distancia sera igual al tiempo transcurrido por la velocidad (partido por 2, porque 
            # el sonido va y vuelve desde el sensor al objeto y del objeto al sensor): 2 D = (T x V)/2
            elapsed = stop-start
            distance = (elapsed * 34300) / 2
            
            # Si la distancia es menor que la distancia limite fijada... Paramos y encendemos led de alerta!
            if distance < distance_limit:
                print( 'Stop! Objeto a ' + str( distance ) +'cm' )
                GPIO.output( GPIO_LED, True )
            else:
                # En este caso, podemos continuar avanzando sin obstaculos!
                print( 'Go! No hay objetos delante' )
                GPIO.output( GPIO_LED, False )
    
            # Pausamos un poco para no saturar el procesador de la Raspberry Pi
            time.sleep( 0.25 )
    
    except KeyboardInterrupt: 
        # Si el usuario pulsa CONTROL+C... fin de aplicacion (limpieando los pines GPIO)
        print( 'Sensor Stopped' )
        GPIO.cleanup()
    
    

    Siguiendo el script, tenemos que crear un circuito teniendo en cuenta la distribución de los pines GPIO de la Raspberry 3:

    Raspberry Pi 3 - GPIO Schema

    • Conectaremos pin VCC+ al pin VCC y el pin GROUND al pin GROUND del sensor HC-SR04, para alimentarlo.
    • Conectaremos el pin de salida GPIO 18 de la Raspberry Pi directamente al pin TRIGGER del sensor HC-SR04
    • Conectaremos el pin de entrada GPIO 24 de la Raspberry Pi al pin ECHO del sensor HC-SR04, pero no lo haremos directamente sino mediante una resistencia de 1K.

    Adicionalmente, conectaremos un led que encenderemos cuando el obstáculo se encuentre más cerca de la distancia límite:

    • Conectaremos el pin de salida GPIO 12 de la Raspberry Pi directamente al pin positivo del led
    • Conectaremos una resistencia de 1K del otro pin del led al pin GROUND de la Raspberry Pi

    Y sólo queda probarlo ejecutando el siguiente comando. El led deberá encenderse y apagarse según acerquemos objetos al sensor.

    python sensor-ultrasonidos-HC-SR04.py
    

    Sensor HC-SR04

    Este código lo adaptaremos en los próximos pasos dentro de nuestro script global, encapsulándolo en un hilo independiente, de manera que podamos utilizar la información obtenida a la hora de controlar el vehículo.

    Más información sobre el proyecto

  • Coche RC autónomo (VI) - Probando la cámara usando Python, OpenCV y streaming

    Llegados a este punto ya tenemos OpenCV instalado (tanto en la Raspberry Pi como en el ordenador para hacer pruebas de forma más sencilla). Con esta librería podremos usar la cámara y Python para la visión por computador. Así, en este post vamos a comentar nuestros primeros scripts en Python para conseguir capturar imágenes de la cámara de la Raspberry Pi usando la librería picamera y OpenCV para procesarlos. En el post Coche RC autónomo (IV) - Configurando la videocámara en la Raspberry Pi ya habíamos conseguido enviar imágenes desde el cliente (Raspberry Pi) al servidor (nuestro ordenador), pero usábamos netcat para ello, y nosotros necesitaremos obtener objetos de tipo imagen para poder ser procesadas mediante OpenCV.

    Python en la Raspberry Pi

    Para ello, igual que hicimos en el post anterior, vamos a enviar imágenes entre la Raspberry Pi (IP en el interfaz WiFi: 192.168.1.199) al servidor (nuestro ordenador, con IP 192.168.1.235, con el puerto 8000 abierto en el cortafuegos). En este post vamos a crear los dos primeros scripts python, los cuales nos servirán de base para ir programando más adelante todo lo necesario para que el vehículo autónomo pueda ver y procesar lo que ve. Los scripts y todo el código que vaya a utilizar los podés descargar de Github - jorgecasas/autonomous-rc-car.

    Ejecutando scripts Python

    Para ejecutar los scripts (tanto en la Raspberry Pi como en el ordenador, donde tenemos instalado las librerías OpenCV y los entornos virtualizados de Python), basta con ejecutar los comandos:

    • Acceder al entorno virtualizado:
    workon cv
    
    • Ejecutar el script Python:
    python script_python.py
    

    Utilizando git para hacer el despliegue de código

    Para copiar los ficheros tanto a la Raspberry Pi como a nuestro ordenador, puede ser interesante crear un repositorio (o un branch de mi repositorio de Github - jorgecasas/autonomous-rc-car) tanto en la Raspberry Pi como en nuestro ordenador, de manera que podamos compartir nuestro código y actualizarlo fácilmente en los diferentes dispositivos. Tendremos que instalar git:

    sudo apt-get install git
    

    Ahora clonamos el repositorio (tanto en la Raspberry Pi como en nuestro ordenador), lo que creará una carpeta autonomous-rc-car. En este caso necesitaremos tener antes una cuenta de Github, que es gratuita:

    git clone git@github.com:jorgecasas/autonomous-rc-car.git
    cd autonomous-rc-car
    

    Podemos crear ahora una nueva rama (branch) para nuestros desarrollos. En el ejemplo esta rama de desarrollo se llamará branch-de-desarrollo (le puedes llamar como quieras):

    git checkout -b branch-de-desarrollo
    

    Podemos actualizar el código y crear nuevos ficheros. Los podremos añadir y hacer commit:

    git add nuevo_fichero.py
    git commit -m "Nuevo fichero creado"
    

    Por último, este nuevo fichero lo podemos subir a Github para tener una copia de seguridad y poder compartir nuestro código:

    git push origin branch-de-desarrollo
    

    Y ahora que está el código subido a Github, lo podemos descargar en la Raspberry Pi o en otro ordenador. Para ello, una vez conectados a la Raspberry Pi por SSH, necesitamos clonar el proyecto (igual que hemos hecho arriba), y acceder a nuestro branch de desarrollo:

    git clone git@github.com:jorgecasas/autonomous-rc-car.git
    cd autonomous-rc-car
    git checkout branch-de-desarrollo
    

    Y una vez en nuestra rama, podemos actualizar los ficheros a la última versión que hayamos subido a Github (a origin):

    git pull
    

    Podemos aprender más sobre git con libros como Aprende Git y de paso Github, o con tutoriales como los de Atlassian - Git. Ahora que ya sabemos cómo utilizar un poco git para tener el código actualizado en el ordenador y en la Raspberry Pi, vamos a ver los scripts, teniendo en cuenta que la versión de Python que yo estoy utilizando es la python3.5 (ya que en versiones anteriores, como la python2.7 algunas funciones y librerías se llaman diferente y nos darán errores de ejecución de los scripts).

    Servidor - Ordenador con Ubuntu

    En el servidor incluiremos el siguiente script stream_server_computer.py. Su función es la siguiente:

    • Importamos las librerías (como OpenCV cv2 para procesar las imágenes recibidas, socketserver para poder crear un servidor de sockets…)
    • Indicamos variables de configuración (como el puerto que va a utilizar el servidor, en nuestro caso el 8000)
    • Creamos un hilo de ejecución (thread), que creará un servidor de socket, esperando hasta recibir la primera petición de un cliente (de la Raspberry Pi). Creamos un hilo porque en el futuro necesitaremos crear varios sockets en paralelo (uno para recibir imágenes, otro para recibir datos de distancia del sensor ultrasónico, etcétera).
    • En cuanto recibamos la primera conexión, iremos leyendo el buffer de bits que nos vaya enviando el cliente (la Raspberry Pi). Este cliente irá enviando imagen a imagen (frames), que serán obtenidos como objetos de imagen de OpenCV.
    • Al obtener un objeto de imagen OpenCV, lo procesaremos decodificando los datos mediante cv2.imdecode() (por ejemplo, lo podemos convertir a escala de grises), y lo mostraremos en una ventana mediante cv2.imshow()
    • Y así hasta que pulsemos sobre la ventana la tecla q, que finalizará la ejecución.

    Es importante tener en cuenta que tendremos que ejecutar este script servidor antes que el script cliente. Para ello, como hemos comentado, basta con ejecutar desde el entorno virtualizado python stream_server_computer.py.

    El código del servidor es el siguiente (descargable desde Github - jorgecasas/autonomous-rc-car):

    # Import libraries
    import threading
    import socketserver
    import socket
    import cv2
    import numpy as np
    import math
    
    # Config vars
    server_ip = '192.168.1.235'
    server_port = 8000
    image_fps = 24 
    
    # Class to handle the jpeg video stream received from client
    class VideoStreamHandler(socketserver.StreamRequestHandler):
     
        def handle(self):
     
            stream_bytes = b' '
    
            # stream video frames one by one
            try:
                while True:
                    stream_bytes += self.rfile.read(1024)
    
                    first = stream_bytes.find(b'\xff\xd8')
                    last = stream_bytes.find(b'\xff\xd9')
                    if first != -1 and last != -1:
                        jpg = stream_bytes[first:last+2]
                        stream_bytes = stream_bytes[last+2:]
                        gray = cv2.imdecode(np.fromstring(jpg, dtype=np.uint8), cv2.IMREAD_GRAYSCALE)
                        image = cv2.imdecode(np.fromstring(jpg, dtype=np.uint8), cv2.IMREAD_UNCHANGED)
    
                        # lower half of the image
                        half_gray = gray[120:240, :]
     
                        # Mostramos imagenes
                        cv2.imshow('image', image)
                        #cv2.imshow('mlp_image', half_gray)
    
                        # reshape image
                        image_array = half_gray.reshape(1, 38400).astype(np.float32)
                        
                        if cv2.waitKey(1) & 0xFF == ord('q'):
                            break
    
                cv2.destroyAllWindows()
    
            finally:
                print( 'Connection closed on videostream thread' )
    
    # Class to handle the different threads 
    class ThreadServer():
    
        # Server thread to handle the video
        def server_thread(host, port):
            server = socketserver.TCPServer((host, port), VideoStreamHandler)
            server.serve_forever()
    
        print( '+ Starting videostream server in ' + str( server_ip ) + ':' + str( server_port ) )
        video_thread = threading.Thread(target=server_thread( server_ip, server_port))
        video_thread.start()
    
    # Starting thread server handler
    if __name__ == '__main__':
        ThreadServer( server_ip, server_port )
    
    

    Cliente - Raspberry Pi

    En el cliente incluiremos el siguiente script stream_client_raspberry.py. Su cometido es el siguiente:

    • Importar librerías (como socket para crear un cliente de sockets, picamera para poder obtener imágenes de la cámara de la Raspberry Pi, etc…)
    • Indicar parámetros de configuración (IP del servidor, puerto del servidor, dimensiones de la imagen a capturar, frames por segundo, duración máxima de envío…)
    • Abrimos un socket a la dirección IP y puerto del servidor.
    • Inicializamos la cámara de la Raspberry Pi, dándole 2 segundos para inicializarse
    • Enviamos imágenes en forma de stream JPEG al servidor hasta que pase el tiempo máximo recording_time o se cierre la comunicación (pulsando Ctrl + C, etcétera)

    El código del cliente es el siguiente (descargable desde Github - jorgecasas/autonomous-rc-car). Lo ejecutaremos mediante python stream_client_raspberry.py.

    # Import libraries
    import io
    import socket
    import struct
    import time
    import picamera
    
    
    # Config vars
    server_ip = '192.168.1.235'
    server_port = 8000
    image_width = 320
    image_height = 240
    image_fps = 10
    recording_time = 600
    
    
    # Connect a client socket to server_ip:server_port
    print( 'Trying to connect to streaming server in ' + str( server_ip ) + ':' + str( server_port ) );
    
    # create socket and bind host
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect((server_ip, server_port))
    connection = client_socket.makefile('wb')
    
    try:
        with picamera.PiCamera() as camera:
            camera.resolution = (image_width, image_height)
            camera.framerate = image_fps
    
            # Give 2 secs for camera to initilize
            time.sleep(2)                       
            start = time.time()
            stream = io.BytesIO()
            
            # send jpeg format video stream
            for foo in camera.capture_continuous(stream, 'jpeg', use_video_port = True):
                connection.write(struct.pack('<L', stream.tell()))
                connection.flush()
                stream.seek(0)
                connection.write(stream.read())
                if time.time() - start > recording_time:
                    break
                stream.seek(0)
                stream.truncate()
        connection.write(struct.pack('<L', 0))
    
    finally:
        connection.close()
        client_socket.close()
        print( 'JPEG streaming finished!' );
    
    
    

    Y con esto, podremos ver en nuestro ordenador las imágenes que vayamos obteniendo de la cámara. En principio no deberían tener mucho lag, por lo que para las pruebas nos va a servir.

    En los siguientes posts iremos actualizando estos scripts para ir introduciendo algo de inteligencia a nuestro vehículo (por ejemplo, crear una red neuronal para detectar una señal de STOP). ¡Nos vemos!

    Más información sobre el proyecto