Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ego traffic light sensor #2

Open
wants to merge 11 commits into
base: windows
Choose a base branch
from
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "carla_msgs"]
path = carla_msgs
url = https://github.com/carla-simulator/ros-carla-msgs
url = https://github.com/ToyotaResearchInstitute/ros-carla-msgs.git
branch = master
2 changes: 1 addition & 1 deletion carla_msgs
13 changes: 12 additions & 1 deletion carla_ros_bridge/src/carla_ros_bridge/actor_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from carla_ros_bridge.actor_list_sensor import ActorListSensor
from carla_ros_bridge.camera import Camera, RgbCamera, DepthCamera, SemanticSegmentationCamera, DVSCamera
from carla_ros_bridge.collision_sensor import CollisionSensor
from carla_ros_bridge.ego_traffic_light_sensor import EgoTrafficLightSensor
from carla_ros_bridge.ego_vehicle import EgoVehicle
from carla_ros_bridge.gnss import Gnss
from carla_ros_bridge.imu import ImuSensor
Expand Down Expand Up @@ -106,7 +107,7 @@ def update_available_objects(self):
destroyed_actors = self._active_actors - current_actors
self._active_actors = current_actors

# Create/destroy actors not managed by the bridge.
# Create/destroy actors not managed by the bridge.
self.lock.acquire()
for actor_id in spawned_actors:
carla_actor = self.world.get_actor(actor_id)
Expand Down Expand Up @@ -351,6 +352,16 @@ def _create_object(self, uid, type_id, name, attach_to, spawn_pose, carla_actor=
parent=parent,
node=self.node)

elif type_id == EgoTrafficLightSensor.get_blueprint_name():
actor = EgoTrafficLightSensor(
uid=uid,
name=name,
parent=parent,
node=self.node,
actor_list=self.actors,
carla_map=self.world.get_map()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since get map is expensive, does it cause lag here? Can we pre-load the map?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you arent seeing lag we can consider not doing that, just curious.

Copy link
Collaborator Author

@hieu-nguyen-tri hieu-nguyen-tri Aug 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't notice any lag. Now when reading Carla APi documentation, it looks like this call to get the current XODR map. I think if we store it, then when a new map is loaded, I don't think the reference will have latest map. I will check it.

self. map = world.get_map() /// When a map is changed, self.map will have the old value

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I expected, when call world.load_world(), the old self.map is not updated. We need to call self.world.get_map() again.

That means the logic I have for checking if the map has been changed won't work.

Is there a way to detect map change? I tried listen to this topic: /carla/world_info but I don't see it outputs anything when the map changes either.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get map is really expensive but get map name i think is cheap. Call that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need the map object to get the waypoints that ego is currently on. Get map is called once at beginning or when a new map is loaded so I think it should be fine.

Regarding detecting if the map has changed. It will require passing the CarlaClient to ActorFactory, then pass it to whatever sensors we want to detect the map change. The reason are:

  • carla_world.id. This one doesn't change when load a new map
  • carla_client.get_world().id. This one does.

I think if we want to load a new map maybe it's better to rerun the motion sim with the new map at this point. That will make sure the rosbag correct and clean; especially, other sensors in carla rosbridge don't detect loading a new map.

Maybe hold off on merging this and let's try it on the motion platform to see if there are any delay then?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi @anrp-tri

)

elif carla_actor.type_id.startswith('traffic'):
if carla_actor.type_id == "traffic.traffic_light":
actor = TrafficLight(uid, name, parent, self.node, carla_actor)
Expand Down
185 changes: 185 additions & 0 deletions carla_ros_bridge/src/carla_ros_bridge/ego_traffic_light_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#!/usr/bin/env python

from collections import defaultdict
from typing import Dict

import carla
from carla_msgs.msg import CarlaEgoTrafficLightInfo, CarlaTrafficLightStatus
from ros_compatibility.node import CompatibleNode
from ros_compatibility.qos import DurabilityPolicy, QoSProfile

from carla_ros_bridge.actor import Actor
from carla_ros_bridge.ego_vehicle import EgoVehicle
from carla_ros_bridge.pseudo_actor import PseudoActor
from carla_ros_bridge.traffic import TrafficLight

# Publish info no less frequently than this.
_PUBLISH_INTERVAL_SECONDS = 5.0

class StoplineInfo:
"""An util class to keep track of a traffic light and its stop location."""

def __init__(self, traffic_light_actor: TrafficLight, approaching_road_id: int, approaching_lane_id: int, stop_location: carla.Location):
self.traffic_light_actor = traffic_light_actor
self.stop_location = stop_location
self.approaching_road_id = approaching_road_id
self.approaching_lane_id = approaching_lane_id


def create_stop_line_info_map(node: CompatibleNode, actor_list: Dict[int, Actor], search_distance: float = 2.0):
""" Return a dictionary of stop line info.

Args:
node: ROS node wrapper for logging purposes
actor_list: A dictionary of spawn actors. Use this to search for traffic light actors
search_distance: the approximate distance where to get the previous waypoints
Return:
A map(road_id, lane_id -> StopLineInfo)
"""
results = defaultdict(dict)
for actor in actor_list.values():
if not isinstance(actor, TrafficLight):
continue

carla_actor = actor.carla_actor
affected_points = carla_actor.get_affected_lane_waypoints()
if len(affected_points) == 0:
node.logerr(f"Unable to find any affected points for traffic light id: {carla_actor.id}")
continue

for point in affected_points:
if not point.is_junction:
node.logerr(
f"The affected waypoint from traffic light is not in a junction. Traffic_light_id: {carla_actor.id}, road_id: {point.road_id}, lane_id: {point.lane_id}"
)
continue

