1414import struct , ctypes
1515
1616
17+
1718# ----- Helper Functions -----
1819def match_existing_pedestrian (
1920 new_center : np .ndarray ,
2021 new_dims : tuple ,
2122 existing_agents : Dict [str , AgentState ],
22- distance_threshold : float = 1.0
23+ prev_velocities : Dict [str , np .ndarray ],
24+ distance_threshold : float = 1.0 ,
25+ size_threshold : float = 0.5 ,
26+ height_threshold : float = 0.3 ,
27+ velocity_threshold : float = 2.0
2328) -> str :
2429 """
25- Return the agent_id of the best match if within distance_threshold;
26- otherwise return None.
30+ Match a newly detected pedestrian with an existing one using:
31+ - Euclidean distance
32+ - Bounding box similarity
33+ - Height consistency
34+ - Velocity consistency
35+
36+ Returns the best-matching agent_id or None if no good match is found.
2737 """
2838 best_agent_id = None
29- best_dist = float ('inf' )
39+ best_score = float ('inf' )
3040
3141 for agent_id , agent_state in existing_agents .items ():
3242 old_center = np .array ([agent_state .pose .x , agent_state .pose .y , agent_state .pose .z ])
43+ old_dims = agent_state .dimensions
44+
45+ # 1. Euclidean Distance Check
3346 dist = np .linalg .norm (new_center - old_center )
34- if dist < distance_threshold and dist < best_dist :
35- best_dist = dist
47+ if dist > distance_threshold :
48+ continue # Skip if too far away
49+
50+ # 2. Bounding Box Size Similarity (with Zero-Division Handling)
51+ size_norm = np .linalg .norm (np .array (old_dims ))
52+ if size_norm > 0 :
53+ size_change = np .linalg .norm (np .array (new_dims ) - np .array (old_dims )) / size_norm
54+ else :
55+ size_change = float ('inf' ) # Prevent invalid matching
56+
57+ if size_change > size_threshold :
58+ continue # Skip if bounding box changes too much
59+
60+ # 3. Height Consistency Check
61+ height_change = abs (new_dims [2 ] - old_dims [2 ]) / old_dims [2 ] if old_dims [2 ] > 0 else 0
62+ if height_change > height_threshold :
63+ continue # Skip if height changes drastically
64+
65+ # 4. Velocity Consistency Check
66+ if agent_id in prev_velocities :
67+ prev_velocity = prev_velocities [agent_id ]
68+ estimated_velocity = (new_center - old_center )
69+ velocity_change = np .linalg .norm (estimated_velocity - prev_velocity )
70+
71+ if velocity_change > velocity_threshold :
72+ continue # Skip if unrealistic velocity jump
73+
74+ # Score: Lower score = better match (distance is primary factor)
75+ score = dist
76+ if score < best_score :
77+ best_score = score
3678 best_agent_id = agent_id
3779
3880 return best_agent_id
3981
4082
83+
84+
4185def compute_velocity (old_pose : ObjectPose , new_pose : ObjectPose , dt : float ) -> tuple :
4286 """
4387 Returns a (vx, vy, vz) velocity in the same frame as old_pose/new_pose.
@@ -128,15 +172,82 @@ def refine_cluster(roi_points, center, eps=0.2, min_samples=10):
128172 return best_cluster
129173
130174
131- def remove_ground_by_min_range ( cluster , z_range = 0.05 ):
175+ def fit_plane_ransac ( points , threshold , min_inliers , iterations = 100 ):
132176 """
133- Remove ground points by finding the minimum z value in the cluster and eliminating
134- all points with z within z_range of that minimum.
177+ A more efficient RANSAC plane fitting method.
178+ - Skips iterations with nearly collinear points.
179+ - Prioritizes diverse point selection to improve stability.
180+ """
181+ best_inliers_count = 0
182+ best_plane = None
183+ best_inliers = None
184+ n_points = points .shape [0 ]
185+
186+ if n_points < 3 :
187+ return None , None
188+
189+ for _ in range (iterations ):
190+ idx = np .random .choice (n_points , 3 , replace = False )
191+ sample = points [idx ]
192+ p1 , p2 , p3 = sample
193+
194+ # Ensure the points are sufficiently spread out (avoid collinearity)
195+ if np .linalg .norm (p1 - p2 ) < 0.1 or np .linalg .norm (p2 - p3 ) < 0.1 :
196+ continue
197+
198+ normal = np .cross (p2 - p1 , p3 - p1 )
199+ norm = np .linalg .norm (normal )
200+
201+ if norm == 0 :
202+ continue # Skip degenerate cases
203+
204+ normal = normal / norm
205+ d = - np .dot (normal , p1 )
206+ plane = np .hstack ([normal , d ])
207+
208+ distances = np .abs (points .dot (normal ) + d )
209+ inliers = np .where (distances < threshold )[0 ]
210+
211+ if len (inliers ) > best_inliers_count :
212+ best_inliers_count = len (inliers )
213+ best_plane = plane
214+ best_inliers = inliers
215+
216+ if best_inliers_count >= min_inliers :
217+ return best_plane , best_inliers
218+ else :
219+ return None , None
220+
221+
222+ def remove_ground (cluster , z_range = 0.05 , ransac_threshold = 0.05 , min_inliers = 20 ):
223+ """
224+ Improved ground removal using RANSAC plane fitting.
225+ - First, attempts to remove the dominant ground plane using RANSAC.
226+ - If no valid plane is found, falls back to simple min-Z filtering.
227+
228+ Parameters:
229+ - cluster (np.ndarray): (N,3) point cloud cluster.
230+ - z_range (float): Threshold for min-Z based ground removal (fallback).
231+ - ransac_threshold (float): RANSAC distance threshold for ground plane fitting.
232+ - min_inliers (int): Minimum number of inliers to accept a ground plane.
233+
234+ Returns:
235+ - filtered (np.ndarray): The cluster with ground points removed.
135236 """
136237 if cluster is None or cluster .shape [0 ] == 0 :
137238 return cluster
138- min_z = np .min (cluster [:, 2 ])
139- filtered = cluster [cluster [:, 2 ] > (min_z + z_range )]
239+
240+ # Attempt RANSAC-based plane removal
241+ plane , inliers = fit_plane_ransac (cluster , threshold = ransac_threshold , min_inliers = min_inliers , iterations = 100 )
242+
243+ if plane is not None and inliers is not None and len (inliers ) > 0 :
244+ # Remove inlier points belonging to the ground plane
245+ filtered = np .delete (cluster , inliers , axis = 0 )
246+ else :
247+ # Fallback to simple min-Z removal
248+ min_z = np .min (cluster [:, 2 ])
249+ filtered = cluster [cluster [:, 2 ] > (min_z + z_range )]
250+
140251 return filtered
141252
142253
@@ -208,6 +319,9 @@ def __init__(self, vehicle_interface: GEMInterface):
208319 self .last_person_boxes = []
209320 self .lidar_pc = None # Will be updated via ROS callback
210321 self .pc_raw = None
322+ self .tracked_agents = {}
323+ self .pedestrian_counter = 0
324+
211325
212326 def rate (self ):
213327 return 4.0
@@ -260,7 +374,6 @@ def update(self, vehicle: VehicleState) -> Dict[str, AgentState]:
260374
261375 for i , box in enumerate (self .last_person_boxes ):
262376 cx , cy , w , h = box
263- # --- same LiDAR + bounding box logic as before ---
264377 ray_dir_cam = backproject_pixel (cx , cy , self .K )
265378 ray_dir_lidar = self .R_c2l @ ray_dir_cam
266379 ray_dir_lidar /= np .linalg .norm (ray_dir_lidar )
@@ -289,10 +402,36 @@ def update(self, vehicle: VehicleState) -> Dict[str, AgentState]:
289402 else :
290403 refined_cluster = refine_cluster (roi_points , intersection , eps = 0.125 , min_samples = 10 )
291404
292- refined_cluster = remove_ground_by_min_range (refined_cluster , z_range = 0.03 )
405+ refined_cluster = remove_ground (refined_cluster , z_range = 0.03 )
406+
407+ # ✅ Ensure existing_agents only contains valid AgentState objects
408+ existing_agents = {k : v [0 ] for k , v in self .tracked_agents .items () if v }
409+
410+ # ✅ Ensure prev_velocities are properly formatted
411+ prev_velocities = {
412+ k : np .array (v [0 ].velocity ) if v [0 ].velocity is not None else np .array ([0 , 0 , 0 ])
413+ for k , v in self .tracked_agents .items ()
414+ }
415+
416+ # ✅ Match before using `existing_id`
417+ existing_id = match_existing_pedestrian (
418+ new_center = np .array (intersection ),
419+ new_dims = (physical_width , depth_margin , physical_height ),
420+ existing_agents = existing_agents ,
421+ prev_velocities = prev_velocities ,
422+ distance_threshold = 1.0 ,
423+ size_threshold = 0.5 ,
424+ height_threshold = 0.3 ,
425+ velocity_threshold = 2.0
426+ )
427+
293428 if refined_cluster is None or refined_cluster .shape [0 ] == 0 :
294429 refined_center = intersection
295- dims = (0 , 0 , 0 )
430+ # ✅ Fix: Ensure `existing_id` exists in `existing_agents` before accessing `.dimensions`
431+ if existing_id is not None and existing_id in existing_agents :
432+ dims = existing_agents [existing_id ].dimensions
433+ else :
434+ dims = (0.5 , 0.5 , 1.7 ) # Default pedestrian size
296435 yaw , pitch , roll = 0 , 0 , 0
297436 else :
298437 pcd = o3d .geometry .PointCloud ()
@@ -302,22 +441,15 @@ def update(self, vehicle: VehicleState) -> Dict[str, AgentState]:
302441 dims = tuple (obb .extent )
303442 R_lidar = obb .R .copy ()
304443
305- # transform to vehicle frame
306- refined_center_lidar_hom = np .array ([refined_center [0 ],
307- refined_center [1 ],
308- refined_center [2 ],
309- 1.0 ])
310- refined_center_vehicle_hom = self .T_l2v @ refined_center_lidar_hom
444+ refined_center_vehicle_hom = self .T_l2v @ np .array ([* refined_center , 1.0 ])
311445 refined_center_vehicle = refined_center_vehicle_hom [:3 ]
312446
313447 R_vehicle = self .T_l2v [:3 , :3 ] @ R_lidar
314448 euler_angles_vehicle = R .from_matrix (R_vehicle ).as_euler ('zyx' , degrees = True )
315- yaw , pitch , roll = euler_angles_vehicle # rename for clarity
449+ yaw , pitch , roll = euler_angles_vehicle
316450
317- refined_center = refined_center_vehicle # override to use vehicle frame
318- # dims remains the same; orientation is now (yaw, pitch, roll)
451+ refined_center = refined_center_vehicle # Override to use vehicle frame
319452
320- # -- CREATE the new pose in the vehicle frame --
321453 new_pose = ObjectPose (
322454 t = current_time ,
323455 x = refined_center [0 ],
@@ -329,16 +461,7 @@ def update(self, vehicle: VehicleState) -> Dict[str, AgentState]:
329461 frame = ObjectFrameEnum .CURRENT
330462 )
331463
332- # 1) Attempt to match with an existing pedestrian
333- existing_id = match_existing_pedestrian (
334- new_center = np .array ([new_pose .x , new_pose .y , new_pose .z ]),
335- new_dims = dims ,
336- existing_agents = {k : v .pose for k , v in self .tracked_agents .items ()},
337- distance_threshold = 1.0
338- )
339-
340- if existing_id is not None :
341- # 2) Update existing agent
464+ if existing_id is not None and existing_id in self .tracked_agents :
342465 old_agent_state , old_time = self .tracked_agents [existing_id ]
343466 dt = current_time - old_time
344467 vx , vy , vz = compute_velocity (old_agent_state .pose , new_pose , dt )
@@ -356,7 +479,6 @@ def update(self, vehicle: VehicleState) -> Dict[str, AgentState]:
356479 self .tracked_agents [existing_id ] = (updated_agent , current_time )
357480
358481 else :
359- # 3) Create a new agent
360482 agent_id = f"pedestrian{ self .pedestrian_counter } "
361483 self .pedestrian_counter += 1
362484
@@ -375,6 +497,9 @@ def update(self, vehicle: VehicleState) -> Dict[str, AgentState]:
375497 return agents
376498
377499
500+
501+
502+
378503# ----- Fake Pedestrian Detector 2D (unchanged) -----
379504
380505class FakePedestrianDetector2D (Component ):
0 commit comments