Skip to content

Commit 0eb64c6

Browse files
Merge pull request #194 from krishauser/gazebo_agent_obstacle_detection
Added Gazebo Obstacle Detection
2 parents 0fe0452 + 3d54689 commit 0eb64c6

6 files changed

Lines changed: 195 additions & 65 deletions

File tree

GEMstack/onboard/interface/gem_gazebo.py

Lines changed: 98 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from GEMstack.state.obstacle import ObstacleMaterialEnum, ObstacleState, ObstacleStateEnum
12
from .gem import *
23
from ...utils import settings
34
import math
@@ -53,7 +54,13 @@ class GNSSReading:
5354
'car': 'car',
5455
'vehicle': 'car',
5556
'truck': 'medium_truck',
56-
'large_truck': 'large_truck'
57+
'large_truck': 'large_truck',
58+
'cone': 'traffic_cone'
59+
}
60+
61+
# Map model prefixes to obstacle types
62+
MODEL_PREFIX_TO_OBSTACLE_TYPE = {
63+
'cone': 'traffic_cone'
5764
}
5865

5966
class GEMGazeboInterface(GEMInterface):
@@ -93,11 +100,15 @@ def __init__(self):
93100
self.model_states_sub = None
94101
self.tracked_model_prefixes = settings.get('simulator.agent_tracker.model_prefixes',
95102
['pedestrian', 'bicycle', 'car'])
103+
self.tracked_obstacle_prefixes = settings.get('simulator.obstacle_tracker.model_prefixes',
104+
['cone'])
96105
self.agent_detector_callback = None
106+
self.obstacle_detector_callback = None
97107
self.last_agent_positions = {}
98108
self.last_agent_velocities = {}
99109
self.last_model_states_time = 0.0
100110
self.agent_detection_rate = settings.get('simulator.agent_tracker.rate', 10.0) # Hz
111+
self.obstacle_detection_rate = settings.get('simulator.obstacle_tracker.rate', 50.0) # Hz
101112

102113
# Frame transformation variables
103114
self.start_pose_abs = None # Initial vehicle pose in GLOBAL frame
@@ -141,15 +152,15 @@ def model_states_callback(self, msg: ModelStates):
141152
current_time = self.time()
142153

143154
# Check if we should process this update (rate limiting)
144-
if current_time - self.last_model_states_time < 1.0/self.agent_detection_rate:
155+
if ((current_time - self.last_model_states_time < 1.0/self.agent_detection_rate) and (current_time - self.last_model_states_time < 1.0/self.obstacle_detection_rate)):
145156
return
146157

147158
# Calculate time delta since last update
148159
dt = current_time - self.last_model_states_time
149160
self.last_model_states_time = current_time
150161

151162
# Skip if no callback is registered
152-
if self.agent_detector_callback is None:
163+
if self.agent_detector_callback is None and self.obstacle_detector_callback is None:
153164
return
154165

155166
# Find vehicle in model states
@@ -203,16 +214,24 @@ def model_states_callback(self, msg: ModelStates):
203214
agent_type = None
204215
for prefix in self.tracked_model_prefixes:
205216
if model_name.lower().startswith(prefix.lower()):
206-
# Find the appropriate agent type from the prefix
207217
for key, value in MODEL_PREFIX_TO_AGENT_TYPE.items():
208218
if prefix.lower().startswith(key.lower()):
209219
agent_type = value
210220
break
211221
break
222+
223+
obstacle_type = None
224+
for prefix in self.tracked_obstacle_prefixes:
225+
if model_name.lower().startswith(prefix.lower()):
226+
for key, value in MODEL_PREFIX_TO_OBSTACLE_TYPE.items():
227+
if prefix.lower().startswith(key.lower()):
228+
obstacle_type = value
229+
break
230+
break
212231

