7 minute read

Construcción del brain y visualización del nubes de puntos LiDAR

Esta semana hemos logrado arreglar los problemas de visualización del sensor LiDAR. Ya se puede visualizar exitosamente la nube de puntos a medida que avanza la simulación y se teleopera un vehículo. Una vez arreglados los problemas de visualización y estructurado el código, ya tenemos creado el cerebro.

Nuestro cerebro tiene las siguientes funciones:

  • Control del vehículo.
  • Generar y visualizar datos de un sensor LiDAR.
  • Visualizar vista frontal del vehículo.

Control del vehículo

Esta función es la que establece el control manual del vehículo para poder teleoperarlo mediante teclado (teclas W,A,S,D):

def control(vehicle):

    control = carla.VehicleControl()

    # Leer teclas
    keys = pygame.key.get_pressed()

    # valores iniciales
    control.throttle = 0.0
    control.steer = 0.0
    control.brake = 0.0
    control.hand_brake = False
    control.reverse = False

    # W acelerar
    if keys[pygame.K_w]:
        control.throttle = 1.0

    # S retroceso/stop
    if keys[pygame.K_s]:
        if vehicle.get_velocity().length() < 0.1:
            # Si está casi parado: poner marcha atrás
            control.reverse = True
            control.throttle = 1.0
        else:
            # Si va hacia adelante: frenar
            control.brake = 1.0
    
    # A girar a la izquierda
    if keys[pygame.K_a]:
        control.steer = -0.5  # negativo = izquierda

    # D : girar a la derecha
    if keys[pygame.K_d]:
        control.steer = 0.5   # positivo = derecha

    # Aplicar al vehículo
    vehicle.apply_control(control)

Sensor LiDAR

Para la parte del sensor LiDAR, se aplican las funciones set_lidar, que añade el LiDAR semántico (etiqueta los puntos con la clase del objeto) al vehículo y lo ‘spawnea’ en el escenario del simulador, y lidar_callback, encargada de estructurar los datos y las coordenadas del sensor para poder visualizarlos. Además, se ha utilizado esta vez el sensor sensor.lidar.ray_cast_semantic, que además de devolver una nube de puntos, cada uno de estos tiene una etiqueta semántica (clase de objeto detectado).

A continuación, se muestran las funciones del sensor LiDAR:

def set_lidar(world, ego_vehicle, delta):

    # Crear LiDAR
    lidar_transform = carla.Transform(carla.Location(z=2.5))
    lidar_bp = world.get_blueprint_library().find('sensor.lidar.ray_cast_semantic')
    lidar_bp.set_attribute('channels', '128')        # Número de rayos
    lidar_bp.set_attribute('range', '100')           # Alcance en metros
    lidar_bp.set_attribute('rotation_frequency', str(1 / delta))  # Hz
    lidar_bp.set_attribute('points_per_second', '1200000') 

    # Spawnear sensor y aplicarlo al vehículo
    sensor_lidar = world.spawn_actor(lidar_bp, lidar_transform, attach_to=ego_vehicle)
    return sensor_lidar
# Función de callback del sensor LiDAR
def lidar_callback(lidar_data, point_cloud, vehicle_transform, absolute_view):

    # Definir el formato del paquete de datos del LiDAR semántico
    dtype = np.dtype([
        ('x', np.float32),
        ('y', np.float32),
        ('z', np.float32),
        ('cos_inc_angle', np.float32),
        ('object_idx', np.uint32),
        ('object_tag', np.uint32)
    ])

    # Convertir los datos binarios a un array de numpy con el dtype estructurado
    data = np.frombuffer(lidar_data.raw_data, dtype=dtype)

    # Extraer coordenadas de los puntos
    points = np.vstack((data['x'], data['y'], data['z'])).T

    # Reflejar los datos en el eje X (para ajustarlos al sistema CARLA)
    points[:, 0] = -points[:, 0]

    # Obtener etiquetas semánticas (object_tag)
    semantic_tags = data['object_tag']

    # Asignar color según la etiqueta semántica
    # Por ejemplo, con el colormap inferno o viridis
    normalized_tags = (semantic_tags - semantic_tags.min()) / (np.ptp(semantic_tags) + 1e-6)
    colors = VIRIDIS[(normalized_tags * (VIRIDIS.shape[0]-1)).astype(np.int32)]

    if absolute_view:
        vehicle_location = np.array([
            -vehicle_transform.location.x,
            vehicle_transform.location.y,
            vehicle_transform.location.z
        ])

        pitch = vehicle_transform.rotation.pitch
        yaw = vehicle_transform.rotation.yaw
        roll = vehicle_transform.rotation.roll

        rotation_matrix = euler_to_rotation_matrix(pitch, yaw, roll).T

        # Transformar los puntos al sistema global
        points = (rotation_matrix @ points.T).T + vehicle_location

    # Actualizar el point cloud en Open3D
    point_cloud.points = o3d.utility.Vector3dVector(points)
    point_cloud.colors = o3d.utility.Vector3dVector(colors)