# Go to previous waypoints to get the road id and lane id on which the ego will be approaching.
previous_points = point.previous(search_distance)
if len(previous_points) == 0:
node.logerr(
f"Unable to find previous points for traffic_light_id: {carla_actor.id}, road_id: {point.road_id}, lane_id: {point.lane_id}"
)
continue

# This stop location is roughly at the stop line. It depends on how the map is created in RoadRunner.
stop_location = point.transform.location
p = previous_points[0]
results[p.road_id][p.lane_id] = StoplineInfo(actor, p.road_id, p.lane_id, stop_location)

return results


def get_stop_line_info(stop_line_info: Dict[int,Dict[int, StoplineInfo]], road_id: int, lane_id: int):
"""Given road_id and lane_id, find and return a StopLineInfo object if any."""
if stop_line_info is None:
return None
if road_id in stop_line_info and lane_id in stop_line_info[road_id]:
return stop_line_info[road_id][lane_id]
return None


class EgoTrafficLightSensor(PseudoActor):
"""A sensor to publish CarlaEgoTrafficLightInfo"""

def __init__(self, uid: int, name: str, parent: Actor, node: CompatibleNode, actor_list: Dict[int, Actor], carla_map: carla.Map):
super(EgoTrafficLightSensor, self).__init__(uid=uid, name=name, parent=parent, node=node)

self.pub = node.new_publisher(
CarlaEgoTrafficLightInfo,
self.get_topic_prefix() + "/info",
qos_profile=QoSProfile(depth=10, durability=DurabilityPolicy.TRANSIENT_LOCAL),
)

self._info_published_at = None

self.map = carla_map
self.map_name = self.map.name
self.ego = None
self.actor_list = actor_list
self.stop_line_info_map = None
self.cur_sli = None

self.msg = CarlaEgoTrafficLightInfo()
self.msg.traffic_light_status = CarlaTrafficLightStatus()
self.msg.inside_intersection = False

def destroy(self):
super(EgoTrafficLightSensor, self).destroy()
self.node.destroy_publisher(self.pub)
self.actor_list = None
self.stop_line_info_map = None
self.node.loginfo("Destroy EgoTrafficLightSensor")

@staticmethod
def get_blueprint_name():
return "sensor.pseudo.ego_traffic_light"

def update(self, frame: int, timestamp: float):
try:
if self.map is None:
self.node.logwarn("Carla Map is not assgined")
return

if self.stop_line_info_map is None or self.map_name != self.map.name:
self.stop_line_info_map = create_stop_line_info_map(self.node, self.actor_list)
andrewbest-tri marked this conversation as resolved.
Show resolved Hide resolved
self.map_name = self.map.name
self.ego = self.get_ego(self.actor_list)

if self.ego is None:
self.node.logwarn("Unable to find ego.")
return

ego_location = self.ego.get_location()
wp = self.map.get_waypoint(ego_location)
sli = get_stop_line_info(self.stop_line_info_map, wp.road_id, wp.lane_id)

inside_intersection = wp.is_junction
cur_status = self.cur_sli.traffic_light_actor.get_status() if self.cur_sli else None
new_status = sli.traffic_light_actor.get_status() if sli else None

if (self.msg.inside_intersection != inside_intersection
or self.has_traffic_light_status_changes(cur_status, new_status)
or self._info_published_at is None
or timestamp - self._info_published_at > _PUBLISH_INTERVAL_SECONDS
):
self.calculate_and_publish_data(ego_location, sli, inside_intersection, timestamp)
self._info_published_at = timestamp
except Exception as e:
self.node.loginfo("Error: {}".format(e))

def calculate_and_publish_data(self, ego_location: carla.Location, new_sli: StoplineInfo, inside_intersection: bool, timestamp: float):
"""Publish CarlaEgoTrafficLightInfo message."""
if inside_intersection:
self.msg.distance_to_stopline = -1.0
if self.cur_sli:
self.msg.traffic_light_status = self.cur_sli.traffic_light_actor.get_status()
else:
if new_sli:
self.msg.distance_to_stopline = ego_location.distance(new_sli.stop_location)
self.msg.traffic_light_status = new_sli.traffic_light_actor.get_status()
else:
self.msg.distance_to_stopline = -1.0
self.msg.traffic_light_status = CarlaTrafficLightStatus()
# Store new StopLineInfo.
self.cur_sli = new_sli

self.msg.ego_id = self.ego.id
self.msg.inside_intersection = inside_intersection
self.msg.header = self.get_msg_header(timestamp=timestamp)
self.pub.publish(self.msg)
andrewbest-tri marked this conversation as resolved.
Show resolved Hide resolved

def get_ego(self, actor_list: Dict[int, Actor]):
"""Return a CarlaActor representing the ego car."""
for actor in actor_list.values():
if isinstance(actor, EgoVehicle):
return actor.carla_actor
return None

def has_traffic_light_status_changes(self, cur_traffic_light_status: CarlaTrafficLightStatus, new_traffic_light_status: CarlaTrafficLightStatus):
"""Return True if the traffic light status (either traffic light id or state) has been changed; False otherwise."""
if cur_traffic_light_status is None or new_traffic_light_status is None:
# Don't detect changes when one of the status is None.
return False

return cur_traffic_light_status != new_traffic_light_status
8 changes: 6 additions & 2 deletions carla_spawn_objects/config/object_sensor_only.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"objects":
{
"objects":
[
{
"type": "sensor.pseudo.objects",
Expand All @@ -12,6 +12,10 @@
{
"type": "sensor.pseudo.traffic_lights",
"id": "traffic_lights"
},
{
"type": "sensor.pseudo.ego_traffic_light",
"id": "ego_traffic_light"
}
]
}