213-
if agent_type is None:
232+
if agent_type is None and obstacle_type is None:
214233
continue # Not an agent we're tracking
215-
234+
216235
# Get position and orientation from model states
217236
position = msg.pose[i].position
218237
orientation = msg.pose[i].orientation
@@ -224,7 +243,6 @@ def model_states_callback(self, msg: ModelStates):
224243
# Convert orientation quaternion to euler angles
225244
quaternion = (orientation.x, orientation.y, orientation.z, orientation.w)
226245
roll, pitch, yaw = euler_from_quaternion(quaternion)
227-
228246
# Create agent pose in ABSOLUTE_CARTESIAN frame (Gazebo's native frame)
229247
agent_global_pose = ObjectPose(
230248
frame=ObjectFrameEnum.ABSOLUTE_CARTESIAN,
@@ -269,61 +287,72 @@ def model_states_callback(self, msg: ModelStates):
269287
# If transformation not initialized yet, just use the model_states pose
270288
agent_pose = agent_global_pose
271289

272-
# Calculate velocity manually if twist data is zero or missing
273-
velocity = (linear_vel.x, linear_vel.y, linear_vel.z)
274-
velocity_is_zero = abs(linear_vel.x) < 1e-6 and abs(linear_vel.y) < 1e-6 and abs(linear_vel.z) < 1e-6
275-
276-
if velocity_is_zero and model_name in self.last_agent_positions and dt > 0:
277-
# Calculate velocity from position difference
278-
prev_pos = self.last_agent_positions[model_name]
279-
dx = position.x - prev_pos[0]
280-
dy = position.y - prev_pos[1]
281-
dz = position.z - prev_pos[2]
290+
if agent_type is not None:
291+
# Calculate velocity manually if twist data is zero or missing
292+
velocity = (linear_vel.x, linear_vel.y, linear_vel.z)
293+
velocity_is_zero = abs(linear_vel.x) < 1e-6 and abs(linear_vel.y) < 1e-6 and abs(linear_vel.z) < 1e-6
282294

283-
# Calculate velocity (position change / time)
284-
calculated_vel = (dx/dt, dy/dt, dz/dt)
295+
if velocity_is_zero and model_name in self.last_agent_positions and dt > 0:
296+
# Calculate velocity from position difference
297+
prev_pos = self.last_agent_positions[model_name]
298+
dx = position.x - prev_pos[0]
299+
dy = position.y - prev_pos[1]
300+
dz = position.z - prev_pos[2]
301+
302+
# Calculate velocity (position change / time)
303+
calculated_vel = (dx/dt, dy/dt, dz/dt)
304+
305+
# Apply some smoothing with the previous velocity if available
306+
if model_name in self.last_agent_velocities:
307+
prev_vel = self.last_agent_velocities[model_name]
308+
# Apply exponential smoothing (0.7 current + 0.3 previous)
309+
velocity = (
310+
0.7 * calculated_vel[0] + 0.3 * prev_vel[0],
311+
0.7 * calculated_vel[1] + 0.3 * prev_vel[1],
312+
0.7 * calculated_vel[2] + 0.3 * prev_vel[2]
313+
)
314+
else:
315+
velocity = calculated_vel
285316

286-
# Apply some smoothing with the previous velocity if available
287-
if model_name in self.last_agent_velocities:
288-
prev_vel = self.last_agent_velocities[model_name]
289-
# Apply exponential smoothing (0.7 current + 0.3 previous)
290-
velocity = (
291-
0.7 * calculated_vel[0] + 0.3 * prev_vel[0],
292-
0.7 * calculated_vel[1] + 0.3 * prev_vel[1],
293-
0.7 * calculated_vel[2] + 0.3 * prev_vel[2]
294-
)
317+
# Determine activity state based on velocity magnitude
318+
velocity_magnitude = np.linalg.norm(velocity)
319+
if velocity_magnitude < 0.1:
320+
activity = AgentActivityEnum.STOPPED
321+
elif velocity_magnitude > 5.0: # Arbitrary threshold for "fast"
322+
activity = AgentActivityEnum.FAST
295323
else:
296-
velocity = calculated_vel
324+
activity = AgentActivityEnum.MOVING
325+
326+
# Get agent dimensions
327+
dimensions = AGENT_DIMENSIONS.get(agent_type, (1.0, 1.0, 1.0)) # Default if unknown
297328

298-
# Determine activity state based on velocity magnitude
299-
velocity_magnitude = np.linalg.norm(velocity)
300-
if velocity_magnitude < 0.1:
301-
activity = AgentActivityEnum.STOPPED
302-
elif velocity_magnitude > 5.0: # Arbitrary threshold for "fast"
303-
activity = AgentActivityEnum.FAST
304-
else:
305-
activity = AgentActivityEnum.MOVING
329+
# Create agent state
330+
agent_state = AgentState(
331+
pose=agent_pose, # Using START frame pose
332+
dimensions=dimensions,
333+
outline=None,
334+
type=getattr(AgentEnum, agent_type.upper()),
335+
activity=activity,
336+
velocity=velocity,
337+
yaw_rate=angular_vel.z
338+
)
306339

307-
# Get agent dimensions
308-
dimensions = AGENT_DIMENSIONS.get(agent_type, (1.0, 1.0, 1.0)) # Default if unknown
309-
310-
# Create agent state
311-
agent_state = AgentState(
312-
pose=agent_pose, # Using START frame pose
313-
dimensions=dimensions,
314-
outline=None,
315-
type=getattr(AgentEnum, agent_type.upper()),
316-
activity=activity,
317-
velocity=velocity,
318-
yaw_rate=angular_vel.z
319-
)
320-
321-
# Store current position for next velocity calculation (using raw positions)
322-
self.last_agent_positions[model_name] = (position.x, position.y, position.z)
323-
self.last_agent_velocities[model_name] = velocity
324-
325-
# Call the callback with the agent state
326-
self.agent_detector_callback(model_name, agent_state)
340+
# Store current position for next velocity calculation (using raw positions)
341+
self.last_agent_positions[model_name] = (position.x, position.y, position.z)
342+
self.last_agent_velocities[model_name] = velocity
343+
# Call the callback with the agent state
344+
self.agent_detector_callback(model_name, agent_state)
345+
346+
if obstacle_type is not None:
347+
# Create obstacle state
348+
obstacle_state = ObstacleState(
349+
dimensions=(0,0,0),
350+
outline=None,
351+
pose=agent_pose,
352+
type=getattr(ObstacleMaterialEnum, obstacle_type.upper()),
353+
activity=ObstacleStateEnum.STANDING,
354+
)
355+
self.obstacle_detector_callback(model_name, obstacle_state)
327356

328357
def subscribe_sensor(self, name, callback, type=None):
329358
if name == 'gnss':
@@ -424,6 +453,11 @@ def callback_with_cv2(msg: Image):
424453
raise ValueError("GEMGazeboInterface only supports AgentState for agent_detector")
425454
self.agent_detector_callback = callback
426455

456+
elif name == 'obstacle_detector':
457+
if type is not None and type is not ObstacleState:
458+
raise ValueError("GEMGazeboInterface only supports ObstacleState for obstacle_detector")
459+
self.obstacle_detector_callback = callback
460+
427461
def hardware_faults(self) -> List[str]:
428462
# In simulation, we don't have real hardware faults
429463
return self.faults
@@ -459,10 +493,12 @@ def send_command(self, command : GEMVehicleCommand):
459493

460494
# Calculate acceleration from pedal positions
461495
acceleration = pedal_positions_to_acceleration(accelerator_pedal_position, brake_pedal_position, v, 0, 1)
462-
496+
print("acceleration before", acceleration)
463497
# Apply reasonable limits to acceleration
464498
max_accel = settings.get('vehicle.limits.max_acceleration', 1.0)
465-
max_decel = settings.get('vehicle.limits.max_deceleration', -2.0)
499+
max_decel = -1 * settings.get('vehicle.limits.max_deceleration', 2.0) # cuz ackermann expects neg but pure pursiut wants positive decel val
500+
print("max_accel", max_accel)
501+
print("max_decel", max_decel)
466502
acceleration = np.clip(acceleration, max_decel, max_accel)
467503

468504
# Convert wheel angle to steering angle (front wheel angle)
@@ -477,14 +513,15 @@ def send_command(self, command : GEMVehicleCommand):
477513
# Don't use infinite speed, instead calculate a reasonable target speed
478514
current_speed = v
479515
target_speed = current_speed
480-
516+
print("acceleration ", acceleration)
481517
if acceleration > 0:
482518
# Accelerating - set target speed to current speed plus some increment
483519
# This is more realistic than infinite speed
484520
max_speed = settings.get('vehicle.limits.max_speed', 10.0)
485521
target_speed = min(current_speed + acceleration * 0.5, max_speed)
486522
elif acceleration < 0:
487523
# Braking - set target speed to zero if deceleration is significant
524+
print("braking ", acceleration)
488525
if brake_pedal_position > 0.1:
489526
target_speed = 0.0
490527

@@ -499,4 +536,4 @@ def send_command(self, command : GEMVehicleCommand):
499536
print(f"[ACKERMANN] Speed: {msg.speed:.2f}, Accel: {msg.acceleration:.2f}, Steer: {msg.steering_angle:.2f}")
500537

501538
self.ackermann_pub.publish(msg)
502-
self.last_command = command
539+
self.last_command = command
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
import threading
3+
import copy
4+
from typing import Dict
5+
from ..component import Component
6+
7+
from ..interface.gem import GEMInterface
8+
from ...state.obstacle import ObstacleState
9+
10+
class OmniscientObstacleDetector(Component):
11+
"""Obtains agent detections from a simulator"""
12+
def __init__(self,vehicle_interface : GEMInterface):
13+
self.vehicle_interface = vehicle_interface
14+
self.obstacles = {}
15+
self.lock = threading.Lock()
16+
17+
def rate(self):
18+
return 15.0
19+
20+
def state_inputs(self):
21+
return []
22+
23+
def state_outputs(self):
24+
return ['obstacles']
25+
26+
def initialize(self):
27+
self.vehicle_interface.subscribe_sensor('obstacle_detector',self.obstacle_callback, ObstacleState)
28+
29+
def obstacle_callback(self, name : str, obstacle : ObstacleState):
30+
with self.lock:
31+
self.obstacles[name] = obstacle
32+
33+
def update(self) -> Dict[str,ObstacleState]:
34+
with self.lock:
35+
return copy.deepcopy(self.obstacles)
36+
37+
38+
class GazeboObstacleDetector(OmniscientObstacleDetector):
39+
"""Obtains agent detections from the Gazebo simulator using model_states topic"""
40+
def __init__(self, vehicle_interface : GEMInterface, tracked_obstacle_prefixes=None):
41+
super().__init__(vehicle_interface)
42+
43+
# If specific model prefixes are provided, configure the interface to track them
44+
if tracked_obstacle_prefixes is not None:
45+
# Check if our interface has the tracked_model_prefixes attribute (is a GazeboInterface)
46+
if hasattr(vehicle_interface, 'tracked_obstacle_prefixes'):
47+
vehicle_interface.tracked_obstacle_prefixes = tracked_obstacle_prefixes
48+
print(f"Configured GazeboConeDetector to track obstacles with prefixes: {tracked_obstacle_prefixes}")
49+
else:
50+
print("Warning: vehicle_interface doesn't support tracked_obstacle_prefixes configuration")
51+
52+
def initialize(self):
53+
# Use the same agent_detector sensor as OmniscientAgentDetector
54+
# The GazeboInterface implements this with model_states subscription
55+
super().initialize()
56+
print("GazeboConeDetector initialized and subscribed to model_states")

GEMstack/onboard/visualization/mpl_visualization.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def update(self, state):
9797
# Print debugging info about the vehicle's position and frame
9898
if self.num_updates % 10 == 0:
9999
print(f"Vehicle position: ({state.vehicle.pose.x:.2f}, {state.vehicle.pose.y:.2f}), frame: {state.vehicle.pose.frame}")
100+
print("Obstacle state:", state.obstacles)
100101

101102
# Pedestrian metrics and position debugging
102103
ped_positions = []
@@ -246,6 +247,18 @@ def update(self, state):
246247
except Exception as e:
247248
print(f"Error in pedestrian plot: {str(e)}")
248249

250+
try:
251+
# ---- plot static obstacles in orange ----
252+
for obs in state.obstacles.values():
253+
x_obs, y_obs = obs.pose.x, obs.pose.y
254+
self.axs[0].plot(x_obs, y_obs, 'o',
255+
markersize=8,
256+
markerfacecolor='orange',
257+
markeredgecolor='darkorange',
258+
label='_nolegend_')
259+
except Exception as e:
260+
print(f"Error plotting obstacles: {str(e)}")
261+
249262
# Update canvas
250263
try:
251264
self.fig.canvas.draw_idle()

GEMstack/state/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
'VehicleState',
99
'Roadgraph',
1010
'Roadmap',
11-
'Obstacle',
11+
'Obstacle', 'ObstacleMaterialEnum','ObstacleStateEnum','ObstacleState',
1212
'Sign',
1313
'AgentState','AgentEnum','AgentActivityEnum',
1414
'SceneState',
@@ -23,7 +23,7 @@
2323
from .trajectory import Path,Trajectory
2424
from .vehicle import VehicleState,VehicleGearEnum
2525
from .roadgraph import Roadgraph, RoadgraphLane, RoadgraphCurve, RoadgraphRegion, RoadgraphCurveEnum, RoadgraphLaneEnum, RoadgraphRegionEnum, RoadgraphSurfaceEnum, RoadgraphConnectionEnum
26-
from .obstacle import Obstacle, ObstacleMaterialEnum
26+
from .obstacle import Obstacle, ObstacleMaterialEnum, ObstacleStateEnum, ObstacleState
2727
from .sign import Sign, SignEnum, SignalLightEnum, SignState
2828
from .roadmap import Roadmap
2929
from .agent import AgentState, AgentEnum, AgentActivityEnum

0 commit comments

Comments
 (0)