Cámara frontal

Para establecer una cámara RGB en el vehículo y poder así visualizar el recorrido de este mientras se teleopera, se han añadido las siguiente funciones:

# Función para establecer cámara en el vehículo
def set_camera(world, vehicle):
    
    camera_bp = world.get_blueprint_library().find('sensor.camera.rgb')
    camera_bp.set_attribute('image_size_x', '800')
    camera_bp.set_attribute('image_size_y', '600')
    camera_bp.set_attribute('fov', '90')
    camera_transform = carla.Transform(
        carla.Location(x=1.5, z=1.2),       # 1.5 m adelante, 1.2 m de altura
        carla.Rotation(pitch=0.0, yaw=0.0, roll=0.0))  # mirando hacia adelante
    
    camera = world.spawn_actor(camera_bp, camera_transform, attach_to=vehicle)
    return camera

Nota: la visión que muestra la cámara es la frontal, es decir, la que se vería si se colocara la cámara en el capó del coche:

# Función para procesar imágenes de la cámara
def camera_callback(image, display):
    array = np.frombuffer(image.raw_data, dtype=np.uint8)
    array = np.reshape(array, (image.height, image.width, 4))
    array = array[:, :, :3]  # Quitar el canal alfa
    array = array[:, :, ::-1]  # Convertir de BGRA a RGB
    surface = pygame.surfarray.make_surface(array.swapaxes(0, 1))
    display.blit(surface, (0, 0))
    # Actualizar solo esta superficie en vez de toda la pantalla
    pygame.display.update(display.get_rect())

Función principal: brain()

La función brain() es la que se encarga de conectar con el servidor, configurar y añadir todos los agente al escenario del simulador (‘Town05’, en este caso), procesa eventos del simulador, actualiza la nube de puntos y, además, maneja las distintas vistas en la visualización del sensor LiDAR. A continuación, se muestra el código del cerebro:

