Skip to content

Commit 874d31f

Browse files
committed
modification based on TianYu's branch
1 parent e0bd3ae commit 874d31f

2 files changed

Lines changed: 202 additions & 43 deletions

File tree

homework/pedestrian_detection.py

Lines changed: 160 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,74 @@
1414
import struct, ctypes
1515

1616

17+
1718
# ----- Helper Functions -----
1819
def 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+
4185
def 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

380505
class FakePedestrianDetector2D(Component):

homework/pointcloud.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,40 @@
1010

1111
def fit_plane_ransac(points, threshold, min_inliers, iterations=100):
1212
"""
13-
A simple RANSAC plane fitting implementation.
13+
A more efficient RANSAC plane fitting method.
14+
- Skips iterations with nearly collinear points.
15+
- Prioritizes diverse point selection to improve stability.
1416
"""
1517
best_inliers_count = 0
1618
best_plane = None
1719
best_inliers = None
1820
n_points = points.shape[0]
21+
1922
if n_points < 3:
2023
return None, None
2124

2225
for _ in range(iterations):
2326
idx = np.random.choice(n_points, 3, replace=False)
2427
sample = points[idx]
2528
p1, p2, p3 = sample
29+
30+
# Ensure the points are sufficiently spread out (avoid collinearity)
31+
if np.linalg.norm(p1 - p2) < 0.1 or np.linalg.norm(p2 - p3) < 0.1:
32+
continue
33+
2634
normal = np.cross(p2 - p1, p3 - p1)
2735
norm = np.linalg.norm(normal)
36+
2837
if norm == 0:
29-
continue
38+
continue # Skip degenerate cases
39+
3040
normal = normal / norm
3141
d = -np.dot(normal, p1)
3242
plane = np.hstack([normal, d])
43+
3344
distances = np.abs(points.dot(normal) + d)
3445
inliers = np.where(distances < threshold)[0]
46+
3547
if len(inliers) > best_inliers_count:
3648
best_inliers_count = len(inliers)
3749
best_plane = plane
@@ -42,6 +54,7 @@ def fit_plane_ransac(points, threshold, min_inliers, iterations=100):
4254
else:
4355
return None, None
4456

57+
4558
def backproject_pixel(u, v, K):
4659
"""
4760
Backproject a pixel (u,v) into a normalized 3D ray in camera coordinates using the intrinsic matrix K.
@@ -118,17 +131,38 @@ def refine_cluster(roi_points, center, eps=0.2, min_samples=10):
118131
best_cluster = cluster
119132
return best_cluster
120133

121-
def remove_ground_by_min_range(cluster, z_range=0.05):
134+
def remove_ground(cluster, z_range=0.05, ransac_threshold=0.05, min_inliers=20):
122135
"""
123-
Remove ground points from the cluster by finding the minimum z value and eliminating
124-
all points within z_range of that minimum.
136+
Improved ground removal using RANSAC plane fitting.
137+
- First, attempts to remove the dominant ground plane using RANSAC.
138+
- If no valid plane is found, falls back to simple min-Z filtering.
139+
140+
Parameters:
141+
- cluster (np.ndarray): (N,3) point cloud cluster.
142+
- z_range (float): Threshold for min-Z based ground removal (fallback).
143+
- ransac_threshold (float): RANSAC distance threshold for ground plane fitting.
144+
- min_inliers (int): Minimum number of inliers to accept a ground plane.
145+
146+
Returns:
147+
- filtered (np.ndarray): The cluster with ground points removed.
125148
"""
126149
if cluster is None or cluster.shape[0] == 0:
127150
return cluster
128-
min_z = np.min(cluster[:, 2])
129-
filtered = cluster[cluster[:, 2] > (min_z + z_range)]
151+
152+
# Attempt RANSAC-based plane removal
153+
plane, inliers = fit_plane_ransac(cluster, threshold=ransac_threshold, min_inliers=min_inliers, iterations=100)
154+
155+
if plane is not None and inliers is not None and len(inliers) > 0:
156+
# Remove inlier points belonging to the ground plane
157+
filtered = np.delete(cluster, inliers, axis=0)
158+
else:
159+
# Fallback to simple min-Z removal
160+
min_z = np.min(cluster[:, 2])
161+
filtered = cluster[cluster[:, 2] > (min_z + z_range)]
162+
130163
return filtered
131164

165+
132166
def get_bounding_box_center_and_dimensions(points):
133167
"""
134168
Compute the bounding box center and dimensions (max - min) for the given points.
@@ -281,7 +315,7 @@ def main():
281315

282316
refined_cluster = refine_cluster(roi_points, intersection, eps=0.125, min_samples=10)
283317

284-
refined_cluster = remove_ground_by_min_range(refined_cluster, z_range=0.05)
318+
refined_cluster = remove_ground(refined_cluster, z_range=0.05)
285319
if refined_cluster is None or refined_cluster.shape[0] == 0:
286320
refined_center = intersection
287321
bbox_dims = np.array([0, 0, 0])

0 commit comments

Comments
 (0)