This workspace provides a multi-robot perception and obstacle avoidance pipeline using Graph Neural Networks (GNNs).
Radar and pose data are merged across robots, converted into graph structures, processed by a GNN, and used within the Nav2 costmap layer for shared obstacle avoidance.
cd ~/dev/ROS_GNN_ws
colcon build
source install/setup.bashros2 launch gnn_object_segmentation multi_robot_inference.launch.py run_id:=Horizontalros2 run gnn_inference inference_node --ros-args -p run_id:=Horizontal- rm03
ros2 run gnn_object_segmentation waypoint_publisher --ros-args -r __ns:=/rm03 -p robot_id:=rm03 -p waypoints_file:=waypoints.yaml -p dwell_sec:=5.0 -p loop:=false -p run_id:=Horizontal- rm04
ros2 run gnn_object_segmentation waypoint_publisher --ros-args -r __ns:=/rm04 -p robot_id:=rm04 -p waypoints_file:=waypoints.yaml -p dwell_sec:=5.0 -p loop:=false -p run_id:=Horizontalrviz2Configs stored in:
ROS_GNN_ws/src/gnn_object_segmentation/configs
- Reference Pose Selection – use EP03 Vicon pose as anchor
- Temporal Alignment – retrieve closest radar + poses for all robots
- Radar Processing – transform into GNN-compatible features + RViz visualization points
- Noise Filtering (Optional) – statistical outlier removal
- Sliding Window Buffer – maintain recent frames for temporal graphs
- Graph Construction – build nodes & edges with spatial-temporal features
- Validation – skip if graph is empty
- Publish Graph – send
GraphDatato/graph_data - Visualization – update radar & robot poses in RViz
START
|
├── Is EP03 pose available?
│ └── No → Abort
│ └── Yes → ref_timestamp
|
├── Retrieve closest radar + poses
├── Filter + transform radar
├── Add to sliding buffer
├── Build graph from window
├── Publish GraphData
└── Update RViz
Purpose
Synchronize multi-robot radar + pose, build windowed graphs for GNN inference, and publish to /graph_data. Logs timing and graph stats to CSV for analysis.
Subscriptions
/rm04/ti_mmwave/radar_scan_pcl(sensor_msgs/PointCloud2)/rm03/ti_mmwave/radar_scan_pcl(sensor_msgs/PointCloud2)/rm04/vicon_pose(geometry_msgs/PoseWithCovarianceStamped)/rm03/vicon_pose(geometry_msgs/PoseWithCovarianceStamped)- TF:
map←→<robot>/base_link(viatf2_ros)
Publications
/graph_data(gnn_interfaces/GraphData) — flattened node/edge arrays,frame_id: "map"- (Visualization helpers exist in code via
GraphVisualizer, currently disabled by default in launch)
Parameters
run_id: string— experiment/run label (used in logs & paths)window_size: int— number of frames used to build the temporal graph window (default in launch: 5)- CLI flags parsed by the node:
--visualize(defaultTruein code, controlled by launch arg)--simulation(defaultFalse)
Internal Buffers & Timing
- Radar buffers:
deque(maxlen=1000)per robot, recent ≤ 2.0 s - Pose buffers:
deque(maxlen=1000)per robot (used for delays/logging) - Merged frames buffer:
deque(maxlen=100) - Fresh data requirement: at least one radar topic with msg newer than 0.75 s
- Temporal window threshold: include frames within 2.0 s of current ref time
- Buffer pruning: old entries dropped after 1.5 s
Normalization & Features
- Loads
normalization_weights_unified.pklat node startup - Node features per radar point:
- Position: x, y, z
- Normalized: snr, range
- Encoded angles: sin/cos(azimuth), sin/cos(elevation)
- robot_id (1 for
rm04/robot_1, 2 forrm03/robot_2)
- Angle normalization uses sin/cos with mean/std from weights file
Edge Construction (KNN)
- Base K: 8 (reduced automatically if N < K)
- For each directed edge i→j:
edge_attr = [dx, dy, dz, Δsnr_norm, Δt]
- Produces:
edge_indexshape (2, E) (flattened on publish)edge_attrshape (E, 5)
Radar → Global Transform & Filtering
- Looks up TF:
map←<robot>/base_linkat the radar timestamp - Skips points too close to robot: (x < 0.3 and y ≤ 0.3)
- Global bounds kept (others skipped):
-12.0 ≤ x ≤ 10.0,-5.0 ≤ y ≤ 7.0
- Optional Statistical Outlier Removal (SOR) per robot if ≥ 15 points:
- k=8,
std_ratio=1.0(keeps points with mean-NN-distance < mean + std_ratio·std)
- k=8,
Graph Publishing
GraphData.header.frame_id = "map"node_features: flattenedfloat32list;node_feature_dim = 10edge_index: flattenedint64list (2×E)edge_attr: flattenedfloat32list;edge_attr_dim = 5num_nodes,num_edgesfilled from arrays- Skips publish if no nodes or no edges
CSV Logging
- Folder:
datalogging/data_merge/<run_id>_<timestamp>/ - File:
data_merger_log.csv - Appended per publish with:
timestamp_ros, run_id, window_size, rm04_points, rm03_points, node_count, edge_count, inter_robot_edge_count, build_time_ms, merge_latency_ms, vicon_delay_rm04_ms, vicon_delay_rm03_ms
Key Methods (high level)
pointcloud2_to_xyz_intensity→ (N×4) arrayprocess_radar_points→ TF to map, bounds & proximity culling, metrics (range/angles), build dictsget_recent_window_frames→ last N frames within 2.0sbuild_graph_from_window→ assemble features, KNN edgespublish_graph→ populateGraphDatamessage and publishsync_and_merge(timer @ 33Hz) → main loop to align, filter, build, log, publish, prune
Purpose
Consumes GraphData, runs a trained GNN to classify nodes & edges, clusters nodes into objects, tracks them over time, and publishes polygons + RViz markers. Also logs inference stats for offline analysis.
Subscriptions
/graph_data(gnn_interfaces/GraphData)
Publications
/gnn_objects(visualization_msgs/MarkerArray) — outline polygons per detected object/class/tracked_polygons(gnn_interfaces/TrackedPolygon) — polygons + label, confidence, and contributor info
Parameters
run_id: string— for logging directory and CSV filename
Model/Config Files (expected in working dir)
configuration_flw_gnn_v1.yml— model/configgraph_based_detector.pt— trained weights
Inference Pipeline
- Tensor prep: convert node/edge arrays to tensors on selected device.
- Forward pass:
detector()returns node logits and edge logits. - Node class: softmax → argmax →
pred_classand confidences. - Edge class: sigmoid on link-prob for edge[:,1]; threshold 0.91 →
pred_edge_class. - Graph clusters: build undirected graph using only edges with:
- same node class,
pred_edge_class == 1,- pairwise distance < 1.2 m.
- Polygon extraction: for each connected component (with ≥6 pts and full-rank), compute alpha shape (
alpha=0.01) to get boundary polygon; compute centroid and average confidence. - Shared perception: compute per-object contributor ratios from node
robot_idfeature; attach to message. - Tracking: per-class Kalman trackers, matched by centroid distance < 0.5 m; trackers expire after 1.0 s of no updates.
- Publishing:
- RViz
MarkerArrayoutlines (LINE_STRIP). TrackedPolygonmessages withlabel,confidence,polygon,contributor_ids,contributor_ratios.
- RViz
- Logging (
datalogging/inference/<run_id>_<timestamp>/gnn_inference_log.csv):timestamp_ros, run_id, graph_timestamp, inference_latency_ms, cluster_count, object_class_distribution, mean_confidence, new_objects, removed_objects, gnn_pipeline_latency_ms, shared_objects_perc.
Alternate Processor (optional)
process_predictions_v2: DBSCAN on node positions class-wise, convex hull per cluster, and RViz labels. (Off by default.)
Utilities
class_to_color— fixed palette → RGBAcreate_pointcloud2— helper for optional OctoMap (currently disabled)publish_tracked_objects_as_pointcloud— builds a labeled PointCloud2 (disabled by default)
Purpose
Sends sequential navigation goals to Nav2’s NavigateToPose action using waypoints from YAML. Supports per-run/per-robot waypoint selection, auto-fills yaw if missing, optional dwell time between goals, and looping.
Action Client
- Server name respects namespace:
/<ns>/navigate_to_pose(e.g.,/rm03/navigate_to_pose)
Parameters
run_id: string— run label; used to pick a nested section from YAML (default:R1-N1in code)robot_id: string—rm03,rm04, … (default:rm03)waypoints_file: string— file name or path; searched in package sharegnn_object_segmentation/config/if relativedwell_sec: double— pause between goals in wall time (default:5.0)loop: bool— repeat the sequence forever (default:False)goal_timeout_sec: double— cancel a goal if it exceeds this time (default:120.0)
Waypoint YAML Structure (supported)
- Nested by run/robot
Horizontal:
rm03:
- {name: wp1, x: 1.0, y: 0.0, theta: 0.0}
- {name: wp2, x: 2.0, y: 1.0, theta: 1.57}
rm04:
- {name: wp1, x: -1.0, y: 0.5, theta: -1.57}- Flat list
waypoints:
- {name: A, x: 0.0, y: 0.0, theta: 0.0}
- {name: B, x: 3.0, y: 0.0, theta: 0.0}- Plain list
- {name: A, x: 0.0, y: 0.0, theta: 0.0}
- {name: B, x: 3.0, y: 0.0, theta: 0.0}Yaw Handling
- If
thetais0or missing, it is auto-filled using the path tangent (next or previous waypoint).
Behavior
- Loads waypoints once at startup; logs count.
- For each waypoint: sends a
NavigateToPosegoal and waits synchronously for result. - If result not received within
goal_timeout_sec, cancels the goal and proceeds. - Sleeps
dwell_secseconds (wall time) between goals. - If
loop:=false, exits after one pass; otherwise, repeats.
Waypoint Scenarios & Intended Paths (per waypoints.yaml)
Below are the waypoint scenarios and what each is designed to test for our shared-perception experiments. Coordinates are in the map frame (meters). If a waypoint’s theta is zero/missing, the publisher auto-fills yaw from the path tangent.
-
R1-N1 — Straight Opposed Traverse
rm03: left-to-right sweep (x: −8 → +8, y ≈ 0).rm04: right-to-left sweep (x: +8 → −8, y ≈ 0).- Intent: head-on, same corridor, to stress opposing-traffic avoidance via shared detections.
-
R1-N2 — Skewed Cross
rm03: diagonal up-right (≈ [−7,−3] → [7,3]).rm04: partial diagonal + return to origin (≈ [7,−3] → [0,0]).- Intent: angled crossing and merge at a common rendezvous.
-
R1-N3 — Straight vs. Patrol Loop
rm03: straight pass (−8 → +8 along y≈0).rm04: right corridor patrol (to [8,0] → up to [0,3] → down to [0,−3] → back).- Intent: one passer, one patroller; tests persistence and spatio-temporal fusion.
-
Diagonal — Dense Diagonal Weave (both robots)
rm03&rm04: long multi-point diagonals with alternating offsets.- Intent: high waypoint density to evaluate tracking continuity and polygon stability under rapid viewpoint changes.
-
Horizontal — Lane Following (east–west)
rm03&rm04: multi-segment horizontal sweeps at several y-bands (≈ +3, +1, 0, −1, −2…).- Intent: parallel-lane coverage and repeated passes for map consistency and false-positive suppression.
-
Vertical — Aisle Sweeps (north–south)
rm03&rm04: columnar sweeps with many turns along x-bands.- Intent: aisle/column coverage; tests edge-case TF alignment and long-horizon tracking.
-
S4-F8 — Figure-Eight (mirrored)
- Both robots trace loops that compose a figure-eight across quadrants.
- Intent: classic crossing paths with curvature; stresses trajectory intersection handling and object identity maintenance.
-
S5-NarrowLane — Tapered Corridor
rm03: climbs from bottom-left through a narrowing central lane to top-right.rm04: mirrored descent.- Intent: bottleneck/narrow-passage negotiation, testing shared situational awareness at constrictions.
-
S6-PerimeterCross — Perimeter + Diagonal Cross
rm03: left perimeter up, across top, then diagonal to right;rm04: symmetric inverse.- Intent: boundary following plus mid-field crossing for mixed context transitions.
CLI Examples
- rm03
ros2 run gnn_object_segmentation waypoint_publisher --ros-args -r __ns:=/rm03 -p robot_id:=rm03 -p waypoints_file:=waypoints.yaml -p dwell_sec:=5.0 -p loop:=false -p run_id:=Horizontal- rm04
ros2 run gnn_object_segmentation waypoint_publisher --ros-args -r __ns:=/rm04 -p robot_id:=rm04 -p waypoints_file:=waypoints.yaml -p dwell_sec:=5.0 -p loop:=false -p run_id:=HorizontalStarts rosbag recording, the data merger, and visualization helpers.
Launch Arguments
visualize(defaultFalse) — enable RViz/markerssimulation(defaultFalse)rviz_config(defaultsrc/gnn_object_segmentation/rviz/flw_hall_gnn.rviz)run_id(defaultdefault_run)
Rosbag Recording
- Output directory:
datalogging/rosbags/<run_id>_<YYYYMMDD_HHMMSS>_bag - Topics recorded (subset shown as in file):
clock/rm03/ti_mmwave/radar_scan_pcl,/rm04/ti_mmwave/radar_scan_pcl/tf,/tf_staticrm03/odom,rm04/odom/rm03/vicon_pose,/rm04/vicon_pose/graph_data,/tracked_polygons,gnn_objects/rm03/global_costmap/costmap_raw,/rm04/global_costmap/costmap_raw/navigate_to_pose/feedback,/navigate_to_pose/result/rm03/plan,/rm04/plan,/rm03/path,/rm04/path/rm03/cmd_vel,/rm04/cmd_vel/rm03/behavior_tree_log,/rm04/behavior_tree_log
Nodes
- Data Merger
package='gnn_object_segmentation',executable='data_merge',name='data_merge'- Parameters:
{run_id, window_size=5} - Arguments:
--visualize,--simulation(from launch args)
- Tracked Polygon Marker Publisher
package='gnn_object_segmentation',executable='tracked_polygon_visualizer'- Parameters:
input_topic=/tracked_polygons,output_topic=/tracked_polygon_markers
- (Optional commented blocks for RViz and arena markers are present in the launch file.)
std_msgs/Header header
geometry_msgs/Polygon polygon
uint32 label
float32 confidence
uint32[] contributor_ids # robot IDs that contributed to this object
float32[] contributor_ratios # normalized ratios per contributor
std_msgs/Header header
# Tensor-like flat arrays with size metadata
float32[] node_features # flattened [N x 10]
uint32 node_feature_dim # always 10
int64[] edge_index # flattened [2 x E]
float32[] edge_attr # flattened [E x 5]
uint32 edge_attr_dim # always 5
# Optional metadata
uint32 num_nodes
uint32 num_edges
run_id→ experiment ID (e.g.,Horizontal)robot_id→ namespace ID (rm03,rm04)waypoints_file→ YAML with navigation waypointsdwell_sec→ pause at waypointloop→ loop through waypoints or notwindow_size→ temporal window length for graph (default 5 via launch)visualize/simulation→ launch flags forwarding to node args
/graph_data→GraphData.msg(node features, edge index, edge attributes)/gnn_objects→MarkerArrayoutlines of detected objects/tracked_polygons→TrackedPolygonmessages (class, confidence, polygon, contributor info)- Logs:
datalogging/data_merge/<run_id>_<timestamp>/data_merger_log.csvdatalogging/inference/<run_id>_<timestamp>/gnn_inference_log.csv
- TF tree must provide
map↔<robot>/base_linkforrm03andrm04. This would be done via the vicon pose publisher node. - Normalization weights file
normalization_weights_unified.pklmust be present for the Data Merger node. - Inference node expects
configuration_flw_gnn_v1.ymlandgraph_based_detector.ptin its working directory. - Waypoint publisher searches
gnn_object_segmentation/config/ifwaypoints_fileis not an absolute path. - Bounds and thresholds (proximity/bounds/SOR/window/edge distance/edge threshold) are tuned for the current arena; adjust as needed.
- If neither radar stream is fresh (older than 0.75s), graph construction is skipped for that tick.