# Cerebro / Función principal
def brain():

    pygame.init()

    # Configurar Pygame sin usar OpenGL explícitamente
    pygame.display.gl_set_attribute(pygame.GL_ACCELERATED_VISUAL, 0)
    width, height = 800, 600
    screen = pygame.display.set_mode((width, height), pygame.SRCALPHA)
    pygame.display.set_caption("CARLA Vehículo Control")

    # Conexión con servidor de Carla
    client = carla.Client('localhost', 2000)
    client.set_timeout(10.0)
    client.load_world('Town05') #Establecer escenario determinado

    # Crear escenario
    world = client.get_world()

    # Librería del simulador
    blueprint_library = world.get_blueprint_library()
    traffic_manager = client.get_trafficmanager(8000)               # Control del tráfico de vehículos automáticos
    traffic_manager.set_synchronous_mode(True)                      # Modo síncrono: avanza en cada "world.tick()"
    traffic_manager.set_global_distance_to_leading_vehicle(2.5)     # Distancia de seguridad entre vehículos

    # Configuración del simulador
    settings = world.get_settings()                # Obtener configuración actual              
    delta = 0.033                                   # 0.05 s por cada tick: 20 FPS
    settings.fixed_delta_seconds = delta           # la simulación avanza cada 0.05 s
    settings.synchronous_mode = True               # Activa el modo síncrono
    world.apply_settings(settings)                 # Aplica los ajustes modificados

    
    # Librería de vehículos
    vehicle_blueprints = blueprint_library.filter('*vehicle*')

    # Cogemos todos los puntos de aparición predefinidos del escenario
    spawn_points = world.get_map().get_spawn_points()

    # Seleccionamos un vehículo determinado de la librería
    vehicle_bp = vehicle_blueprints.find('vehicle.audi.tt')

    # Generar vehículo con el que se va a trabajar
    ego_vehicle = world.spawn_actor(vehicle_bp, spawn_points[0])
    if ego_vehicle is not None:
        print('Vehículo spawneado correctamente')

    # Establecer sensor LiDAR al vehículo
    lidar = set_lidar(world, ego_vehicle, delta)

    # Establecer cámara rgb al vehículo
    camera = set_camera(world, ego_vehicle)

    global actor_list, current_view_index, origin_sphere_added, absolute_view

    actor_list.append(ego_vehicle)
    actor_list.append(camera)
    actor_list.append(lidar)

    camera.listen(lambda image: camera_callback(image, screen))

    # Crear nube de puntos y llamar al callback
    point_cloud = o3d.geometry.PointCloud()
    lidar.listen(lambda data: lidar_callback(data, point_cloud, lidar.get_transform(), absolute_view))

    # Visualizador de Open3D y configuraciones iniciales
    viz = o3d.visualization.VisualizerWithKeyCallback()
    viz.create_window(window_name='Sensor LiDAR', width=960, height=540, left=480, top=270)
    viz.get_render_option().background_color = [0.05, 0.05, 0.05]
    viz.get_render_option().point_size = 1.35
    viz.get_render_option().show_coordinate_frame = True

    # Configurar vista
    set_camera_view(viz,current_view_index)

    #Inicializar efera en el origen
    origin_sphere = create_origin_sphere()
    origin_sphere_added = False

    def toggle_camera_view(_):
        global current_view_index, absolute_view, origin_sphere_added
        # Alternar a la siguiente vista en el array de cámaras
        current_view_index = (current_view_index + 1) % len(camera_views)
        set_camera_view(viz, current_view_index)
        
        # Activar o desactivar vista absoluta y la esfera en el origen
        absolute_view = (current_view_index == len(camera_views) - 1)  # Vista cenital como absoluta
        if absolute_view and not origin_sphere_added:
            viz.add_geometry(origin_sphere)
            origin_sphere_added = True
        elif not absolute_view and origin_sphere_added:
            viz.remove_geometry(origin_sphere)
            origin_sphere_added = False
        
        print(f"Cambiando a vista {current_view_index + 1}")
        return True

    # Callback para alternar vistas
    viz.register_key_callback(ord("V"), toggle_camera_view)

    # Ciclo principal de visualización
    lidar_data_received = False
    dt0 = datetime.now()
    frame = 0
    try:
        while True:

            # Procesar control del vehículo
            control(ego_vehicle)

            # Procesar eventos de Pygame
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    print("\n Cierre de ventana detectado.")
                    raise KeyboardInterrupt

            # Añadir nube de puntos al visualizador una sola vez
            if frame == 5 and not lidar_data_received:
                viz.add_geometry(point_cloud)
                lidar_data_received = True
                print("Nube de puntos añadida a Open3D")

            # Actualizar visualización
            viz.update_geometry(point_cloud)    # actualizar nube de puntos
            viz.poll_events()                   # procesar eventos del sistema de ventanas
            viz.update_renderer()               # actualizar el contenido de la ventana
            time.sleep(0.03)
            world.tick()                        # avanzar la simulación


            # Mostrar FPS
            process_time = datetime.now() - dt0
            if process_time.total_seconds() > 0:
                fps = 1.0 / process_time.total_seconds()
                sys.stdout.write(f'\rFPS: {fps:.2f} ')
                sys.stdout.flush()
            dt0 = datetime.now()
            frame += 1

            # Salir si Open3D se cierra
            if not viz.poll_events():
                break

    except KeyboardInterrupt:
        pass
    finally:
        cleanup()

Resultado de la visualización del LiDAR

Al ejecutar el código anterior junto con el simulador, aparecen las siguiente ventanas de simulación: cámara frontal del vehículo generado (izquierda) y la visualización del sensor LiDAR.

Simulador Carla

[Futuros avances]

Tras haber creado ya brain(), el siguiente paso será el de procesar los datos del LiDAR para entrenar un modelo que en función de estos datos, sea capaz de darnos una salida que sea el comportamiento que deben de seguir los actuadores del vehículo (throttle, steer, brake).

El modelo que prevemos utilizar, será de Imitation Learning (aprendizaje por imitación). A priori, tendremos que utilizar un dataset con suficientes muestras de conducción experta, para que el modelo las aprenda, o bien generaremos muestras estableciendo el autopilot al vehículo en Carla. Un posible modelo a implementar sería PilotNet (de NVIDIA), que es muy utilizado para conducción autónoma a partir de imágenes (desconozco por el momento si también se ha entrenado con nubes de puntos). Las siguientes semanas seguiremos avanzando por esta línea.