From cfbd1700b1537a086bc5a3b97b73da27e270c6d8 Mon Sep 17 00:00:00 2001 From: seefelke Date: Thu, 21 Nov 2024 18:36:02 +0100 Subject: [PATCH 01/55] Added new message types Trajectory and Navigation Point. Added Behaviour Enum in localplanning/utils.py --- code/planning/CMakeLists.txt | 23 ++++++++++++++++------- code/planning/msg/NavigationPoint.msg | 3 +++ code/planning/msg/Trajectory.msg | 2 ++ code/planning/package.xml | 4 ++++ code/planning/src/local_planner/utils.py | 7 +++++++ 5 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 code/planning/msg/NavigationPoint.msg create mode 100644 code/planning/msg/Trajectory.msg diff --git a/code/planning/CMakeLists.txt b/code/planning/CMakeLists.txt index 34c3ccd0..9da52430 100755 --- a/code/planning/CMakeLists.txt +++ b/code/planning/CMakeLists.txt @@ -13,8 +13,11 @@ project(planning) find_package(catkin REQUIRED COMPONENTS perception rospy + rosout roslaunch std_msgs + geometry_msgs + message_generation ) roslaunch_add_file_check(launch) @@ -48,9 +51,11 @@ catkin_python_setup() ## * add every package in MSG_DEP_SET to generate_messages(DEPENDENCIES ...) ## Generate messages in the 'msg' folder -# add_message_files( -# MinDistance.msg -# ) + add_message_files( + FILES + NavigationPoint.msg + Trajectory.msg + ) ## Generate services in the 'srv' folder # add_service_files( @@ -67,9 +72,12 @@ catkin_python_setup() # ) ## Generate added messages and services with any dependencies listed here -# generate_messages( -# perception -# ) + generate_messages( + DEPENDENCIES + std_msgs + perception + geometry_msgs + ) ################################################ ## Declare ROS dynamic reconfigure parameters ## @@ -105,7 +113,8 @@ catkin_package( # LIBRARIES planning # CATKIN_DEPENDS other_catkin_pkg # DEPENDS system_lib - CATKIN_DEPENDS perception rospy +# CATKIN_DEPENDS perception rospy + CATKIN_DEPENDS message_runtime ) ########### diff --git a/code/planning/msg/NavigationPoint.msg b/code/planning/msg/NavigationPoint.msg new file mode 100644 index 00000000..e9eb53c9 --- /dev/null +++ b/code/planning/msg/NavigationPoint.msg @@ -0,0 +1,3 @@ +geometry_msgs/Point position +float32 speed +uint8 behaviour \ No newline at end of file diff --git a/code/planning/msg/Trajectory.msg b/code/planning/msg/Trajectory.msg new file mode 100644 index 00000000..94060cdd --- /dev/null +++ b/code/planning/msg/Trajectory.msg @@ -0,0 +1,2 @@ +Header header +planning/NavigationPoint[] navigationPoints \ No newline at end of file diff --git a/code/planning/package.xml b/code/planning/package.xml index a321a109..daf36bc6 100755 --- a/code/planning/package.xml +++ b/code/planning/package.xml @@ -47,6 +47,9 @@ + message_generation + message_runtime + rospy roslaunch @@ -57,6 +60,7 @@ carla_msgs sensor_msgs std_msgs + geometry_msgs perception perception diff --git a/code/planning/src/local_planner/utils.py b/code/planning/src/local_planner/utils.py index d976506d..5d684df9 100644 --- a/code/planning/src/local_planner/utils.py +++ b/code/planning/src/local_planner/utils.py @@ -3,6 +3,7 @@ import math import carla import os +from enum import IntEnum # import rospy @@ -23,6 +24,12 @@ EARTH_RADIUS_EQUA = 6378137.0 +# getattr(Behaviour, "name").value returns the ID +# Behaviour(id) returns Behaviour.NAME +class Behaviour(IntEnum): + CRUISING = 0 + + def get_distance(pos_1, pos_2): """Calculate the distance between two positions From a4355e7c451a7dbaf616cdb63eb61673091e4d45 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:51:17 +0100 Subject: [PATCH 02/55] Update code/planning/CMakeLists.txt Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- code/planning/CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/planning/CMakeLists.txt b/code/planning/CMakeLists.txt index 9da52430..c6288e03 100755 --- a/code/planning/CMakeLists.txt +++ b/code/planning/CMakeLists.txt @@ -113,8 +113,7 @@ catkin_package( # LIBRARIES planning # CATKIN_DEPENDS other_catkin_pkg # DEPENDS system_lib -# CATKIN_DEPENDS perception rospy - CATKIN_DEPENDS message_runtime + CATKIN_DEPENDS perception rospy message_runtime ) ########### From 668b6d745a1a443aa265330d47a5e562006946d0 Mon Sep 17 00:00:00 2001 From: seefelke Date: Sun, 24 Nov 2024 11:45:39 +0100 Subject: [PATCH 03/55] refactored enum into seperate file --- code/planning/src/BehaviourEnum.py | 8 ++++++++ code/planning/src/local_planner/utils.py | 7 ------- 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 code/planning/src/BehaviourEnum.py diff --git a/code/planning/src/BehaviourEnum.py b/code/planning/src/BehaviourEnum.py new file mode 100644 index 00000000..361b0b43 --- /dev/null +++ b/code/planning/src/BehaviourEnum.py @@ -0,0 +1,8 @@ +from enum import IntEnum + +# getattr(Behaviour, "name").value returns the ID +# Behaviour(id) returns Behaviour.NAME + + +class BehaviourEnum(IntEnum): + CRUISING = 0 diff --git a/code/planning/src/local_planner/utils.py b/code/planning/src/local_planner/utils.py index 5d684df9..d976506d 100644 --- a/code/planning/src/local_planner/utils.py +++ b/code/planning/src/local_planner/utils.py @@ -3,7 +3,6 @@ import math import carla import os -from enum import IntEnum # import rospy @@ -24,12 +23,6 @@ EARTH_RADIUS_EQUA = 6378137.0 -# getattr(Behaviour, "name").value returns the ID -# Behaviour(id) returns Behaviour.NAME -class Behaviour(IntEnum): - CRUISING = 0 - - def get_distance(pos_1, pos_2): """Calculate the distance between two positions From 292075598cab544f4351a9865bcc9e9c80999b14 Mon Sep 17 00:00:00 2001 From: asamluka Date: Mon, 25 Nov 2024 04:37:02 +0100 Subject: [PATCH 04/55] Updated the architeture_current.md with the current state of the system. --- doc/assets/overview.jpg | Bin 113753 -> 61362 bytes doc/general/README.md | 3 +- doc/general/architecture_current.md | 348 ++++++++++++++++++ ...rchitecture.md => architecture_planned.md} | 67 ++-- 4 files changed, 392 insertions(+), 26 deletions(-) create mode 100644 doc/general/architecture_current.md rename doc/general/{architecture.md => architecture_planned.md} (87%) diff --git a/doc/assets/overview.jpg b/doc/assets/overview.jpg index 3bdc9225299eff033503f29dafce75defacaa958..7c6b3c76577a342fcb394619d1cce63e5df4c314 100644 GIT binary patch literal 61362 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 LQ6<4J|Gx4Z)Q9qdR;5D<{iH1q_5^bUgb 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]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

=o?G2~(w3V<2KCSoKiIngHchb08 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 E0f^link to ROS documentation of the message type) + +### Vision Node ([vision_node.py](/../paf/code/perception/src/vision_node.py)) + +Evaluates sensor data to detect and classify objects around the ego vehicle. +Other road users and objects blocking the vehicle's path are recognized (most of the time). +Recognized traffic lights get cut out from the image and made available for further processing. + +Subscriptions: +- ```/paf/hero/Center/dist_array``` \(/lidar_distance\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```/carla/hero/Center/image``` \(/carla_ros_bridge\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```/carla/hero/Back/image``` \(/carla_ros_bridge\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```/carla/hero/Left/image``` \(/carla_ros_bridge\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```/carla/hero/Right/image``` \(/carla_ros_bridge\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) + +Publishes: + +- ```/paf/hero/Center/segmented_image``` \(/rviz\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```/paf/hero/Back/segmented_image``` \(/rviz\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```/paf/hero/Left/segmented_image``` \(/rviz\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```/paf/hero/Right/segmented_image``` \(/rviz\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```/paf/hero/Center/object_distance``` \(/CollisionCheck\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) +- ```/paf/hero/Back/object_distance``` \(/CollisionCheck\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) +- ```/paf/hero/Left/object_distance``` \(/CollisionCheck\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) +- ```/paf/hero/Right/object_distance``` \(/CollisionCheck\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) +- ```/paf/hero/Center/segmented_traffic_light``` \(/TrafficLightNode\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```/paf/hero/Back/segmented_traffic_light``` \(/TrafficLightNode\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```/paf/hero/Left/segmented_traffic_light``` \(/TrafficLightNode\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```/paf/hero/Right/segmented_traffic_light``` \(/TrafficLightNode\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) + +### Traffic Light Detection ([traffic_light_node.py](/../paf/code/perception/src/traffic_light_node.py)) + +Recognizes traffic lights and what they are showing at the moment. +In particular traffic lights that are relevant for the correct traffic behavior of the ego vehicle, +are recognized early and reliably. + +Subscriptions: + +- ```/paf/hero/Center/segmented_traffic_light``` \(/VisionNode\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) + +Publishes: + +- ```/paf/hero/Center/traffic_light_state``` \(/behavior_agent\) ([perception/TrafficLightState](/../paf/code/perception/msg/TrafficLightState.msg)) +- ```/paf/hero/Center/traffic_light_y_distance``` \(/behavior_agent\) ([std_msgs/Int16](https://docs.ros.org/en/api/std_msgs/html/msg/Int16.html)) + + +### Position Heading Node ([position_heading_publisher_node.py](/../paf/code/perception/src/position_heading_publisher_node.py)) + +Calculates the current_pos (Location of the car) and current_heading (Orientation of the car around the Z- axis). + +Subscriptions: + +- ```/carla/hero/OpenDRIVE``` \(/carla_ros_bridge\) ([sensor_msgs/String](https://docs.ros.org/en/melodic/api/std_msgs/html/msg/String.html)) +- ```/carla/hero/IMU``` \(/carla_ros_bridge\) ([sensor_msgs/Imu](https://docs.ros.org/en/noetic/api/sensor_msgs/html/msg/Imu.html)) +- ```/carla/hero/GPS``` \(/carla_ros_bridge\) ([sensor_msgs/NavSatFix](https://docs.ros.org/en/noetic/api/sensor_msgs/html/msg/NavSatFix.html)) +- ```/paf/hero/kalman_pos``` \(/kalman_filter_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/paf/hero/kalman_heading``` \(/kalman_filter_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +Publishes: + +- ```/paf/hero/unfiltered_heading``` \(no subscriber at the moment\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/unfiltered_pos``` \(/kalman_filter_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/paf/hero/current_pos``` \(/pure_pursuit_controller, /stanley_controller, /MotionPlanning, /ACC, /MainFramePublisher, /GlobalPlanDistance, /position_heading_publisher_node, /curr_behavior \) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/paf/hero/current_heading``` \(/stanley_controller, /behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + + +### Global distances ([global_plan_distance_publisher.py](/../paf/code/perception/src/global_plan_distance_publisher.py)) + +Subscriptions: + +- ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/carla/hero/global_plan``` \(/ \) ([ros_carla_msgs/msg/CarlaRoute](https://github.com/carla-simulator/ros-carla-msgs/blob/leaderboard-2.0/msg/CarlaRoute.msg)) + +Publishes: + +- ```/paf/hero/waypoint_distance``` \(/MotionPlanning, /behavior_agent\) ([geographic_msgs/WayPoint](https://docs.ros.org/en/melodic/api/geographic_msgs/html/msg/WayPoint.html)) +- ```/paf/hero/lane_change_distance``` \(/MotionPlanning, /behavior_agent\) ([perception/msg/LaneChange](/../paf/code/perception/msg/LaneChange.msg)) + +### Kalman filtering ([kalman_filter.py](/../paf/code/perception/src/kalman_filter.py)) + +Subscriptions: + +- ```/carla/hero/OpenDRIVE``` \(/carla_ros_bridge\) ([sensor_msgs/String](https://docs.ros.org/en/melodic/api/std_msgs/html/msg/String.html)) +- ```/carla/hero/IMU``` \(/carla_ros_bridge\) ([sensor_msgs/Imu](https://docs.ros.org/en/noetic/api/sensor_msgs/html/msg/Imu.html)) +- ```/carla/hero/GPS``` \(/carla_ros_bridge\) ([sensor_msgs/NavSatFix](https://docs.ros.org/en/noetic/api/sensor_msgs/html/msg/NavSatFix.html)) +- ```/paf/hero/unfiltered_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) + +Publishes: + +- ```/paf/hero/kalman_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/paf/hero/kalman_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +### Localization + +Provides corrected accurate position, direction and speed of the ego vehicle. For this to be achived the [Position Heading Node](#position-heading-node-position_heading_publisher_nodepy), [Kalman filtering](#kalman-filtering) and the [Coordinate Transformation](/../paf/code/perception/src/coordinate_transformation.py) help class are used. + +## Planning + +The planning uses the data from the [Perception](#Perception) to find a path on which the ego vehicle can safely reach its destination. It also detects situations and reacts accordingly in traffic. It publishes signals such as a trajecotry or a target speed to acting. + +Further information regarding the planning can be found [here](../planning/README.md). +Research for the planning can be found [here](../research/planning/README.md). + +### PrePlaner ([global_planner.py](/../paf/code/planning/src/global_planner/global_planner.py)) + +Uses information from the map and the path specified by CARLA to find a first concrete path to the next intermediate point. More about it can be found [here](../planning/Global_Planner.md). + +Subscriptions: + +- ```/carla/hero/OpenDRIVE``` \(/carla_ros_bridge\) ([sensor_msgs/String](https://docs.ros.org/en/melodic/api/std_msgs/html/msg/String.html)) +- ```/carla/hero/global_plan``` \(/ \) ([ros_carla_msgs/msg/CarlaRoute](https://github.com/carla-simulator/ros-carla-msgs/blob/leaderboard-2.0/msg/CarlaRoute.msg)) +- ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) + +Publishes: + +- ```/paf/hero/trajectory_global``` \(/ACC, /MotionPlanning\) ([nav_msgs/Path](https://docs.ros.org/en/noetic/api/nav_msgs/html/msg/Path.html)) +- ```/paf/hero/speed_limits_OpenDrive``` \(/ACC\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) + +### Behavior Agent ([behavior_agent](/../paf/code/planning/src/behavior_agent/)) + +Decides which speed is the right one to pass through a certain situation and +also checks if an overtake is necessary. +Everything is based on the data from the Perception [Perception](#Perception). More about the behavior tree can be found [here](../planning/Behavior_tree.md) + +Subscriptions: + +[topics2blackboard.py](/../paf/code/planning/src/behavior_agent/behaviours/topics2blackboard.py) +- ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([ros_carla_msgs/msg/CarlaSpeedometer](https://github.com/carla-simulator/ros-carla-msgs/blob/leaderboard-2.0/msg/CarlaSpeedometer.msg)) +- ```/paf/hero/slowed_by_car_in_front``` \(/\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) +- ```/paf/hero/waypoint_distance``` \(/GlobalPlanDistance\) ([geographic_msgs/WayPoint](https://docs.ros.org/en/melodic/api/geographic_msgs/html/msg/WayPoint.html)) +- ```/paf/hero/stop_sign``` \(/\) ([mock/Stop_sign](/../paf/code/mock/msg/Stop_sign.msg)) +- ```/paf/hero/Center/traffic_light_state``` \(/TrafficLightNode\) ([perception/TrafficLightState](/../paf/code/perception/msg/TrafficLightState.msg)) +- ```/paf/hero/Center/traffic_light_y_distance``` \(/TrafficLightNode\) ([std_msgs/Int16](https://docs.ros.org/en/api/std_msgs/html/msg/Int16.html)) +- ```/paf/hero/max_velocity``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/speed_limit``` \(/ACC\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/lane_change_distance``` \(/GlobalPlanDistance\) ([perception/msg/LaneChange](/../paf/code/perception/msg/LaneChange.msg)) +- ```/paf/hero/collision``` \(/CollisionCheck\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) +- ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/overtake_success``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/oncoming``` \(/CollisionCheck\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/target_velocity``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +Publishes: + +[maneuvers.py](/../paf/code/planning/src/behavior_agent/behaviours/maneuvers.py) +- ```/paf/hero/curr_behavior``` \(/MotionPlanning, /vehicle_controller\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) +- ```/paf/hero/unstuck_distance``` \(/ACC, /MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/unstuck_flag``` \(/ACC\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) + +[intersection.py](/../paf/code/planning/src/behavior_agent/behaviours/intersection.py) +- ```/paf/hero/curr_behavior``` \(/MotionPlanning, /vehicle_controller\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) + +[lane_change.py](/../paf/code/planning/src/behavior_agent/behaviours/lane_change.py) +- ```/paf/hero/curr_behavior``` \(/MotionPlanning, /vehicle_controller\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) + +[meta.py](/../paf/code/planning/src/behavior_agent/behaviours/meta.py) +- ```/paf/hero/max_velocity``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +[overtake.py](/../paf/code/planning/src/behavior_agent/behaviours/overtake.py) +- ```/paf/hero/curr_behavior``` \(/MotionPlanning, /vehicle_controller\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) + + +### [Local Planning](../planning/Local_Planning.md) + +It consists of three components: + +- [Collision Check](../planning//Collision_Check.md): Checks for collisions based on objects recieved from [Perception](#perception) +- [ACC](../planning/ACC.md): Generates a new speed based on a possible collision recieved from Collision Check and speedlimits recieved from [Global Planner](#global-planning) +- [Motion Planning](../planning/motion_planning.md): Decides the target speed and modifies trajectory if signal recieved from [Decision Making](#decision-making) + +Subscriptions: + +[ACC.py](/../paf/code/planning/src/local_planner/ACC.py) +- ```/paf/hero/unstuck_flag``` \(/behavior_agent\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) +- ```/paf/hero/unstuck_distance``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([ros_carla_msgs/msg/CarlaSpeedometer](https://github.com/carla-simulator/ros-carla-msgs/blob/leaderboard-2.0/msg/CarlaSpeedometer.msg)) +- ```/paf/hero/speed_limits_OpenDrive``` \(/PrePlanner\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) +- ```/paf/hero/trajectory_global``` \(/PrePlanner\) ([nav_msgs/Path](https://docs.ros.org/en/noetic/api/nav_msgs/html/msg/Path.html)) +- ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/paf/hero/collision``` \(/CollisionCheck\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) + +[collision_check.py](/../paf/code/planning/src/local_planner/collision_check.py) +- ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([ros_carla_msgs/msg/CarlaSpeedometer](https://github.com/carla-simulator/ros-carla-msgs/blob/leaderboard-2.0/msg/CarlaSpeedometer.msg)) +- ```/paf/hero/Center/object_distance``` \(/CollisionCheck\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) + +[motion_planning.py](/../paf/code/planning/src/local_planner/motion_planning.py) +- ```/paf/hero/spawn_car``` \(/\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/speed_limit``` \(/ACC\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([ros_carla_msgs/msg/CarlaSpeedometer](https://github.com/carla-simulator/ros-carla-msgs/blob/leaderboard-2.0/msg/CarlaSpeedometer.msg)) +- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/trajectory_global``` \(/PrePlanner\) ([nav_msgs/Path](https://docs.ros.org/en/noetic/api/nav_msgs/html/msg/Path.html)) +- ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/paf/hero/curr_behavior``` \(/behavior_agent\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) +- ```/paf/hero/unchecked_emergency``` \(/\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) +- ```/paf/hero/acc_velocity``` \(/ACC\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/waypoint_distance``` \(/GlobalPlanDistance\) ([geographic_msgs/WayPoint](https://docs.ros.org/en/melodic/api/geographic_msgs/html/msg/WayPoint.html)) +- ```/paf/hero/lane_change_distance``` \(/GlobalPlanDistance\) ([perception/msg/LaneChange](/../paf/code/perception/msg/LaneChange.msg)) +- ```/paf/hero/collision``` \(/CollisionCheck\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) +- ```/paf/hero/Center/traffic_light_y_distance``` \(/TrafficLightNode\) ([std_msgs/Int16](https://docs.ros.org/en/api/std_msgs/html/msg/Int16.html)) +- ```/paf/hero/unstuck_distance``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/current_wp``` \(/ACC\) ([std_msgs/Float32](https://docs.ros.org/en/api/std_msgs/html/msg/Float32.html)) + +Publishes: +[ACC.py](/../paf/code/planning/src/behavior_agent/local_planner/ACC.py) +- ```/paf/hero/acc_velocity``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/current_wp``` ([std_msgs/Float32](https://docs.ros.org/en/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/speed_limit``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +[collision_check.py](/../paf/code/planning/src/local_planner/collision_check.py) +- ```/paf/hero/emergency``` \(/vehicle_controller\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) +- ```/paf/hero/collision``` \(/ACC, /MotionPlanning\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) +- ```/paf/hero/oncoming``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +[motion_planning.py](/../paf/code/planning/src/local_planner/motion_planning.py) +- ```/paf/hero/trajectory``` \(/pure_pursuit_controller\) ([nav_msgs/Path](https://docs.ros.org/en/noetic/api/nav_msgs/html/msg/Path.html)) +- ```/paf/hero/target_velocity``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/overtake_success``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + + + + + +## Acting + +The job of this component is to take the planned trajectory and target-velocities from the [Planning](#Planning) component and convert them into steering and throttle/brake controls for the CARLA-vehicle. + +All information regarding research done about acting can be found [here](../research/acting/README.md). + +Indepth information about the currently implemented acting Components can be found [here](../acting/README.md)! + +### [MainFramePublisher](/../paf/code/acting/src/acting/MainFramePublisher.py) + +Subscription: +- ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +### [pure_pursuit_controller](/../paf/code/acting/src/acting/pure_pursuit_controller.py) + +Calculates steering angles that keep the ego vehicle on the path given by +the [Local path planning](#Local-path-planning). + +Subscription: +- ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +Publishes: +- ```/paf/hero/pure_pursuit_steer``` \(/vehicle_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/pure_p_debug``` \(/\) ([acting/msg/Debug](/../paf/code/acting/msg/Debug.msg)) + +### [stanley_controller](/../paf/code/acting/src/acting/stanley_controller.py) + +Calculates steering angles that keep the ego vehicle on the path given by +the [Local path planning](#Local-path-planning). + +Subscription: +- ```/paf/hero/trajectory_global``` \(/PrePlanner\) ([nav_msgs/Path](https://docs.ros.org/en/noetic/api/nav_msgs/html/msg/Path.html)) +- ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +Publishes: +- ```/paf/hero/stanley_steer``` \(/vehicle_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/stanley_debug``` \(/\) ([acting/msg/StanleyDebug](/../paf/code/acting/msg/StanleyDebug.msg)) + +### [vehicle_controller](/../paf/code/acting/src/acting/vehicle_controller.py) + +Subscription: +- ```/paf/hero/curr_behavior``` \(/behavior_agent\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) +- ```/paf/hero/emergency``` \(/vehicle_controller\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) +- ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) +- ```/paf/hero/throttle``` \(/velocity_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/brake``` \(/velocity_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/reverse``` \(/velocity_controller\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) +- ```/paf/hero/pure_pursuit_steer``` \(/pure_pursuit_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/stanley_steer``` \(/pure_pursuit_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +Publishes: +- ```/carla/hero/vehicle_control_cmd``` \(/\) ([ros_carla_msgs/msg/CarlaEgoVehicleControl](https://github.com/carla-simulator/ros-carla-msgs/blob/master/msg/CarlaEgoVehicleControl.msg)) +- ```/carla/hero/status``` \(/\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) +- ```/paf/hero/controller``` \(/\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/emergency``` \(/vehicle_controller\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) + +### [velocity_controller](/../paf/code/acting/src/acting/velocity_controller.py) + +Calculates acceleration values to drive the target-velocity given by the [Local path planning](#Local-path-planning). + +For further indepth information about the currently implemented Vehicle Controller click [here](../acting/vehicle_controller.md) + +Subscription: +- ```/paf/hero/target_velocity``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([ros_carla_msgs/msg/CarlaSpeedometer](https://github.com/carla-simulator/ros-carla-msgs/blob/leaderboard-2.0/msg/CarlaSpeedometer.msg)) + +Publishes: +- ```/paf/hero/throttle``` \(/vehicle_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/brake``` \(/vehicle_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/reverse``` \(/vehicle_controller\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) diff --git a/doc/general/architecture.md b/doc/general/architecture_planned.md similarity index 87% rename from doc/general/architecture.md rename to doc/general/architecture_planned.md index f8eaf839..911392f8 100644 --- a/doc/general/architecture.md +++ b/doc/general/architecture_planned.md @@ -3,24 +3,25 @@ **Summary:** This page gives an overview over the planned general architecture of the vehicle agent. The document contains an overview over all [nodes](#overview) and [topics](#topics). -- [Planned architecture of vehicle agent](#planned-architecture-of-vehicle-agent) - - [Overview](#overview) - - [Perception](#perception) - - [Obstacle Detection and Classification](#obstacle-detection-and-classification) - - [Traffic Light Detection](#traffic-light-detection) - - [Localization](#localization) - - [Planning](#planning) - - [Global Planning](#global-planning) - - [Decision Making](#decision-making) - - [Local Planning](#local-planning) - - [Collision Check](#collision-check) - - [ACC](#acc) - - [Motion Planning](#motion-planning) - - [Acting](#acting) - - [Path following with Steering Controllers](#path-following-with-steering-controllers) - - [Velocity control](#velocity-control) - - [Vehicle controller](#vehicle-controller) - - [Visualization](#visualization) +- [Overview](#overview) +- [Perception](#perception) + - [Vision Node](#vision-node) + - [Traffic Light Detection](#traffic-light-detection) + - [Position Heading Node](#position-heading-node) + - [Distance to Objects](#distance-to-objects) + - [Localization](#localization) +- [Planning](#planning) + - [Global Planning](#global-planning) + - [Decision Making](#decision-making) + - [Local Planning](#local-planning) + - [Collision Check](#collision-check) + - [ACC](#acc) + - [Motion Planning](#motion-planning) +- [Acting](#acting) + - [Path following with Steering Controllers](#path-following-with-steering-controllers) + - [Velocity control](#velocity-control) + - [Vehicle controller](#vehicle-controller) +- [Visualization](#visualization) ## Overview @@ -28,12 +29,13 @@ The vehicle agent is split into three major components: [Perception](#Perception and [Acting](#Acting). A separate node is responsible for the [visualization](#Visualization). The topics published by the Carla bridge can be -found [here](https://carla.readthedocs.io/projects/ros-bridge/en/latest/ros_sensors/). -The msgs necessary to control the vehicle via the Carla bridge can be -found [here](https://carla.readthedocs.io/en/0.9.8/ros_msgs/#CarlaEgoVehicleControlmsg) +found [here](https://carla.readthedocs.io/projects/ros-bridge/en/latest/ros_sensors/).\ +The messages necessary to control the vehicle via the Carla bridge can be +found [here](https://carla.readthedocs.io/en/0.9.8/ros_msgs/#CarlaEgoVehicleControlmsg).\ -![Architecture overview](../assets/overview.jpg) The miro-board can be found [here](https://miro.com/welcomeonboard/a1F0d1dya2FneWNtbVk4cTBDU1NiN3RiZUIxdGhHNzJBdk5aS3N4VmdBM0R5c2Z1VXZIUUN4SkkwNHpuWlk2ZXwzNDU4NzY0NTMwNjYwNzAyODIzfDI=?share_link_id=785020837509). +![Architecture overview](../assets/overview.jpg "Connections between nodes visualized") +![Department node overview](../assets/research_assets/node_path_ros.png "In- and outgoing topics for every node of the departments") ## Perception @@ -43,7 +45,7 @@ environment representation that can be used by the [Planning](#Planning) for fur Further information regarding the perception can be found [here](../perception/README.md). Research for the perception can be found [here](../research/perception/README.md). -### Obstacle Detection and Classification +### Vision Node Evaluates sensor data to detect and classify objects around the ego vehicle. Other road users and objects blocking the vehicle's path are recognized. @@ -51,18 +53,28 @@ The node classifies objects into static and dynamic objects. In the case of dynamic objects, an attempt is made to recognize the direction and speed of movement. Subscriptions: - -- ```radar``` ([sensor_msgs/PointCloud2](https://docs.ros.org/en/api/sensor_msgs/html/msg/PointCloud2.html)) - ```lidar``` ([sensor_msgs/PointCloud2](https://docs.ros.org/en/api/sensor_msgs/html/msg/PointCloud2.html)) - ```rgb_camera``` ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) - ````gnss```` ([sensor_msgs/NavSatFix](https://carla.readthedocs.io/projects/ros-bridge/en/latest/ros_sensors/)) - ```map``` ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) +- ```radar``` ([sensor_msgs/PointCloud2](https://docs.ros.org/en/api/sensor_msgs/html/msg/PointCloud2.html)) SOON TO COME Publishes: - ```obstacles``` (Custom msg: obstacle ([vision_msgs/Detection3DArray Message](http://docs.ros.org/en/api/vision_msgs/html/msg/Detection3DArray.html)) and its classification ([std_msgs/String Message](http://docs.ros.org/en/noetic/api/std_msgs/html/msg/String.html))) +- ```segmented_image``` ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) + - /paf/hero/Center/segmented_image + - /paf/hero/Back/segmented_image + - /paf/hero/Left/segmented_image + - /paf/hero/Right/segmented_image +- ```segmented_traffic_light``` ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```object_distance``` ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) + - /paf/hero/Center/object_distance + - /paf/hero/Back/object_distance + - /paf/hero/Left/object_distance + - /paf/hero/Right/object_distance ### Traffic Light Detection @@ -84,6 +96,11 @@ Publishes: position ([geometry_msgs/Pose Message](http://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/Pose.html)), distance_to_stop_line ([std_msgs/Float64 Message](http://docs.ros.org/en/api/std_msgs/html/msg/Float64.html))) + +### Position Heading Node + +### Distance to Objects + ### Localization Provides corrected accurate position, direction and speed of the ego vehicle From 4245b707cb01c8546127b82e809f0540cdaf3df7 Mon Sep 17 00:00:00 2001 From: asamluka Date: Mon, 25 Nov 2024 07:56:46 +0100 Subject: [PATCH 05/55] Correct linting mistakes --- doc/general/architecture_current.md | 129 ++++++++++++++++------------ 1 file changed, 76 insertions(+), 53 deletions(-) diff --git a/doc/general/architecture_current.md b/doc/general/architecture_current.md index 3be7c975..f771d5a5 100644 --- a/doc/general/architecture_current.md +++ b/doc/general/architecture_current.md @@ -47,7 +47,8 @@ Further information regarding the perception can be found [here](../perception/R Research for the perception can be found [here](../research/perception/README.md). In the following, all subscribed and published topics and their corresponding nodes are listed with the format: -- ```name of topic``` \(origin/destination node\) (typo of message) (link to ROS documentation of the message type) + +- ```name of topic``` \(origin/destination node\) (typo of message) (link to ROS documentation of the message type) ### Vision Node ([vision_node.py](/../paf/code/perception/src/vision_node.py)) @@ -56,6 +57,7 @@ Other road users and objects blocking the vehicle's path are recognized (most of Recognized traffic lights get cut out from the image and made available for further processing. Subscriptions: + - ```/paf/hero/Center/dist_array``` \(/lidar_distance\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) - ```/carla/hero/Center/image``` \(/carla_ros_bridge\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) - ```/carla/hero/Back/image``` \(/carla_ros_bridge\) ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) @@ -92,7 +94,6 @@ Publishes: - ```/paf/hero/Center/traffic_light_state``` \(/behavior_agent\) ([perception/TrafficLightState](/../paf/code/perception/msg/TrafficLightState.msg)) - ```/paf/hero/Center/traffic_light_y_distance``` \(/behavior_agent\) ([std_msgs/Int16](https://docs.ros.org/en/api/std_msgs/html/msg/Int16.html)) - ### Position Heading Node ([position_heading_publisher_node.py](/../paf/code/perception/src/position_heading_publisher_node.py)) Calculates the current_pos (Location of the car) and current_heading (Orientation of the car around the Z- axis). @@ -107,11 +108,10 @@ Subscriptions: Publishes: -- ```/paf/hero/unfiltered_heading``` \(no subscriber at the moment\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/unfiltered_heading``` \(no subscriber at the moment\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - ```/paf/hero/unfiltered_pos``` \(/kalman_filter_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) - ```/paf/hero/current_pos``` \(/pure_pursuit_controller, /stanley_controller, /MotionPlanning, /ACC, /MainFramePublisher, /GlobalPlanDistance, /position_heading_publisher_node, /curr_behavior \) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) -- ```/paf/hero/current_heading``` \(/stanley_controller, /behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - +- ```/paf/hero/current_heading``` \(/stanley_controller, /behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) ### Global distances ([global_plan_distance_publisher.py](/../paf/code/perception/src/global_plan_distance_publisher.py)) @@ -122,8 +122,8 @@ Subscriptions: Publishes: -- ```/paf/hero/waypoint_distance``` \(/MotionPlanning, /behavior_agent\) ([geographic_msgs/WayPoint](https://docs.ros.org/en/melodic/api/geographic_msgs/html/msg/WayPoint.html)) -- ```/paf/hero/lane_change_distance``` \(/MotionPlanning, /behavior_agent\) ([perception/msg/LaneChange](/../paf/code/perception/msg/LaneChange.msg)) +- ```/paf/hero/waypoint_distance``` \(/MotionPlanning, /behavior_agent\) ([geographic_msgs/WayPoint](https://docs.ros.org/en/melodic/api/geographic_msgs/html/msg/WayPoint.html)) +- ```/paf/hero/lane_change_distance``` \(/MotionPlanning, /behavior_agent\) ([perception/msg/LaneChange](/../paf/code/perception/msg/LaneChange.msg)) ### Kalman filtering ([kalman_filter.py](/../paf/code/perception/src/kalman_filter.py)) @@ -138,11 +138,17 @@ Subscriptions: Publishes: - ```/paf/hero/kalman_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) -- ```/paf/hero/kalman_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/kalman_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) ### Localization -Provides corrected accurate position, direction and speed of the ego vehicle. For this to be achived the [Position Heading Node](#position-heading-node-position_heading_publisher_nodepy), [Kalman filtering](#kalman-filtering) and the [Coordinate Transformation](/../paf/code/perception/src/coordinate_transformation.py) help class are used. +Provides corrected accurate position, direction and speed of the ego vehicle. For this to be achived the + +- [Position Heading Node](#position-heading-node-position_heading_publisher_nodepy) +- [Kalman filtering](#kalman-filtering) and +- [Coordinate Transformation](/../paf/code/perception/src/coordinate_transformation.py) + +classes are used. ## Planning @@ -175,41 +181,46 @@ Everything is based on the data from the Perception [Perception](#Perception). M Subscriptions: [topics2blackboard.py](/../paf/code/planning/src/behavior_agent/behaviours/topics2blackboard.py) + - ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([ros_carla_msgs/msg/CarlaSpeedometer](https://github.com/carla-simulator/ros-carla-msgs/blob/leaderboard-2.0/msg/CarlaSpeedometer.msg)) - ```/paf/hero/slowed_by_car_in_front``` \(/\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) -- ```/paf/hero/waypoint_distance``` \(/GlobalPlanDistance\) ([geographic_msgs/WayPoint](https://docs.ros.org/en/melodic/api/geographic_msgs/html/msg/WayPoint.html)) -- ```/paf/hero/stop_sign``` \(/\) ([mock/Stop_sign](/../paf/code/mock/msg/Stop_sign.msg)) +- ```/paf/hero/waypoint_distance``` \(/GlobalPlanDistance\) ([geographic_msgs/WayPoint](https://docs.ros.org/en/melodic/api/geographic_msgs/html/msg/WayPoint.html)) +- ```/paf/hero/stop_sign``` \(/\) ([mock/Stop_sign](/../paf/code/mock/msg/Stop_sign.msg)) - ```/paf/hero/Center/traffic_light_state``` \(/TrafficLightNode\) ([perception/TrafficLightState](/../paf/code/perception/msg/TrafficLightState.msg)) - ```/paf/hero/Center/traffic_light_y_distance``` \(/TrafficLightNode\) ([std_msgs/Int16](https://docs.ros.org/en/api/std_msgs/html/msg/Int16.html)) -- ```/paf/hero/max_velocity``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/speed_limit``` \(/ACC\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/lane_change_distance``` \(/GlobalPlanDistance\) ([perception/msg/LaneChange](/../paf/code/perception/msg/LaneChange.msg)) +- ```/paf/hero/max_velocity``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/speed_limit``` \(/ACC\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/lane_change_distance``` \(/GlobalPlanDistance\) ([perception/msg/LaneChange](/../paf/code/perception/msg/LaneChange.msg)) - ```/paf/hero/collision``` \(/CollisionCheck\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) - ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) -- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/overtake_success``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/oncoming``` \(/CollisionCheck\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/target_velocity``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/overtake_success``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/oncoming``` \(/CollisionCheck\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/target_velocity``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) Publishes: [maneuvers.py](/../paf/code/planning/src/behavior_agent/behaviours/maneuvers.py) + - ```/paf/hero/curr_behavior``` \(/MotionPlanning, /vehicle_controller\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) -- ```/paf/hero/unstuck_distance``` \(/ACC, /MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/unstuck_distance``` \(/ACC, /MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - ```/paf/hero/unstuck_flag``` \(/ACC\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) [intersection.py](/../paf/code/planning/src/behavior_agent/behaviours/intersection.py) + - ```/paf/hero/curr_behavior``` \(/MotionPlanning, /vehicle_controller\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) [lane_change.py](/../paf/code/planning/src/behavior_agent/behaviours/lane_change.py) + - ```/paf/hero/curr_behavior``` \(/MotionPlanning, /vehicle_controller\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) [meta.py](/../paf/code/planning/src/behavior_agent/behaviours/meta.py) -- ```/paf/hero/max_velocity``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +- ```/paf/hero/max_velocity``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) [overtake.py](/../paf/code/planning/src/behavior_agent/behaviours/overtake.py) -- ```/paf/hero/curr_behavior``` \(/MotionPlanning, /vehicle_controller\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) +- ```/paf/hero/curr_behavior``` \(/MotionPlanning, /vehicle_controller\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) ### [Local Planning](../planning/Local_Planning.md) @@ -222,8 +233,9 @@ It consists of three components: Subscriptions: [ACC.py](/../paf/code/planning/src/local_planner/ACC.py) + - ```/paf/hero/unstuck_flag``` \(/behavior_agent\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) -- ```/paf/hero/unstuck_distance``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/unstuck_distance``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([ros_carla_msgs/msg/CarlaSpeedometer](https://github.com/carla-simulator/ros-carla-msgs/blob/leaderboard-2.0/msg/CarlaSpeedometer.msg)) - ```/paf/hero/speed_limits_OpenDrive``` \(/PrePlanner\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) - ```/paf/hero/trajectory_global``` \(/PrePlanner\) ([nav_msgs/Path](https://docs.ros.org/en/noetic/api/nav_msgs/html/msg/Path.html)) @@ -231,45 +243,47 @@ Subscriptions: - ```/paf/hero/collision``` \(/CollisionCheck\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) [collision_check.py](/../paf/code/planning/src/local_planner/collision_check.py) + - ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([ros_carla_msgs/msg/CarlaSpeedometer](https://github.com/carla-simulator/ros-carla-msgs/blob/leaderboard-2.0/msg/CarlaSpeedometer.msg)) - ```/paf/hero/Center/object_distance``` \(/CollisionCheck\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) [motion_planning.py](/../paf/code/planning/src/local_planner/motion_planning.py) -- ```/paf/hero/spawn_car``` \(/\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/speed_limit``` \(/ACC\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +- ```/paf/hero/spawn_car``` \(/\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/speed_limit``` \(/ACC\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([ros_carla_msgs/msg/CarlaSpeedometer](https://github.com/carla-simulator/ros-carla-msgs/blob/leaderboard-2.0/msg/CarlaSpeedometer.msg)) -- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - ```/paf/hero/trajectory_global``` \(/PrePlanner\) ([nav_msgs/Path](https://docs.ros.org/en/noetic/api/nav_msgs/html/msg/Path.html)) - ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) - ```/paf/hero/curr_behavior``` \(/behavior_agent\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) - ```/paf/hero/unchecked_emergency``` \(/\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) -- ```/paf/hero/acc_velocity``` \(/ACC\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/waypoint_distance``` \(/GlobalPlanDistance\) ([geographic_msgs/WayPoint](https://docs.ros.org/en/melodic/api/geographic_msgs/html/msg/WayPoint.html)) -- ```/paf/hero/lane_change_distance``` \(/GlobalPlanDistance\) ([perception/msg/LaneChange](/../paf/code/perception/msg/LaneChange.msg)) +- ```/paf/hero/acc_velocity``` \(/ACC\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/waypoint_distance``` \(/GlobalPlanDistance\) ([geographic_msgs/WayPoint](https://docs.ros.org/en/melodic/api/geographic_msgs/html/msg/WayPoint.html)) +- ```/paf/hero/lane_change_distance``` \(/GlobalPlanDistance\) ([perception/msg/LaneChange](/../paf/code/perception/msg/LaneChange.msg)) - ```/paf/hero/collision``` \(/CollisionCheck\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) - ```/paf/hero/Center/traffic_light_y_distance``` \(/TrafficLightNode\) ([std_msgs/Int16](https://docs.ros.org/en/api/std_msgs/html/msg/Int16.html)) -- ```/paf/hero/unstuck_distance``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/unstuck_distance``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - ```/paf/hero/current_wp``` \(/ACC\) ([std_msgs/Float32](https://docs.ros.org/en/api/std_msgs/html/msg/Float32.html)) Publishes: + [ACC.py](/../paf/code/planning/src/behavior_agent/local_planner/ACC.py) -- ```/paf/hero/acc_velocity``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +- ```/paf/hero/acc_velocity``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - ```/paf/hero/current_wp``` ([std_msgs/Float32](https://docs.ros.org/en/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/speed_limit``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/speed_limit``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) [collision_check.py](/../paf/code/planning/src/local_planner/collision_check.py) + - ```/paf/hero/emergency``` \(/vehicle_controller\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) - ```/paf/hero/collision``` \(/ACC, /MotionPlanning\) ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) -- ```/paf/hero/oncoming``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/oncoming``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) [motion_planning.py](/../paf/code/planning/src/local_planner/motion_planning.py) -- ```/paf/hero/trajectory``` \(/pure_pursuit_controller\) ([nav_msgs/Path](https://docs.ros.org/en/noetic/api/nav_msgs/html/msg/Path.html)) -- ```/paf/hero/target_velocity``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/overtake_success``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - - - +- ```/paf/hero/trajectory``` \(/pure_pursuit_controller\) ([nav_msgs/Path](https://docs.ros.org/en/noetic/api/nav_msgs/html/msg/Path.html)) +- ```/paf/hero/target_velocity``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/overtake_success``` \(/behavior_agent\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) ## Acting @@ -282,8 +296,9 @@ Indepth information about the currently implemented acting Components can be fou ### [MainFramePublisher](/../paf/code/acting/src/acting/MainFramePublisher.py) Subscription: + - ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) -- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) ### [pure_pursuit_controller](/../paf/code/acting/src/acting/pure_pursuit_controller.py) @@ -291,13 +306,15 @@ Calculates steering angles that keep the ego vehicle on the path given by the [Local path planning](#Local-path-planning). Subscription: + - ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) - ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) -- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) Publishes: -- ```/paf/hero/pure_pursuit_steer``` \(/vehicle_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/pure_p_debug``` \(/\) ([acting/msg/Debug](/../paf/code/acting/msg/Debug.msg)) + +- ```/paf/hero/pure_pursuit_steer``` \(/vehicle_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/pure_p_debug``` \(/\) ([acting/msg/Debug](/../paf/code/acting/msg/Debug.msg)) ### [stanley_controller](/../paf/code/acting/src/acting/stanley_controller.py) @@ -305,31 +322,35 @@ Calculates steering angles that keep the ego vehicle on the path given by the [Local path planning](#Local-path-planning). Subscription: + - ```/paf/hero/trajectory_global``` \(/PrePlanner\) ([nav_msgs/Path](https://docs.ros.org/en/noetic/api/nav_msgs/html/msg/Path.html)) - ```/paf/hero/current_pos``` \(/position_heading_publisher_node\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) - ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) -- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/current_heading``` \(/position_heading_publisher_node\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) Publishes: -- ```/paf/hero/stanley_steer``` \(/vehicle_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/stanley_debug``` \(/\) ([acting/msg/StanleyDebug](/../paf/code/acting/msg/StanleyDebug.msg)) + +- ```/paf/hero/stanley_steer``` \(/vehicle_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/stanley_debug``` \(/\) ([acting/msg/StanleyDebug](/../paf/code/acting/msg/StanleyDebug.msg)) ### [vehicle_controller](/../paf/code/acting/src/acting/vehicle_controller.py) Subscription: + - ```/paf/hero/curr_behavior``` \(/behavior_agent\) ([std_msgs/String](https://docs.ros.org/en/api/std_msgs/html/msg/String.html)) - ```/paf/hero/emergency``` \(/vehicle_controller\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) - ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([geometry_msgs/PoseStamped](https://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/PoseStamped.html)) -- ```/paf/hero/throttle``` \(/velocity_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/brake``` \(/velocity_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/throttle``` \(/velocity_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/brake``` \(/velocity_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - ```/paf/hero/reverse``` \(/velocity_controller\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) -- ```/paf/hero/pure_pursuit_steer``` \(/pure_pursuit_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/stanley_steer``` \(/pure_pursuit_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/pure_pursuit_steer``` \(/pure_pursuit_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/stanley_steer``` \(/pure_pursuit_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) Publishes: + - ```/carla/hero/vehicle_control_cmd``` \(/\) ([ros_carla_msgs/msg/CarlaEgoVehicleControl](https://github.com/carla-simulator/ros-carla-msgs/blob/master/msg/CarlaEgoVehicleControl.msg)) - ```/carla/hero/status``` \(/\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) -- ```/paf/hero/controller``` \(/\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/controller``` \(/\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - ```/paf/hero/emergency``` \(/vehicle_controller\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) ### [velocity_controller](/../paf/code/acting/src/acting/velocity_controller.py) @@ -339,10 +360,12 @@ Calculates acceleration values to drive the target-velocity given by the [Local For further indepth information about the currently implemented Vehicle Controller click [here](../acting/vehicle_controller.md) Subscription: -- ```/paf/hero/target_velocity``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +- ```/paf/hero/target_velocity``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - ```/carla/hero/Speed``` \(/carla_ros_bridge\) ([ros_carla_msgs/msg/CarlaSpeedometer](https://github.com/carla-simulator/ros-carla-msgs/blob/leaderboard-2.0/msg/CarlaSpeedometer.msg)) Publishes: -- ```/paf/hero/throttle``` \(/vehicle_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) -- ```/paf/hero/brake``` \(/vehicle_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) + +- ```/paf/hero/throttle``` \(/vehicle_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) +- ```/paf/hero/brake``` \(/vehicle_controller\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - ```/paf/hero/reverse``` \(/vehicle_controller\) ([std_msg/Bool](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Bool.html)) From 17d9f9efabfc1ab6eb123be0bec566e1f2d90906 Mon Sep 17 00:00:00 2001 From: asamluka Date: Mon, 25 Nov 2024 08:01:07 +0100 Subject: [PATCH 06/55] More linting fixes --- doc/general/architecture_planned.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/doc/general/architecture_planned.md b/doc/general/architecture_planned.md index 911392f8..1d6fd803 100644 --- a/doc/general/architecture_planned.md +++ b/doc/general/architecture_planned.md @@ -53,6 +53,7 @@ The node classifies objects into static and dynamic objects. In the case of dynamic objects, an attempt is made to recognize the direction and speed of movement. Subscriptions: + - ```lidar``` ([sensor_msgs/PointCloud2](https://docs.ros.org/en/api/sensor_msgs/html/msg/PointCloud2.html)) - ```rgb_camera``` ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) - ````gnss```` ([sensor_msgs/NavSatFix](https://carla.readthedocs.io/projects/ros-bridge/en/latest/ros_sensors/)) @@ -64,17 +65,17 @@ Publishes: - ```obstacles``` (Custom msg: obstacle ([vision_msgs/Detection3DArray Message](http://docs.ros.org/en/api/vision_msgs/html/msg/Detection3DArray.html)) and its classification ([std_msgs/String Message](http://docs.ros.org/en/noetic/api/std_msgs/html/msg/String.html))) -- ```segmented_image``` ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) - - /paf/hero/Center/segmented_image - - /paf/hero/Back/segmented_image - - /paf/hero/Left/segmented_image - - /paf/hero/Right/segmented_image -- ```segmented_traffic_light``` ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) -- ```object_distance``` ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) - - /paf/hero/Center/object_distance - - /paf/hero/Back/object_distance - - /paf/hero/Left/object_distance - - /paf/hero/Right/object_distance +- ```segmented_image``` ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) + - /paf/hero/Center/segmented_image + - /paf/hero/Back/segmented_image + - /paf/hero/Left/segmented_image + - /paf/hero/Right/segmented_image +- ```segmented_traffic_light``` ([sensor_msgs/Image](https://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html)) +- ```object_distance``` ([sensor_msgs/Float32MultiArray](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32MultiArray.html)) + - /paf/hero/Center/object_distance + - /paf/hero/Back/object_distance + - /paf/hero/Left/object_distance + - /paf/hero/Right/object_distance ### Traffic Light Detection @@ -96,11 +97,14 @@ Publishes: position ([geometry_msgs/Pose Message](http://docs.ros.org/en/noetic/api/geometry_msgs/html/msg/Pose.html)), distance_to_stop_line ([std_msgs/Float64 Message](http://docs.ros.org/en/api/std_msgs/html/msg/Float64.html))) - ### Position Heading Node +There are currently no planned improvements of the Position Heading Node. + ### Distance to Objects +There are currently no planned improvements for Distance to Objects. + ### Localization Provides corrected accurate position, direction and speed of the ego vehicle From 5083db9c81d3a34686cb087d52c4448cd05a3bb5 Mon Sep 17 00:00:00 2001 From: asamluka Date: Mon, 25 Nov 2024 12:55:46 +0100 Subject: [PATCH 07/55] fixing broken or wrong references --- doc/general/architecture_current.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/general/architecture_current.md b/doc/general/architecture_current.md index f771d5a5..aa0a472a 100644 --- a/doc/general/architecture_current.md +++ b/doc/general/architecture_current.md @@ -44,7 +44,7 @@ environment representation that can be used by the [Planning](#Planning) for fur It provides localization of the agent, filtering noise from sensor data, preconditions images for further usage in other nodes and detects certain points of interest (at the moment only traffic lights). Further information regarding the perception can be found [here](../perception/README.md). -Research for the perception can be found [here](../research/perception/README.md). +Research for the perception can be found [here](../research/paf24/perception/). In the following, all subscribed and published topics and their corresponding nodes are listed with the format: @@ -228,7 +228,7 @@ It consists of three components: - [Collision Check](../planning//Collision_Check.md): Checks for collisions based on objects recieved from [Perception](#perception) - [ACC](../planning/ACC.md): Generates a new speed based on a possible collision recieved from Collision Check and speedlimits recieved from [Global Planner](#global-planning) -- [Motion Planning](../planning/motion_planning.md): Decides the target speed and modifies trajectory if signal recieved from [Decision Making](#decision-making) +- [Motion Planning](../planning/motion_planning.md): Decides the target speed and modifies trajectory if signal recieved from [Behavior Agent](#behavior-agent-behavior_agent) Subscriptions: @@ -267,7 +267,7 @@ Subscriptions: Publishes: -[ACC.py](/../paf/code/planning/src/behavior_agent/local_planner/ACC.py) +[ACC.py](/../paf/code/planning/src/local_planner/ACC.py) - ```/paf/hero/acc_velocity``` \(/MotionPlanning\) ([std_msgs/Float32](https://docs.ros.org/en/noetic/api/std_msgs/html/msg/Float32.html)) - ```/paf/hero/current_wp``` ([std_msgs/Float32](https://docs.ros.org/en/api/std_msgs/html/msg/Float32.html)) From c9dde1f55d5b843b543c902b4b7f4737749c4488 Mon Sep 17 00:00:00 2001 From: SirMDA Date: Tue, 26 Nov 2024 18:19:12 +0100 Subject: [PATCH 08/55] added yolov11 --- code/perception/launch/perception.launch | 11 +- code/perception/src/vision_node.py | 161 ++++++++++++---------- code/perception/src/vision_node_helper.py | 126 +++++++++++++++++ code/requirements.txt | 2 +- 4 files changed, 225 insertions(+), 75 deletions(-) create mode 100644 code/perception/src/vision_node_helper.py diff --git a/code/perception/launch/perception.launch b/code/perception/launch/perception.launch index 67a9c2ab..7059f24f 100644 --- a/code/perception/launch/perception.launch +++ b/code/perception/launch/perception.launch @@ -32,8 +32,7 @@ --> - - + @@ -58,10 +57,14 @@ Image-Segmentation: - deeplabv3_resnet101 - - yolov8x-seg + - yolov8x-seg + - yolo11n-seg + - yolo11s-seg + - yolo11m-seg + - yolo11l-seg --> - + diff --git a/code/perception/src/vision_node.py b/code/perception/src/vision_node.py index 7039b9f3..9df05e27 100755 --- a/code/perception/src/vision_node.py +++ b/code/perception/src/vision_node.py @@ -15,6 +15,7 @@ ) import torchvision.transforms as t import cv2 +from perception.src.vision_node_helper import get_carla_class_name, get_carla_color from rospy.numpy_msg import numpy_msg from sensor_msgs.msg import Image as ImageMsg from std_msgs.msg import Header, Float32MultiArray @@ -24,6 +25,7 @@ from ultralytics import NAS, YOLO, RTDETR, SAM, FastSAM import asyncio import rospy +from ultralytics.utils.ops import scale_masks class VisionNode(CompatibleNode): @@ -77,6 +79,10 @@ def __init__(self, name, **kwargs): "yolov8x-seg": (YOLO, "yolov8x-seg.pt", "segmentation", "ultralytics"), "sam_l": (SAM, "sam_l.pt", "detection", "ultralytics"), "FastSAM-x": (FastSAM, "FastSAM-x.pt", "detection", "ultralytics"), + "yolo11n-seg": (YOLO, "yolo11n-seg.pt", "segmentation", "ultralytics"), + "yolo11s-seg": (YOLO, "yolo11s-seg.pt", "segmentation", "ultralytics"), + "yolo11m-seg": (YOLO, "yolo11m-seg.pt", "segmentation", "ultralytics"), + "yolo11l-seg": (YOLO, "yolo11l-seg.pt", "segmentation", "ultralytics"), } # general setup @@ -239,7 +245,7 @@ def handle_camera_image(self, image): vision_result = self.predict_ultralytics(image) # publish vision result to rviz - img_msg = self.bridge.cv2_to_imgmsg(vision_result, encoding="rgb8") + img_msg = self.bridge.cv2_to_imgmsg(vision_result, encoding="bgr8") img_msg.header = image.header # publish img to corresponding angle topic @@ -341,7 +347,7 @@ def predict_ultralytics(self, image): cv_image = cv2.cvtColor(cv_image, cv2.COLOR_RGB2BGR) # run model prediction - output = self.model(cv_image, half=True, verbose=False) + output = self.model.track(cv_image, half=True, verbose=False, imgsz=640) # handle distance of objects @@ -349,81 +355,86 @@ def predict_ultralytics(self, image): distance_output = [] c_boxes = [] c_labels = [] + c_colors = [] + masks = output[0].masks.data boxes = output[0].boxes - for box in boxes: - cls = box.cls.item() # class index of object - pixels = box.xyxy[0] # upper left and lower right pixel coords + for i, box_image in enumerate(boxes): + cls = box_image.cls.item() # class index of object + pixels = box_image.xyxy[0] # upper left and lower right pixel coords # only run distance calc when dist_array is available # this if is needed because the lidar starts # publishing with a delay - if self.dist_arrays is not None: - - # crop bounding box area out of depth image - distances = np.asarray( - self.dist_arrays[ - int(pixels[1]) : int(pixels[3]) : 1, - int(pixels[0]) : int(pixels[2]) : 1, - ::, - ] - ) + if self.dist_arrays is None: + continue + + # crop bounding box area out of depth image + distances = np.asarray( + self.dist_arrays[ + int(pixels[1]) : int(pixels[3]) : 1, + int(pixels[0]) : int(pixels[2]) : 1, + ::, + ] + ) - # set all 0 (black) values to np.inf (necessary if - # you want to search for minimum) - # these are all pixels where there is no - # corresponding lidar point in the depth image - condition = distances[:, :, 0] != 0 - non_zero_filter = distances[condition] - distances_copy = distances.copy() - distances_copy[distances_copy == 0] = np.inf - - # only proceed if there is more than one lidar - # point in the bounding box - if len(non_zero_filter) > 0: - - """ - !Watch out: - The calculation of min x and min abs y is currently - only for center angle - For back, left and right the values are different in the - coordinate system of the lidar. - (Example: the closedt distance on the back view should the - max x since the back view is on the -x axis) - """ - - # copy actual lidar points - obj_dist_min_x = self.min_x(dist_array=distances_copy) - obj_dist_min_abs_y = self.min_abs_y(dist_array=distances_copy) - - # absolut distance to object for visualization - abs_distance = np.sqrt( - obj_dist_min_x[0] ** 2 - + obj_dist_min_x[1] ** 2 - + obj_dist_min_x[2] ** 2 - ) - - # append class index, min x and min abs y to output array - distance_output.append(float(cls)) - distance_output.append(float(obj_dist_min_x[0])) - distance_output.append(float(obj_dist_min_abs_y[1])) - - else: - # fallback values for bounding box if - # no lidar points where found - obj_dist_min_x = (np.inf, np.inf, np.inf) - obj_dist_min_abs_y = (np.inf, np.inf, np.inf) - abs_distance = np.inf - - # add values for visualization - c_boxes.append(torch.tensor(pixels)) - c_labels.append( - f"Class: {cls}," - f"Meters: {round(abs_distance, 2)}," - f"({round(float(obj_dist_min_x[0]), 2)}," - f"{round(float(obj_dist_min_abs_y[1]), 2)})" + # set all 0 (black) values to np.inf (necessary if + # you want to search for minimum) + # these are all pixels where there is no + # corresponding lidar point in the depth image + condition = distances[:, :, 0] != 0 + non_zero_filter = distances[condition] + distances_copy = distances.copy() + distances_copy[distances_copy == 0] = np.inf + + # only proceed if there is more than one lidar + # point in the bounding box + if len(non_zero_filter) > 0: + + """ + !Watch out: + The calculation of min x and min abs y is currently + only for center angle + For back, left and right the values are different in the + coordinate system of the lidar. + (Example: the closedt distance on the back view should the + max x since the back view is on the -x axis) + """ + + # copy actual lidar points + obj_dist_min_x = self.min_x(dist_array=distances_copy) + obj_dist_min_abs_y = self.min_abs_y(dist_array=distances_copy) + + # absolut distance to object for visualization + abs_distance = np.sqrt( + obj_dist_min_x[0] ** 2 + + obj_dist_min_x[1] ** 2 + + obj_dist_min_x[2] ** 2 ) + # append class index, min x and min abs y to output array + distance_output.append(float(cls)) + distance_output.append(float(obj_dist_min_x[0])) + distance_output.append(float(obj_dist_min_abs_y[1])) + + else: + # fallback values for bounding box if + # no lidar points where found + obj_dist_min_x = (np.inf, np.inf, np.inf) + obj_dist_min_abs_y = (np.inf, np.inf, np.inf) + abs_distance = np.inf + + # add values for visualization + c_boxes.append(torch.tensor(pixels)) + c_labels.append( + f"Class: {get_carla_class_name(cls)}, " + f"Meters: {round(abs_distance, 2)}, " + f"TrackingId: {int(box_image.id)}, " + f"({round(float(obj_dist_min_x[0]), 2)}, " + f"{round(float(obj_dist_min_abs_y[1]), 2)})", + ) + c_colors.append(get_carla_color(int(cls))) + # publish list of distances of objects for planning self.distance_publisher.publish(Float32MultiArray(data=distance_output)) @@ -437,7 +448,7 @@ def predict_ultralytics(self, image): # draw bounding boxes and distance values on image c_boxes = torch.stack(c_boxes) - box = draw_bounding_boxes( + box_image = draw_bounding_boxes( image_np_with_detections, c_boxes, c_labels, @@ -445,7 +456,17 @@ def predict_ultralytics(self, image): width=3, font_size=12, ) - np_box_img = np.transpose(box.detach().numpy(), (1, 2, 0)) + np_box_img = np.transpose(box_image.detach().numpy(), (1, 2, 0)) + + scaled_masks = np.squeeze( + scale_masks(masks.unsqueeze(1), cv_image.shape[:2], True).cpu().numpy(), 1 + ) + + mask_image = draw_segmentation_masks( + box_image, torch.from_numpy(scaled_masks > 0), alpha=0.6, colors=c_colors + ) + np_box_img = np.transpose(mask_image.detach().numpy(), (1, 2, 0)) + box_img = cv2.cvtColor(np_box_img, cv2.COLOR_BGR2RGB) return box_img diff --git a/code/perception/src/vision_node_helper.py b/code/perception/src/vision_node_helper.py new file mode 100644 index 00000000..0ea33d95 --- /dev/null +++ b/code/perception/src/vision_node_helper.py @@ -0,0 +1,126 @@ +# Carla-Farben +carla_colors = [ + [0, 0, 0], # 0: None + [70, 70, 70], # 1: Buildings + [190, 153, 153], # 2: Fences + [72, 0, 90], # 3: Other + [220, 20, 60], # 4: Pedestrians + [153, 153, 153], # 5: Poles + [157, 234, 50], # 6: RoadLines + [128, 64, 128], # 7: Roads + [244, 35, 232], # 8: Sidewalks + [107, 142, 35], # 9: Vegetation + [0, 0, 255], # 10: Vehicles + [102, 102, 156], # 11: Walls + [220, 220, 0], # 12: TrafficSigns +] + +carla_class_names = [ + "None", # 0 + "Buildings", # 1 + "Fences", # 2 + "Other", # 3 + "Pedestrians", # 4 + "Poles", # 5 + "RoadLines", # 6 + "Roads", # 7 + "Sidewalks", # 8 + "Vegetation", # 9 + "Vehicles", # 10 + "Walls", # 11 + "TrafficSigns", # 12 +] + +# COCO-Klassen → Carla-Klassen Mapping +coco_to_carla = [ + 4, # 0: Person -> Pedestrians + 10, # 1: Bicycle -> Vehicles + 10, # 2: Car -> Vehicles + 10, # 3: Motorbike -> Vehicles + 10, # 4: Airplane -> Vehicles + 10, # 5: Bus -> Vehicles + 10, # 6: Train -> Vehicles + 10, # 7: Truck -> Vehicles + 10, # 8: Boat -> Vehicles + 12, # 9: Traffic Light -> TrafficSigns + 3, # 10: Fire Hydrant -> Other + 12, # 11: Stop Sign -> TrafficSigns + 3, # 12: Parking Meter -> Other + 3, # 13: Bench -> Other + 3, # 14: Bird -> Other + 3, # 15: Cat -> Other + 3, # 16: Dog -> Other + 3, # 17: Horse -> Other + 3, # 18: Sheep -> Other + 3, # 19: Cow -> Other + 3, # 20: Elephant -> Other + 3, # 21: Bear -> Other + 3, # 22: Zebra -> Other + 3, # 23: Giraffe -> Other + 3, # 24: Backpack -> Other + 3, # 25: Umbrella -> Other + 3, # 26: Handbag -> Other + 3, # 27: Tie -> Other + 3, # 28: Suitcase -> Other + 3, # 29: Frisbee -> Other + 3, # 30: Skis -> Other + 3, # 31: Snowboard -> Other + 3, # 32: Sports Ball -> Other + 3, # 33: Kite -> Other + 3, # 34: Baseball Bat -> Other + 3, # 35: Baseball Glove -> Other + 3, # 36: Skateboard -> Other + 3, # 37: Surfboard -> Other + 3, # 38: Tennis Racket -> Other + 3, # 39: Bottle -> Other + 3, # 40: Wine Glass -> Other + 3, # 41: Cup -> Other + 3, # 42: Fork -> Other + 3, # 43: Knife -> Other + 3, # 44: Spoon -> Other + 3, # 45: Bowl -> Other + 3, # 46: Banana -> Other + 3, # 47: Apple -> Other + 3, # 48: Sandwich -> Other + 3, # 49: Orange -> Other + 3, # 50: Broccoli -> Other + 3, # 51: Carrot -> Other + 3, # 52: Hot Dog -> Other + 3, # 53: Pizza -> Other + 3, # 54: Donut -> Other + 3, # 55: Cake -> Other + 3, # 56: Chair -> Other + 3, # 57: Couch -> Other + 3, # 58: Potted Plant -> Other + 3, # 59: Bed -> Other + 3, # 60: Dining Table -> Other + 3, # 61: Toilet -> Other + 3, # 62: TV -> Other + 3, # 63: Laptop -> Other + 3, # 64: Mouse -> Other + 3, # 65: Remote -> Other + 3, # 66: Keyboard -> Other + 3, # 67: Cell Phone -> Other + 3, # 68: Microwave -> Other + 3, # 69: Oven -> Other + 3, # 70: Toaster -> Other + 3, # 71: Sink -> Other + 3, # 72: Refrigerator -> Other + 3, # 73: Book -> Other + 3, # 74: Clock -> Other + 3, # 75: Vase -> Other + 3, # 76: Scissors -> Other + 3, # 77: Teddy Bear -> Other + 3, # 78: Hair Drier -> Other + 3, # 79: Toothbrush -> Other +] + + +def get_carla_color(coco_class): + carla_class = coco_to_carla[coco_class] + return carla_colors[carla_class] + + +def get_carla_class_name(coco_class): + carla_class = coco_to_carla[int(coco_class)] + return carla_class_names[carla_class] diff --git a/code/requirements.txt b/code/requirements.txt index 55de87f0..746fcdfe 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -13,7 +13,7 @@ scipy==1.10.1 xmltodict==0.13.0 py-trees==2.2.3 numpy==1.23.5 -ultralytics==8.1.11 +ultralytics==8.3.32 scikit-learn>=0.18 pandas==2.0.3 debugpy==1.8.7 \ No newline at end of file From 7b1cc263d232c4ff066b23796e31878e18fe91ac Mon Sep 17 00:00:00 2001 From: SirMDA Date: Tue, 26 Nov 2024 18:22:09 +0100 Subject: [PATCH 09/55] removed debug --- code/perception/launch/perception.launch | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/perception/launch/perception.launch b/code/perception/launch/perception.launch index 7059f24f..1c3f487e 100644 --- a/code/perception/launch/perception.launch +++ b/code/perception/launch/perception.launch @@ -32,7 +32,8 @@ --> - + + From 77c68f646d706c727b55c6c6eaa1f631cd63cf32 Mon Sep 17 00:00:00 2001 From: SirMDA Date: Tue, 26 Nov 2024 18:23:08 +0100 Subject: [PATCH 10/55] removed superflous enumerate --- code/perception/src/vision_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/perception/src/vision_node.py b/code/perception/src/vision_node.py index 9df05e27..65647ba9 100755 --- a/code/perception/src/vision_node.py +++ b/code/perception/src/vision_node.py @@ -359,7 +359,7 @@ def predict_ultralytics(self, image): masks = output[0].masks.data boxes = output[0].boxes - for i, box_image in enumerate(boxes): + for box_image in boxes: cls = box_image.cls.item() # class index of object pixels = box_image.xyxy[0] # upper left and lower right pixel coords From f20ac2b56becc2c07388438c54c93eb587983577 Mon Sep 17 00:00:00 2001 From: SirMDA Date: Tue, 26 Nov 2024 18:24:47 +0100 Subject: [PATCH 11/55] renamed --- code/perception/src/vision_node.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/code/perception/src/vision_node.py b/code/perception/src/vision_node.py index 65647ba9..df8a4244 100755 --- a/code/perception/src/vision_node.py +++ b/code/perception/src/vision_node.py @@ -359,9 +359,9 @@ def predict_ultralytics(self, image): masks = output[0].masks.data boxes = output[0].boxes - for box_image in boxes: - cls = box_image.cls.item() # class index of object - pixels = box_image.xyxy[0] # upper left and lower right pixel coords + for box in boxes: + cls = box.cls.item() # class index of object + pixels = box.xyxy[0] # upper left and lower right pixel coords # only run distance calc when dist_array is available # this if is needed because the lidar starts @@ -429,7 +429,7 @@ def predict_ultralytics(self, image): c_labels.append( f"Class: {get_carla_class_name(cls)}, " f"Meters: {round(abs_distance, 2)}, " - f"TrackingId: {int(box_image.id)}, " + f"TrackingId: {int(box.id)}, " f"({round(float(obj_dist_min_x[0]), 2)}, " f"{round(float(obj_dist_min_abs_y[1]), 2)})", ) @@ -448,7 +448,7 @@ def predict_ultralytics(self, image): # draw bounding boxes and distance values on image c_boxes = torch.stack(c_boxes) - box_image = draw_bounding_boxes( + bounding_box_images = draw_bounding_boxes( image_np_with_detections, c_boxes, c_labels, @@ -456,19 +456,21 @@ def predict_ultralytics(self, image): width=3, font_size=12, ) - np_box_img = np.transpose(box_image.detach().numpy(), (1, 2, 0)) + np_box_img = np.transpose(bounding_box_images.detach().numpy(), (1, 2, 0)) scaled_masks = np.squeeze( scale_masks(masks.unsqueeze(1), cv_image.shape[:2], True).cpu().numpy(), 1 ) - mask_image = draw_segmentation_masks( - box_image, torch.from_numpy(scaled_masks > 0), alpha=0.6, colors=c_colors + mask_images = draw_segmentation_masks( + bounding_box_images, + torch.from_numpy(scaled_masks > 0), + alpha=0.6, + colors=c_colors, ) - np_box_img = np.transpose(mask_image.detach().numpy(), (1, 2, 0)) + np_box_img = np.transpose(mask_images.detach().numpy(), (1, 2, 0)) - box_img = cv2.cvtColor(np_box_img, cv2.COLOR_BGR2RGB) - return box_img + return cv2.cvtColor(np_box_img, cv2.COLOR_BGR2RGB) def min_x(self, dist_array): """ From b29e3258838875981f98248d5cfbc9e71e406ab8 Mon Sep 17 00:00:00 2001 From: Ralf Date: Wed, 27 Nov 2024 16:17:12 +0100 Subject: [PATCH 12/55] added first draft of radar data visualization --- code/perception/src/radar_node.py | 115 +++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/code/perception/src/radar_node.py b/code/perception/src/radar_node.py index 4ddf5b4d..247a2df4 100755 --- a/code/perception/src/radar_node.py +++ b/code/perception/src/radar_node.py @@ -2,10 +2,12 @@ import rospy import ros_numpy import numpy as np -from std_msgs.msg import String -from sensor_msgs.msg import PointCloud2 +from std_msgs.msg import String, Header +from sensor_msgs.msg import PointCloud2, PointField from sklearn.cluster import DBSCAN import json +from sensor_msgs import point_cloud2 +import struct class RadarNode: @@ -25,6 +27,23 @@ def callback(self, data): clustered_points_json = json.dumps(clustered_points) self.dist_array_radar_publisher.publish(clustered_points_json) + # output array [x, y, z, distance] + dataarray = pointcloud2_to_array(data) + + # input array [x, y, z, distance], max_distance, output: filtered data + # dataarray = filter_data(dataarray, 10) + + # input array [x, y, z, distance], output: dict clustered + clustered_data = cluster_data(dataarray) + + # input array [x, y, z, distance], clustered labels + cloud = create_pointcloud2(dataarray, clustered_data.labels_) + + self.visualization_radar_publisher.publish(cloud) + + cluster_info = generate_cluster_labels_and_colors(clustered_data, dataarray) + self.cluster_info_radar_publisher.publish(cluster_info) + def listener(self): """Initializes the node and its publishers.""" rospy.init_node("radar_node") @@ -35,6 +54,20 @@ def listener(self): String, queue_size=10, ) + self.visualization_radar_publisher = rospy.Publisher( + rospy.get_param( + "~image_distance_topic", "/paf/hero/Radar/Visualization" + ), + PointCloud2, + queue_size=10, + ) + self.cluster_info_radar_publisher = rospy.Publisher( + rospy.get_param( + "~image_distance_topic", "/paf/hero/Radar/ClusterInfo" + ), + String, + queue_size=10, + ) rospy.Subscriber( rospy.get_param("~source_topic", "/carla/hero/RADAR"), PointCloud2, @@ -109,6 +142,84 @@ def cluster_radar_data_from_pointcloud( return clustered_points +# filters radar data in distance, y, z direction +def filter_data(data, max_distance): + + filtered_data = data[data[:, 3] < max_distance] + filtered_data = filtered_data[ + (filtered_data[:, 1] >= -1) + & (filtered_data[:, 1] <= 1) + & (filtered_data[:, 2] <= 1.3) + & (filtered_data[:, 2] >= -0.7) + ] + return filtered_data + + +# clusters data with DBSCAN +def cluster_data(filtered_data, eps=0.2, min_samples=1): + + if len(filtered_data) == 0: + return {} + coordinates = filtered_data[:, :2] + clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(coordinates) + return clustering + + +# generates random color for cluster +def generate_color_map(num_clusters): + np.random.seed(42) + colors = np.random.randint(0, 255, size=(num_clusters, 3)) + return colors + + +# creates pointcloud2 for publishing clustered radar data +def create_pointcloud2(clustered_points, cluster_labels): + header = Header() + header.stamp = rospy.Time.now() + header.frame_id = "hero/RADAR" + + points = [] + unique_labels = np.unique(cluster_labels) + colors = generate_color_map(len(unique_labels)) + + for i, point in enumerate(clustered_points): + x, y, z, _ = point + label = cluster_labels[i] + + if label == -1: + r, g, b = 128, 128, 128 + else: + r, g, b = colors[label] + + rgb = struct.unpack('f', struct.pack('I', (r << 16) | (g << 8) | b))[0] + points.append([x, y, z, rgb]) + + fields = [ + PointField('x', 0, PointField.FLOAT32, 1), + PointField('y', 4, PointField.FLOAT32, 1), + PointField('z', 8, PointField.FLOAT32, 1), + PointField('rgb', 12, PointField.FLOAT32, 1), + ] + + return point_cloud2.create_cloud(header, fields, points) + + +# generates string with label-id and cluster size +def generate_cluster_labels_and_colors(clusters, data): + cluster_info = [] + + for label in set(clusters.labels_): + cluster_points = data[clusters.labels_ == label] + cluster_size = len(cluster_points) + if label != -1: + cluster_info.append({ + "label": int(label), + "points_count": cluster_size + }) + + return json.dumps(cluster_info) + + if __name__ == "__main__": radar_node = RadarNode() radar_node.listener() From 614869034793471e0a4c396da3eedce809cb7475 Mon Sep 17 00:00:00 2001 From: Ralf Date: Thu, 28 Nov 2024 16:15:49 +0100 Subject: [PATCH 13/55] Added velocity as clustering criteria --- code/perception/src/radar_node.py | 32 +++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/code/perception/src/radar_node.py b/code/perception/src/radar_node.py index 247a2df4..08dede8e 100755 --- a/code/perception/src/radar_node.py +++ b/code/perception/src/radar_node.py @@ -5,6 +5,7 @@ from std_msgs.msg import String, Header from sensor_msgs.msg import PointCloud2, PointField from sklearn.cluster import DBSCAN +from sklearn.preprocessing import StandardScaler import json from sensor_msgs import point_cloud2 import struct @@ -23,9 +24,9 @@ def callback(self, data): Args: data: Point2Cloud message containing radar data """ - clustered_points = cluster_radar_data_from_pointcloud(data, 10) - clustered_points_json = json.dumps(clustered_points) - self.dist_array_radar_publisher.publish(clustered_points_json) + # clustered_points = cluster_radar_data_from_pointcloud(data, 10) + # clustered_points_json = json.dumps(clustered_points) + # self.dist_array_radar_publisher.publish(clustered_points_json) # output array [x, y, z, distance] dataarray = pointcloud2_to_array(data) @@ -91,11 +92,14 @@ def pointcloud2_to_array(pointcloud_msg): [x, y, z, distance], where "distance" is the distance from the origin. """ cloud_array = ros_numpy.point_cloud2.pointcloud2_to_array(pointcloud_msg) - distances = np.sqrt( - cloud_array["x"] ** 2 + cloud_array["y"] ** 2 + cloud_array["z"] ** 2 - ) + # distances = np.sqrt( + # cloud_array["x"] ** 2 + cloud_array["y"] ** 2 + cloud_array["z"] ** 2 + # ) + # return np.column_stack( + # (cloud_array["x"], cloud_array["y"], cloud_array["z"], distances) + # ) return np.column_stack( - (cloud_array["x"], cloud_array["y"], cloud_array["z"], distances) + (cloud_array["x"], cloud_array["y"], cloud_array["z"], cloud_array["Velocity"]) ) @@ -160,8 +164,15 @@ def cluster_data(filtered_data, eps=0.2, min_samples=1): if len(filtered_data) == 0: return {} - coordinates = filtered_data[:, :2] - clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(coordinates) + # coordinates = filtered_data[:, :2] + # clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(coordinates) + + # worse than without scaling + # scaler = StandardScaler() + # data_scaled = scaler.fit_transform(filtered_data) + # clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(data_scaled) + + clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(filtered_data) return clustering @@ -183,7 +194,8 @@ def create_pointcloud2(clustered_points, cluster_labels): colors = generate_color_map(len(unique_labels)) for i, point in enumerate(clustered_points): - x, y, z, _ = point + # x, y, z, _ = point + x, y, z, v = point label = cluster_labels[i] if label == -1: From 78179453423369e2b51792a0d0cecae272f7e2ca Mon Sep 17 00:00:00 2001 From: Ludwig Holl Date: Fri, 29 Nov 2024 17:04:01 +0100 Subject: [PATCH 14/55] updated vision_node --- code/perception/src/vision_node.py | 34 ++++++++++---------- code/perception/src/vision_node_helper.py | 38 +++++++++++++++++++++-- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/code/perception/src/vision_node.py b/code/perception/src/vision_node.py index df8a4244..0e6a03cd 100755 --- a/code/perception/src/vision_node.py +++ b/code/perception/src/vision_node.py @@ -356,7 +356,10 @@ def predict_ultralytics(self, image): c_boxes = [] c_labels = [] c_colors = [] - masks = output[0].masks.data + if hasattr(output[0], "masks") and output[0].masks is not None: + masks = output[0].masks.data + else: + masks = None boxes = output[0].boxes for box in boxes: @@ -390,7 +393,6 @@ def predict_ultralytics(self, image): # only proceed if there is more than one lidar # point in the bounding box if len(non_zero_filter) > 0: - """ !Watch out: The calculation of min x and min abs y is currently @@ -448,7 +450,7 @@ def predict_ultralytics(self, image): # draw bounding boxes and distance values on image c_boxes = torch.stack(c_boxes) - bounding_box_images = draw_bounding_boxes( + drawn_images = draw_bounding_boxes( image_np_with_detections, c_boxes, c_labels, @@ -456,21 +458,21 @@ def predict_ultralytics(self, image): width=3, font_size=12, ) - np_box_img = np.transpose(bounding_box_images.detach().numpy(), (1, 2, 0)) - - scaled_masks = np.squeeze( - scale_masks(masks.unsqueeze(1), cv_image.shape[:2], True).cpu().numpy(), 1 - ) + if masks is not None: + scaled_masks = np.squeeze( + scale_masks(masks.unsqueeze(1), cv_image.shape[:2], True).cpu().numpy(), + 1, + ) - mask_images = draw_segmentation_masks( - bounding_box_images, - torch.from_numpy(scaled_masks > 0), - alpha=0.6, - colors=c_colors, - ) - np_box_img = np.transpose(mask_images.detach().numpy(), (1, 2, 0)) + drawn_images = draw_segmentation_masks( + drawn_images, + torch.from_numpy(scaled_masks > 0), + alpha=0.6, + colors=c_colors, + ) - return cv2.cvtColor(np_box_img, cv2.COLOR_BGR2RGB) + np_image = np.transpose(drawn_images.detach().numpy(), (1, 2, 0)) + return cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB) def min_x(self, dist_array): """ diff --git a/code/perception/src/vision_node_helper.py b/code/perception/src/vision_node_helper.py index 0ea33d95..a96a0c82 100644 --- a/code/perception/src/vision_node_helper.py +++ b/code/perception/src/vision_node_helper.py @@ -1,3 +1,6 @@ +from typing import List, Tuple, Union + + # Carla-Farben carla_colors = [ [0, 0, 0], # 0: None @@ -115,12 +118,41 @@ 3, # 79: Toothbrush -> Other ] +COCO_CLASS_COUNT = 80 + + +def get_carla_color(coco_class: Union[int, float]) -> List[int]: + """Get the Carla color for a given COCO class. + Args: + coco_class: COCO class index (0-79) -def get_carla_color(coco_class): + Returns: + RGB color values for the corresponding Carla class + + Raises: + ValueError: If coco_class is out of valid range + """ + coco_idx = int(coco_class) + if not 0 <= coco_idx < COCO_CLASS_COUNT: + raise ValueError(f"Invalid COCO class index: {coco_idx}") carla_class = coco_to_carla[coco_class] return carla_colors[carla_class] -def get_carla_class_name(coco_class): - carla_class = coco_to_carla[int(coco_class)] +def get_carla_class_name(coco_class: Union[int, float]) -> str: + """Get the Carla class name for a given COCO class. + Args: + coco_class: COCO class index (0-79) + + Returns: + Name of the corresponding Carla class + + Raises: + ValueError: If coco_class is out of valid range + """ + coco_idx = int(coco_class) + + if not 0 <= coco_idx < COCO_CLASS_COUNT: + raise ValueError(f"Invalid COCO class index: {coco_idx}") + carla_class = coco_to_carla[coco_idx] return carla_class_names[carla_class] From 38d85f7dad0453a983a2ebc303e2bc61d61278b6 Mon Sep 17 00:00:00 2001 From: Ludwig Holl Date: Fri, 29 Nov 2024 17:09:51 +0100 Subject: [PATCH 15/55] fixed minor bug --- code/perception/src/vision_node_helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/perception/src/vision_node_helper.py b/code/perception/src/vision_node_helper.py index a96a0c82..6778f7b6 100644 --- a/code/perception/src/vision_node_helper.py +++ b/code/perception/src/vision_node_helper.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Union +from typing import List, Union # Carla-Farben @@ -135,7 +135,7 @@ def get_carla_color(coco_class: Union[int, float]) -> List[int]: coco_idx = int(coco_class) if not 0 <= coco_idx < COCO_CLASS_COUNT: raise ValueError(f"Invalid COCO class index: {coco_idx}") - carla_class = coco_to_carla[coco_class] + carla_class = coco_to_carla[coco_idx] return carla_colors[carla_class] From 3b5a15c7a9e7129a84f7dd8d1dfb32fe997d93ff Mon Sep 17 00:00:00 2001 From: SirMDA Date: Tue, 3 Dec 2024 20:01:03 +0100 Subject: [PATCH 16/55] modified requirements.txt with pyopenssl version --- code/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/requirements.txt b/code/requirements.txt index 55de87f0..cbe57476 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -16,4 +16,5 @@ numpy==1.23.5 ultralytics==8.1.11 scikit-learn>=0.18 pandas==2.0.3 -debugpy==1.8.7 \ No newline at end of file +debugpy==1.8.7 +pyopenssl==24.3.0 \ No newline at end of file From f5bbe472694e29c32a5cc882cca3ac1b7c9f0c67 Mon Sep 17 00:00:00 2001 From: Ralf Date: Wed, 4 Dec 2024 16:17:28 +0100 Subject: [PATCH 17/55] started implementation of bounding boxes for detected radar clusters --- code/perception/src/radar_node.py | 176 ++++++++++++++++++++++++++++-- 1 file changed, 165 insertions(+), 11 deletions(-) diff --git a/code/perception/src/radar_node.py b/code/perception/src/radar_node.py index 08dede8e..39ed7ec0 100755 --- a/code/perception/src/radar_node.py +++ b/code/perception/src/radar_node.py @@ -8,6 +8,9 @@ from sklearn.preprocessing import StandardScaler import json from sensor_msgs import point_cloud2 +from visualization_msgs.msg import Marker, MarkerArray +from geometry_msgs.msg import Point + import struct @@ -37,14 +40,26 @@ def callback(self, data): # input array [x, y, z, distance], output: dict clustered clustered_data = cluster_data(dataarray) + # transformed_data = transform_data_to_2d(dataarray) + # input array [x, y, z, distance], clustered labels cloud = create_pointcloud2(dataarray, clustered_data.labels_) - self.visualization_radar_publisher.publish(cloud) cluster_info = generate_cluster_labels_and_colors(clustered_data, dataarray) self.cluster_info_radar_publisher.publish(cluster_info) + points_with_labels = np.hstack((dataarray, clustered_data.labels_.reshape(-1, 1))) + bounding_boxes = generate_bounding_boxes(points_with_labels) + + marker_array = MarkerArray() + for label, bbox in bounding_boxes: + if label != -1: + marker = create_bounding_box_marker(label, bbox) + marker_array.markers.append(marker) + + self.marker_visualization_radar_publisher.publish(marker_array) + def listener(self): """Initializes the node and its publishers.""" rospy.init_node("radar_node") @@ -62,6 +77,13 @@ def listener(self): PointCloud2, queue_size=10, ) + self.marker_visualization_radar_publisher = rospy.Publisher( + rospy.get_param( + "~image_distance_topic", "/paf/hero/Radar/Marker" + ), + MarkerArray, + queue_size=10, + ) self.cluster_info_radar_publisher = rospy.Publisher( rospy.get_param( "~image_distance_topic", "/paf/hero/Radar/ClusterInfo" @@ -149,18 +171,20 @@ def cluster_radar_data_from_pointcloud( # filters radar data in distance, y, z direction def filter_data(data, max_distance): - filtered_data = data[data[:, 3] < max_distance] + # filtered_data = data[data[:, 3] < max_distance] + filtered_data = data filtered_data = filtered_data[ - (filtered_data[:, 1] >= -1) - & (filtered_data[:, 1] <= 1) - & (filtered_data[:, 2] <= 1.3) - & (filtered_data[:, 2] >= -0.7) + # (filtered_data[:, 1] >= -1) + # & (filtered_data[:, 1] <= 1) + # & (filtered_data[:, 2] <= 1.3) + (filtered_data[:, 2] <= 1.3) + & (filtered_data[:, 2] >= -0.6) # -0.7 ] return filtered_data # clusters data with DBSCAN -def cluster_data(filtered_data, eps=0.2, min_samples=1): +def cluster_data(filtered_data, eps=0.8, min_samples=5): if len(filtered_data) == 0: return {} @@ -168,11 +192,11 @@ def cluster_data(filtered_data, eps=0.2, min_samples=1): # clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(coordinates) # worse than without scaling - # scaler = StandardScaler() - # data_scaled = scaler.fit_transform(filtered_data) - # clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(data_scaled) + scaler = StandardScaler() + data_scaled = scaler.fit_transform(filtered_data) + clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(data_scaled) - clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(filtered_data) + # clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(filtered_data) return clustering @@ -216,6 +240,136 @@ def create_pointcloud2(clustered_points, cluster_labels): return point_cloud2.create_cloud(header, fields, points) +def transform_data_to_2d(clustered_data): + + transformed_points = clustered_data + transformed_points[:, 0] = clustered_data[:, 0] + transformed_points[:, 1] = clustered_data[:, 1] + transformed_points[:, 2] = 0 + transformed_points[:, 3] = clustered_data[:, 3] + + return transformed_points + + +def calculate_aabb(cluster_points): + """_summary_ + + Args: + cluster_points (_type_): _description_ + + Returns: + _type_: _description_ + """ + + # for 2d (top-down) boxes + # x_min = np.min(cluster_points[:, 0]) + # x_max = np.max(cluster_points[:, 0]) + # y_min = np.min(cluster_points[:, 1]) + # y_max = np.max(cluster_points[:, 1]) + + # return x_min, x_max, y_min, y_max + + # for 3d boxes + x_min = np.min(cluster_points[:, 0]) + x_max = np.max(cluster_points[:, 0]) + y_min = np.min(cluster_points[:, 1]) + y_max = np.max(cluster_points[:, 1]) + z_min = np.min(cluster_points[:, 2]) + z_max = np.max(cluster_points[:, 2]) + rospy.loginfo(f"Bounding box for label: X({x_min}, {x_max}), Y({y_min}, {y_max}), Z({z_min}, {z_max})") + return x_min, x_max, y_min, y_max, z_min, z_max + + +def generate_bounding_boxes(points_with_labels): + """_summary_ + + Args: + points_with_labels (_type_): _description_ + + Returns: + _type_: _description_ + """ + bounding_boxes = [] + unique_labels = np.unique(points_with_labels[:, -1]) + for label in unique_labels: + if label == -1: + continue + cluster_points = points_with_labels[points_with_labels[:, -1] == label, :3] + bbox = calculate_aabb(cluster_points) + bounding_boxes.append((label, bbox)) + return bounding_boxes + + +def create_bounding_box_marker(label, bbox): + """_summary_ + + Returns: + _type_: _description_ + """ + # for 2d (top-down) boxes + # x_min, x_max, y_min, y_max = bbox + + # for 3d boxes + x_min, x_max, y_min, y_max, z_min, z_max = bbox + + marker = Marker() + marker.header.frame_id = "hero/RADAR" + marker.id = int(label) + # marker.type = Marker.LINE_STRIP + marker.type = Marker.LINE_LIST + marker.action = Marker.ADD + marker.scale.x = 0.1 + marker.color.r = 1.0 + marker.color.g = 1.0 + marker.color.b = 0.0 + marker.color.a = 1.0 + + # for 2d (top-down) boxes + # points = [ + # Point(x_min, y_min, 0), + # Point(x_max, y_min, 0), + # Point(x_max, y_max, 0), + # Point(x_min, y_max, 0), + # Point(x_min, y_min, 0), + # ] + # marker.points = points + + # for 3d boxes + points = [ + Point(x_min, y_min, z_min), # Ecke 0 + Point(x_max, y_min, z_min), # Ecke 1 + Point(x_max, y_max, z_min), # Ecke 2 + Point(x_min, y_max, z_min), # Ecke 3 + Point(x_min, y_min, z_max), # Ecke 4 + Point(x_max, y_min, z_max), # Ecke 5 + Point(x_max, y_max, z_max), # Ecke 6 + Point(x_min, y_max, z_max), # Ecke 7 + + # Point(x_min, y_min, z_min), # Verbinde z_min zu z_max + # Point(x_min, y_min, z_max), + + # Point(x_max, y_min, z_min), + # Point(x_max, y_min, z_max), + + # Point(x_max, y_max, z_min), + # Point(x_max, y_max, z_max), + + # Point(x_min, y_max, z_min), + # Point(x_min, y_max, z_max), + ] + # marker.points = points + lines = [ + (0, 1), (1, 2), (2, 3), (3, 0), # Boden + (4, 5), (5, 6), (6, 7), (7, 4), # Deckel + (0, 4), (1, 5), (2, 6), (3, 7), # Vertikale Kanten + ] + for start, end in lines: + marker.points.append(points[start]) + marker.points.append(points[end]) + + return marker + + # generates string with label-id and cluster size def generate_cluster_labels_and_colors(clusters, data): cluster_info = [] From 44864ac17fb9ad6aa74dade9387b997608ad0f6a Mon Sep 17 00:00:00 2001 From: SirMDA Date: Thu, 5 Dec 2024 12:52:48 +0100 Subject: [PATCH 18/55] bugfixing wip --- code/perception/src/vision_node.py | 48 ++++++++++++++---------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/code/perception/src/vision_node.py b/code/perception/src/vision_node.py index 0e6a03cd..abbe52a7 100755 --- a/code/perception/src/vision_node.py +++ b/code/perception/src/vision_node.py @@ -356,12 +356,9 @@ def predict_ultralytics(self, image): c_boxes = [] c_labels = [] c_colors = [] - if hasattr(output[0], "masks") and output[0].masks is not None: - masks = output[0].masks.data - else: - masks = None boxes = output[0].boxes + masks = output[0].masks.data for box in boxes: cls = box.cls.item() # class index of object pixels = box.xyxy[0] # upper left and lower right pixel coords @@ -369,17 +366,16 @@ def predict_ultralytics(self, image): # only run distance calc when dist_array is available # this if is needed because the lidar starts # publishing with a delay - if self.dist_arrays is None: - continue - - # crop bounding box area out of depth image - distances = np.asarray( - self.dist_arrays[ - int(pixels[1]) : int(pixels[3]) : 1, - int(pixels[0]) : int(pixels[2]) : 1, - ::, - ] - ) + if self.dist_arrays is not None: + + # crop bounding box area out of depth image + distances = np.asarray( + self.dist_arrays[ + int(pixels[1]) : int(pixels[3]) : 1, + int(pixels[0]) : int(pixels[2]) : 1, + ::, + ] + ) # set all 0 (black) values to np.inf (necessary if # you want to search for minimum) @@ -458,18 +454,18 @@ def predict_ultralytics(self, image): width=3, font_size=12, ) - if masks is not None: - scaled_masks = np.squeeze( - scale_masks(masks.unsqueeze(1), cv_image.shape[:2], True).cpu().numpy(), - 1, - ) - drawn_images = draw_segmentation_masks( - drawn_images, - torch.from_numpy(scaled_masks > 0), - alpha=0.6, - colors=c_colors, - ) + scaled_masks = np.squeeze( + scale_masks(masks.unsqueeze(1), cv_image.shape[:2], True).cpu().numpy(), + 1, + ) + + drawn_images = draw_segmentation_masks( + drawn_images, + torch.from_numpy(scaled_masks > 0), + alpha=0.6, + colors=c_colors, + ) np_image = np.transpose(drawn_images.detach().numpy(), (1, 2, 0)) return cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB) From 8532d6a6ab1c40ad986b020dabae261a29bbe030 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 5 Dec 2024 18:06:34 +0100 Subject: [PATCH 19/55] 490 remodel acc concept (#516) * Research on general ACC implementations is added * Update ACC.md added information about current implementation, new concept and next steps * Update ACC.md fix linter * PID Control added * Link fixed * Update ACC.md made proposed concept more detailed * Update ACC.md linter * Update ACC.md * Update ACC.md added diagram for presentation * Integrated review feedback * Edited graphics link --------- Co-authored-by: Veronika Neumann Co-authored-by: vinzenzm <73160933+vinzenzm@users.noreply.github.com> --- .../research_assets/ACC_FLC_Example_1.PNG | Bin 0 -> 132699 bytes doc/research/paf24/planning/ACC.md | 115 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 doc/assets/research_assets/ACC_FLC_Example_1.PNG create mode 100644 doc/research/paf24/planning/ACC.md diff --git a/doc/assets/research_assets/ACC_FLC_Example_1.PNG b/doc/assets/research_assets/ACC_FLC_Example_1.PNG new file mode 100644 index 0000000000000000000000000000000000000000..e6382981d249d3d9d81f0516b55f7c92b0e252fe GIT binary patch literal 132699 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

)7`r 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 40 km/h). Apart from that, there are Stop & Go systems that support lower velocites (e.g. < 40 km/h). In our case, both systems might be needed and it might be reasonable to develop two different systems for ACC and Stop & Go. +The threshold to distinguish between the two systems has to be chosen reasonably. +There are basically three different techniques that can be used to implement an ACC: PID Control, Model Predictive Control and Fuzzy Logic Control. Another option is CACC (Cooperative Adaptive Cruise Control) but this is not relevant for our project since it requires communication between the vehicles. + +### PID Control + +The PID Controller consists of three terms: the proportional term, the integral term and the derivative term. One possible simple controller model looks as follows: + +$$ v_f(t) = v_f(t - t_s) + k_p e(t-t_s) + k_i \int_{0}^{t} e(\tau) d\tau + k_d \dot{e}(t - t_s) $$ +$$ e(t-t_s) = \Delta x(t - t_s) - t_{hw,d} v_f (t - t_s) $$ + +- $v_f$: follower vehicle velocity (transmitted to the acting component) +- $t_s$: sampling time +- $k_p$ and $k_i$ and $k_d$: coefficients for proportional, integral and derivative terms +- $e$: distance error (difference between actual distance $\Delta x$ and desired distance $\Delta x_d$) +- $t_{hw,d}$: desired time headway (duration between the arrival of the first car at a certain waypoint and the arrival of the following car at the same waypoint) + +### Model Predictive Control (MPC) + +It calculates the current control action by solving an online, iterative and finite-horizon optimization of the model. +Procedure: + +1. Prediction of future system states based on current states +2. Computation of the cost function for a finite time horizon in the future +3. Implementation of the first step of the solved control sequence +4. Application of the feedback control loop to compensate for the predictive error and model inaccuracy +5. Sampling of new current states and repitition of the process + +### Fuzzy Logic Control (FLC) + +Provides a unified control framework to offer both functions: ACC and Stop & Go. + +![Example of a FLC control algorithm showing input variables (relative distance, host vehicle speed), fuzzy rules processing, and output variable](../../../assets/research_assets/ACC_FLC_Example_1.PNG) + +## ACC in our project + +### Current implementation + +- General behaviour: + checks for obstacle distance < safety distance + + calculates new speed based on obstacle distance + + else keep current speed + +- publishes speed to acc_velocity +- safe distance calculation is currently not correct, uses speed + (speed * 0.36)² which results in wrong distances +- if car in front of us ignores speed limits we ignore them as well +- some parts of unstuck routine are in ACC and need to be refactored +- same goes for publishing of current waypoint, this should not be in ACC + +In summary, the current implementation is not sufficient and needs major refactoring. + +### Concept for new implementation + +The new concept for the ACC is to take the trajectory, look at all or a limited subset of the next points and add a target velocity and the current behaviour to each point. +This way the Acting has more knowledge about what the car is doing and can adjust accordingly in a local manner. +For this a new trajectory message type was implemented in #511. + +![trajectoryMsg](https://github.com/user-attachments/assets/0b452f1a-4c60-45b2-882f-3a50118c9cb9) + +Since behaviour is passed as an ID a new enum for behaviours was implemented in utils.py as well. + +The general idea for speeds above the 40 km/h mark is to calculate a proper safety distance, a general target velocity and velocity targets based on PID. For speeds lower than that a stop and go system can be discussed if it is really needed. + +For safety distance we would like to calculate it like FLC but that most likely needs some adjustments as setting the distance to 100m when there is no vehicle ahead seems unreasonable. +We can just directly set the velocity to the speed limit in that case. + +For a general speed target we either take the speed of the car in front or the speed limit, whichever is lower. In cases where the car in front is substantially slower than the speed limit ACC could inititate overtaking. + +Since we want to calculate the desired speed at each point of the trajectory, the way PID calculates velocity seems reasonable since we can treat the trajectory points as different points in time for the sampling time. +For example let's say we sample every fifth point and calculate the velocity for that, then we can just interpolate every other point inbetween. + +$v_f(t - t_s)$ would then simply be the velocity of the fifth point before the current one. Theoretically this allows us to dynamically adjust the sampling time as well if needed. + +For the distance error we can use the safety distance as the desired distance. The distance at time t needs to be predicted based on the (predicted) distance at the prior sample point and the calculated speed at the prior sample point. + +We calculate velocities like that up to the point where the actual distance is within 5% of the optimal safety distance. For points further than that we simply use the desired general speed. + +![accDiagram](https://github.com/user-attachments/assets/9a7b4572-f041-4da0-900c-51ab20d1904b) + +### Possible next steps + +A seperate file for the new ACC should be created to not disturb the system. + +The parts that might get cut from ACC like current waypoint and unstuck routine need to be evaluated for necessity and if need be moved to somewhere more fitting. + +Implement publisher for new message type. + +Start implementing safety distance and general target speed logic. Subscriber logic could be taken from old implementation. + +Implement PID logic + +### Requirements + +- obstacle speed +- obstacle distance + +## Discussion + +- How to test adaptations of the ACC? (Suggestion: Create test scenarios which represent different situations like driving straight forward behing a leading vehicle, no leading vehicle, sudden breaking, etc.) +- Which output should be transfered to the Acting component? (Suggestion: The desired speed is published in the new trajectory. No acceleration data is needed, the acceleration is handled by the acting component.) +- Which input do we get from the Perception component? (Suggestion: The distance and velocity of the car in front would be really helpful.) + +## Sources + +He, Yinglong et al. (2019). Adaptive Cruise Control Strategies Implemented on Experimental Vehicles: A Review. IFAC-PapersOnLine. 52. 21-27. 10.1016/j.ifacol.2019.09.004. +[Link](https://www.researchgate.net/publication/335934496_Adaptive_Cruise_Control_Strategies_Implemented_on_Experimental_Vehicles_A_Review) From 094d8ed084b2912d36e84251e9a2859d8dcc2b25 Mon Sep 17 00:00:00 2001 From: ll7 <32880741+ll7@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:20:50 +0100 Subject: [PATCH 20/55] update project management and review guidelines to use TIP instead of INFO --- doc/development/project_management.md | 2 +- doc/development/review_guideline.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/development/project_management.md b/doc/development/project_management.md index ef5e45d7..ec772185 100644 --- a/doc/development/project_management.md +++ b/doc/development/project_management.md @@ -50,4 +50,4 @@ Merge the pull request after: After merging, remember to delete the source branch to keep the repository clean. ->[!INFO] For more information about the review process, see [Review process](./review_guideline.md). +>[!TIP] For more information about the review process, see [Review process](./review_guideline.md). diff --git a/doc/development/review_guideline.md b/doc/development/review_guideline.md index 840adb48..64a502c9 100644 --- a/doc/development/review_guideline.md +++ b/doc/development/review_guideline.md @@ -95,7 +95,7 @@ If a comment of a review was resolved by either, a new commit or a discussion be If a new commit took place it is encouraged to comment the commit SHA to have a connection between comment and resolving commit ![img.png](../assets/Resolve_conversation.png) -> [!INFO] All conversations should be resolved before merging the pull request. +> [!TIP] All conversations should be resolved before merging the pull request. ## 5. Merging a Pull Request @@ -120,7 +120,7 @@ Long story short, the reviewer who approves the PR should merge. Only approve if Before merging a pull request, the request checks by the CI/CD pipeline should be successful. If the checks fail, the pull request should not be merged. -> [!INFO] An exception can be made for a PR that only addresses the documentation and the `driving` check is not yet completed. +> [!TIP] An exception can be made for a PR that only addresses the documentation and the `driving` check is not yet completed. ### 5.3. Deleting the branch From 1452879c894c9036ad64d07932f51725c559faa7 Mon Sep 17 00:00:00 2001 From: SirMDA Date: Mon, 9 Dec 2024 14:58:46 +0100 Subject: [PATCH 21/55] fixed import error with __init__ file --- code/perception/src/__init__.py | 0 code/perception/src/vision_node.py | 50 ++++++++++++++++-------------- 2 files changed, 27 insertions(+), 23 deletions(-) create mode 100755 code/perception/src/__init__.py diff --git a/code/perception/src/__init__.py b/code/perception/src/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/code/perception/src/vision_node.py b/code/perception/src/vision_node.py index abbe52a7..1a558df2 100755 --- a/code/perception/src/vision_node.py +++ b/code/perception/src/vision_node.py @@ -15,7 +15,7 @@ ) import torchvision.transforms as t import cv2 -from perception.src.vision_node_helper import get_carla_class_name, get_carla_color +from vision_node_helper import get_carla_class_name, get_carla_color from rospy.numpy_msg import numpy_msg from sensor_msgs.msg import Image as ImageMsg from std_msgs.msg import Header, Float32MultiArray @@ -356,9 +356,12 @@ def predict_ultralytics(self, image): c_boxes = [] c_labels = [] c_colors = [] + if hasattr(output[0], "masks") and output[0].masks is not None: + masks = output[0].masks.data + else: + masks = None boxes = output[0].boxes - masks = output[0].masks.data for box in boxes: cls = box.cls.item() # class index of object pixels = box.xyxy[0] # upper left and lower right pixel coords @@ -366,16 +369,17 @@ def predict_ultralytics(self, image): # only run distance calc when dist_array is available # this if is needed because the lidar starts # publishing with a delay - if self.dist_arrays is not None: - - # crop bounding box area out of depth image - distances = np.asarray( - self.dist_arrays[ - int(pixels[1]) : int(pixels[3]) : 1, - int(pixels[0]) : int(pixels[2]) : 1, - ::, - ] - ) + if self.dist_arrays is None: + continue + + # crop bounding box area out of depth image + distances = np.asarray( + self.dist_arrays[ + int(pixels[1]) : int(pixels[3]) : 1, + int(pixels[0]) : int(pixels[2]) : 1, + ::, + ] + ) # set all 0 (black) values to np.inf (necessary if # you want to search for minimum) @@ -454,18 +458,18 @@ def predict_ultralytics(self, image): width=3, font_size=12, ) + if masks is not None: + scaled_masks = np.squeeze( + scale_masks(masks.unsqueeze(1), cv_image.shape[:2], True).cpu().numpy(), + 1, + ) - scaled_masks = np.squeeze( - scale_masks(masks.unsqueeze(1), cv_image.shape[:2], True).cpu().numpy(), - 1, - ) - - drawn_images = draw_segmentation_masks( - drawn_images, - torch.from_numpy(scaled_masks > 0), - alpha=0.6, - colors=c_colors, - ) + drawn_images = draw_segmentation_masks( + drawn_images, + torch.from_numpy(scaled_masks > 0), + alpha=0.6, + colors=c_colors, + ) np_image = np.transpose(drawn_images.detach().numpy(), (1, 2, 0)) return cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB) From 07794547ed31d4af0743270c7c1480c8d0a49642 Mon Sep 17 00:00:00 2001 From: Ralf Date: Mon, 9 Dec 2024 17:17:27 +0100 Subject: [PATCH 22/55] added deletion of old markers --- code/perception/src/radar_node.py | 93 +++++++++++++++++++++++++++++-- code/requirements.txt | 3 +- 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/code/perception/src/radar_node.py b/code/perception/src/radar_node.py index 39ed7ec0..ddff6511 100755 --- a/code/perception/src/radar_node.py +++ b/code/perception/src/radar_node.py @@ -46,20 +46,26 @@ def callback(self, data): cloud = create_pointcloud2(dataarray, clustered_data.labels_) self.visualization_radar_publisher.publish(cloud) - cluster_info = generate_cluster_labels_and_colors(clustered_data, dataarray) - self.cluster_info_radar_publisher.publish(cluster_info) - points_with_labels = np.hstack((dataarray, clustered_data.labels_.reshape(-1, 1))) bounding_boxes = generate_bounding_boxes(points_with_labels) marker_array = MarkerArray() + # marker_array = clear_old_markers(marker_array) for label, bbox in bounding_boxes: if label != -1: marker = create_bounding_box_marker(label, bbox) marker_array.markers.append(marker) + # min_marker, max_marker = create_min_max_markers(label, bbox) + # marker_array.markers.append(min_marker) + # marker_array.markers.append(max_marker) + + marker_array = clear_old_markers(marker_array, max_id=len(bounding_boxes) - 1) self.marker_visualization_radar_publisher.publish(marker_array) + cluster_info = generate_cluster_labels_and_colors(clustered_data, dataarray, marker_array, bounding_boxes) + self.cluster_info_radar_publisher.publish(cluster_info) + def listener(self): """Initializes the node and its publishers.""" rospy.init_node("radar_node") @@ -366,12 +372,85 @@ def create_bounding_box_marker(label, bbox): for start, end in lines: marker.points.append(points[start]) marker.points.append(points[end]) - + return marker +def create_min_max_markers(label, bbox, frame_id="hero/RADAR", min_color=(0.0, 1.0, 0.0, 1.0), max_color=(1.0, 0.0, 0.0, 1.0)): + """ + Erstellt RViz-Marker für die Min- und Max-Punkte einer Bounding Box. + + Args: + label (int): Die ID des Clusters (wird als Marker-ID genutzt). + bbox (tuple): Min- und Max-Werte der Bounding Box (x_min, x_max, y_min, y_max, z_min, z_max). + frame_id (str): Frame ID, in dem die Marker gezeichnet werden. + min_color (tuple): RGBA-Farbwerte für den Min-Punkt-Marker. + max_color (tuple): RGBA-Farbwerte für den Max-Punkt-Marker. + + Returns: + tuple: Ein Paar von Markern (min_marker, max_marker). + """ + x_min, x_max, y_min, y_max, z_min, z_max = bbox + + # marker = Marker() + # marker.header.frame_id = "hero/RADAR" + # marker.id = int(label) + # # marker.type = Marker.LINE_STRIP + # marker.type = Marker.LINE_LIST + # marker.action = Marker.ADD + # marker.scale.x = 0.1 + # marker.color.r = 1.0 + # marker.color.g = 1.0 + # marker.color.b = 0.0 + # marker.color.a = 1.0 + + # Min-Punkt-Marker + min_marker = Marker() + min_marker.header.frame_id = frame_id + min_marker.id = int(label * 10) # ID für Min-Punkt + min_marker.type = Marker.SPHERE + min_marker.action = Marker.ADD + min_marker.scale.x = 0.2 # Größe des Punktes + min_marker.scale.y = 0.2 + min_marker.scale.z = 0.2 + min_marker.color.r = min_color[0] + min_marker.color.g = min_color[1] + min_marker.color.b = min_color[2] + min_marker.color.a = min_color[3] + min_marker.pose.position.x = x_min + min_marker.pose.position.y = y_min + min_marker.pose.position.z = z_min + + # Max-Punkt-Marker + max_marker = Marker() + max_marker.header.frame_id = frame_id + max_marker.id = int(label * 10 + 1) # ID für Max-Punkt + max_marker.type = Marker.SPHERE + max_marker.action = Marker.ADD + max_marker.scale.x = 0.2 + max_marker.scale.y = 0.2 + max_marker.scale.z = 0.2 + max_marker.color.r = max_color[0] + max_marker.color.g = max_color[1] + max_marker.color.b = max_color[2] + max_marker.color.a = max_color[3] + max_marker.pose.position.x = x_max + max_marker.pose.position.y = y_max + max_marker.pose.position.z = z_max + + return min_marker, max_marker + + +def clear_old_markers(marker_array, max_id): + """Löscht alte Marker aus dem MarkerArray.""" + for marker in marker_array.markers: + if marker.id >= max_id: + marker.action = Marker.DELETE + return marker_array + + # generates string with label-id and cluster size -def generate_cluster_labels_and_colors(clusters, data): +def generate_cluster_labels_and_colors(clusters, data, marker_array, bounding_boxes): cluster_info = [] for label in set(clusters.labels_): @@ -380,7 +459,9 @@ def generate_cluster_labels_and_colors(clusters, data): if label != -1: cluster_info.append({ "label": int(label), - "points_count": cluster_size + "points_count": cluster_size, + "Anzahl marker": len(marker_array.markers), + "Anzahl Boundingboxen": len(bounding_boxes) }) return json.dumps(cluster_info) diff --git a/code/requirements.txt b/code/requirements.txt index 55de87f0..cbe57476 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -16,4 +16,5 @@ numpy==1.23.5 ultralytics==8.1.11 scikit-learn>=0.18 pandas==2.0.3 -debugpy==1.8.7 \ No newline at end of file +debugpy==1.8.7 +pyopenssl==24.3.0 \ No newline at end of file From c46c93428e9ba3988c2b5413972ce422687a4fad Mon Sep 17 00:00:00 2001 From: michalal7 Date: Wed, 11 Dec 2024 14:39:38 +0100 Subject: [PATCH 23/55] - Added clustering functionality for LiDAR data processing. - Refactored the existing node code by modularizing it into functions for better readability and maintainability. --- code/perception/launch/perception.launch | 2 +- code/perception/src/lidar_distance.py | 48 ++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/code/perception/launch/perception.launch b/code/perception/launch/perception.launch index 67a9c2ab..127ae9a4 100644 --- a/code/perception/launch/perception.launch +++ b/code/perception/launch/perception.launch @@ -75,7 +75,7 @@ - + diff --git a/code/perception/src/lidar_distance.py b/code/perception/src/lidar_distance.py index 04394ee2..2842172c 100755 --- a/code/perception/src/lidar_distance.py +++ b/code/perception/src/lidar_distance.py @@ -3,13 +3,13 @@ import ros_numpy import numpy as np import lidar_filter_utility -from sensor_msgs.msg import PointCloud2 +from sensor_msgs.msg import PointCloud2, Image as ImageMsg +from sklearn.cluster import DBSCAN +from cv_bridge import CvBridge +import json # from mpl_toolkits.mplot3d import Axes3D # from itertools import combinations -from sensor_msgs.msg import Image as ImageMsg -from cv_bridge import CvBridge - # from matplotlib.colors import LinearSegmentedColormap @@ -30,6 +30,22 @@ def callback(self, data): """ coordinates = ros_numpy.point_cloud2.pointcloud2_to_array(data) + # clustered_points = cluster_lidar_data_from_pointcloud(coordinates.copy()) + try: + filtered_coordinates = coordinates[coordinates["z"] > 0] + + # Konvertiere in JSON + clustered_points_json = json.dumps(filtered_coordinates.tolist()) + + # Veröffentliche JSON-Daten + # self.dist_array_lidar_publisher.publish(clustered_points_json) + rospy.loginfo( + f"JSON mit {len(clustered_points_json)} Punkten veröffentlicht." + ) + rospy.loginfo(clustered_points_json) + except Exception as e: + rospy.logerr(f"Fehler im Callback: {e}") + # Center reconstruct_bit_mask_center = lidar_filter_utility.bounding_box( coordinates, @@ -146,6 +162,20 @@ def listener(self): queue_size=10, ) + """ + try: + self.dist_array_lidar_publisher = rospy.Publisher( + rospy.get_param( + "~image_distance_topic_cluster", "/paf/hero/Lidar/dist_clustered" + ), + String, + queue_size=10, + ) + rospy.loginfo("dist_array_lidar_publisher erfolgreich erstellt.") + except Exception as e: + rospy.logerr(f"Fehler beim Erstellen von dist_array_lidar_publisher: {e}") + """ + # publisher for dist_array self.dist_array_left_publisher = rospy.Publisher( rospy.get_param("~image_distance_topic", "/paf/hero/Left/dist_array"), @@ -166,6 +196,7 @@ def listener(self): self.callback, ) + rospy.loginfo("Lidar Processor Node gestartet.") rospy.spin() def reconstruct_img_from_lidar(self, coordinates_xyz, focus): @@ -246,6 +277,15 @@ def reconstruct_img_from_lidar(self, coordinates_xyz, focus): return dist_array +def cluster_lidar_data_from_pointcloud(coordinates, eps=1.0, min_samples=2): + + clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(coordinates) + labels = clustering.labels_ + clustered_points = {label: list(labels).count(label) for label in set(labels)} + clustered_points = {int(label): count for label, count in clustered_points.items()} + return clustered_points + + if __name__ == "__main__": lidar_distance = LidarDistance() lidar_distance.listener() From 08e21757aa08fa8d3b3132d4823e673f38a8e834 Mon Sep 17 00:00:00 2001 From: michalal7 Date: Wed, 11 Dec 2024 14:45:44 +0100 Subject: [PATCH 24/55] Added clustering functionality for LiDAR data processing. --- code/agent/config/rviz_config.rviz | 93 +++- code/perception/src/lidar_distance.py | 482 ++++++++++++++------ code/perception/src/lidar_distance_neu.py | 516 ++++++++++++++++++++++ 3 files changed, 933 insertions(+), 158 deletions(-) mode change 100755 => 100644 code/perception/src/lidar_distance.py create mode 100755 code/perception/src/lidar_distance_neu.py diff --git a/code/agent/config/rviz_config.rviz b/code/agent/config/rviz_config.rviz index 2e419cc2..998284c8 100644 --- a/code/agent/config/rviz_config.rviz +++ b/code/agent/config/rviz_config.rviz @@ -1,14 +1,12 @@ Panels: - Class: rviz/Displays - Help Height: 78 + Help Height: 70 Name: Displays Property Tree Widget: Expanded: - - /PointCloud23 - - /PointCloud24 - - /PointCloud25 + - /PointCloud2 dist_clustered1 Splitter Ratio: 0.5 - Tree Height: 308 + Tree Height: 325 - Class: rviz/Selection Name: Selection - Class: rviz/Tool Properties @@ -66,26 +64,23 @@ Visualization Manager: Grid: false Imu: false Path: false - PointCloud2: false + PointCloud2: true + PointCloud2 dist_clustered: true Value: true + VisonNode Output: true Zoom Factor: 1 - Class: rviz/Image Enabled: true - Image Rendering: background and overlay Image Topic: /paf/hero/Center/segmented_image + Max Value: 1 + Median window: 5 + Min Value: 0 Name: VisonNode Output - Overlay Alpha: 0.5 + Normalize Range: true Queue Size: 2 Transport Hint: raw Unreliable: false Value: true - Visibility: - Grid: true - Imu: true - Path: true - PointCloud2: true - Value: true - Zoom Factor: 1 - Alpha: 1 Class: rviz_plugin_tutorials/Imu Color: 204; 51; 204 @@ -260,6 +255,62 @@ Visualization Manager: Use Fixed Frame: true Use rainbow: true Value: true + - Alpha: 1 + Autocompute Intensity Bounds: true + Autocompute Value Bounds: + Max Value: 10 + Min Value: -10 + Value: true + Axis: Z + Channel Name: intensity + Class: rviz/PointCloud2 + Color: 255; 255; 255 + Color Transformer: "" + Decay Time: 0 + Enabled: true + Invert Rainbow: false + Max Color: 255; 255; 255 + Min Color: 0; 0; 0 + Name: PointCloud2 + Position Transformer: "" + Queue Size: 10 + Selectable: true + Size (Pixels): 3 + Size (m): 0.009999999776482582 + Style: Flat Squares + Topic: /paf/hero/dist_clustered + Unreliable: false + Use Fixed Frame: true + Use rainbow: true + Value: true + - Alpha: 1 + Autocompute Intensity Bounds: true + Autocompute Value Bounds: + Max Value: 10 + Min Value: -10 + Value: true + Axis: Z + Channel Name: intensity + Class: rviz/PointCloud2 + Color: 255; 255; 255 + Color Transformer: "" + Decay Time: 0 + Enabled: true + Invert Rainbow: false + Max Color: 255; 255; 255 + Min Color: 0; 0; 0 + Name: PointCloud2 dist_clustered + Position Transformer: "" + Queue Size: 10 + Selectable: true + Size (Pixels): 3 + Size (m): 0.009999999776482582 + Style: Flat Squares + Topic: /paf/hero/dist_clustered + Unreliable: false + Use Fixed Frame: true + Use rainbow: true + Value: true Enabled: true Global Options: Background Color: 48; 48; 48 @@ -288,7 +339,7 @@ Visualization Manager: Views: Current: Class: rviz/Orbit - Distance: 34.785499572753906 + Distance: 10.540092468261719 Enable Stereo Rendering: Stereo Eye Separation: 0.05999999865889549 Stereo Focal Distance: 1 @@ -304,9 +355,9 @@ Visualization Manager: Invert Z Axis: false Name: Current View Near Clip Distance: 0.009999999776482582 - Pitch: 0.19039836525917053 + Pitch: 0.3953982889652252 Target Frame: - Yaw: 4.520427227020264 + Yaw: 3.255431652069092 Saved: ~ Window Geometry: Camera: @@ -316,7 +367,7 @@ Window Geometry: Height: 1376 Hide Left Dock: false Hide Right Dock: false - QMainWindow State: 000000ff00000000fd000000040000000000000304000004c6fc0200000009fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003b0000022a000000c700fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000c00430061006d006500720061010000026b000002960000001600ffffff000000010000010f000004c6fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000003b000004c6000000a000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000009b80000003efc0100000002fb0000000800540069006d00650100000000000009b80000030700fffffffb0000000800540069006d0065010000000000000450000000000000000000000599000004c600000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 + QMainWindow State: 000000ff00000000fd000000040000000000000304000004c6fc020000000afb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003b000001c6000000c700fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000c00430061006d00650072006101000002070000021e0000001600fffffffb00000020005600690073006f006e004e006f006400650020004f00750074007000750074010000042b000000d60000001600ffffff000000010000010f000004c6fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000003b000004c6000000a000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000009b80000003efc0100000002fb0000000800540069006d00650100000000000009b80000030700fffffffb0000000800540069006d0065010000000000000450000000000000000000000599000004c600000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 Selection: collapsed: false Time: @@ -325,6 +376,8 @@ Window Geometry: collapsed: false Views: collapsed: false + VisonNode Output: + collapsed: false Width: 2488 X: 1992 - Y: 27 \ No newline at end of file + Y: 27 diff --git a/code/perception/src/lidar_distance.py b/code/perception/src/lidar_distance.py old mode 100755 new mode 100644 index 2842172c..260cbea8 --- a/code/perception/src/lidar_distance.py +++ b/code/perception/src/lidar_distance.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +from joblib import Parallel, delayed import rospy import ros_numpy import numpy as np @@ -6,7 +7,6 @@ from sensor_msgs.msg import PointCloud2, Image as ImageMsg from sklearn.cluster import DBSCAN from cv_bridge import CvBridge -import json # from mpl_toolkits.mplot3d import Axes3D # from itertools import combinations @@ -18,35 +18,159 @@ class LidarDistance: how to configute this node """ + cluster_buffer = [] + def callback(self, data): - """Callback function, filters a PontCloud2 message - by restrictions defined in the launchfile. + """ + Callback-Funktion, die LiDAR-Punktwolkendaten verarbeitet. + + Führt Clustering und Bildberechnungen für die Punktwolken aus. - Publishes a Depth image for the specified camera angle. - Each angle has do be delt with differently since the signs of the - coordinate system change with the view angle. + :param data: LiDAR-Punktwolken als ROS PointCloud2-Nachricht. + """ + + self.start_clustering(data) + self.start_image_calculation(data) - :param data: a PointCloud2 + def listener(self): """ + Initialisiert die ROS-Node, erstellt Publisher/Subscriber und hält sie aktiv. + """ + rospy.init_node("lidar_distance") + self.bridge = CvBridge() # OpenCV-Bridge für Bildkonvertierungen + + # Publisher für gefilterte Punktwolken + self.pub_pointcloud = rospy.Publisher( + rospy.get_param( + "~point_cloud_topic", + "/carla/hero/" + rospy.get_namespace() + "_filtered", + ), + PointCloud2, + queue_size=1, + ) + + # Publisher für Distanzbilder in verschiedene Richtungen + self.dist_array_center_publisher = rospy.Publisher( + rospy.get_param("~image_distance_topic", "/paf/hero/Center/dist_array"), + ImageMsg, + queue_size=10, + ) + self.dist_array_back_publisher = rospy.Publisher( + rospy.get_param("~image_distance_topic", "/paf/hero/Back/dist_array"), + ImageMsg, + queue_size=10, + ) + self.dist_array_lidar_publisher = rospy.Publisher( + rospy.get_param( + "~image_distance_topic_cluster", "/paf/hero/dist_clustered" + ), + PointCloud2, + queue_size=10, + ) + rospy.loginfo("dist_array_lidar_publisher erfolgreich erstellt.") + self.dist_array_left_publisher = rospy.Publisher( + rospy.get_param("~image_distance_topic", "/paf/hero/Left/dist_array"), + ImageMsg, + queue_size=10, + ) + self.dist_array_right_publisher = rospy.Publisher( + rospy.get_param("~image_distance_topic", "/paf/hero/Right/dist_array"), + ImageMsg, + queue_size=10, + ) + + # Subscriber für LiDAR-Daten (Punktwolken) + rospy.Subscriber( + rospy.get_param("~source_topic", "/carla/hero/LIDAR"), + PointCloud2, + self.callback, + ) + + rospy.loginfo("Lidar Processor Node gestartet.") + rospy.spin() + + def start_clustering(self, data): + """ + Filtert LiDAR-Punktwolken, führt Clustering durch und veröffentlicht die kombinierten Cluster. + + :param data: LiDAR-Punktwolken im ROS PointCloud2-Format. + """ + + # Punktwolken filtern, um irrelevante Daten zu entfernen coordinates = ros_numpy.point_cloud2.pointcloud2_to_array(data) + filtered_coordinates = coordinates[ + ~( + (-2 <= coordinates["x"]) + & (coordinates["x"] <= 2) + & (-1 <= coordinates["y"]) + & (coordinates["y"] <= 1) + ) # Ausschluss von Punkten die das eigene Auto betreffen + & ( + coordinates["z"] > -1.7 + 0.05 + ) # Mindesthöhe in z, um die Straße nicht zu clustern + ] + + # Cluster-Daten aus den gefilterten Koordinaten berechnen + clustered_points = cluster_lidar_data_from_pointcloud( + coordinates=filtered_coordinates + ) - # clustered_points = cluster_lidar_data_from_pointcloud(coordinates.copy()) - try: - filtered_coordinates = coordinates[coordinates["z"] > 0] + # Nur gültige Cluster-Daten speichern + if clustered_points: + LidarDistance.cluster_buffer.append(clustered_points) + else: + rospy.logwarn("Keine Cluster-Daten erzeugt.") - # Konvertiere in JSON - clustered_points_json = json.dumps(filtered_coordinates.tolist()) + # Cluster kombinieren + combined_clusters = combine_clusters(LidarDistance.cluster_buffer) - # Veröffentliche JSON-Daten - # self.dist_array_lidar_publisher.publish(clustered_points_json) - rospy.loginfo( - f"JSON mit {len(clustered_points_json)} Punkten veröffentlicht." - ) - rospy.loginfo(clustered_points_json) - except Exception as e: - rospy.logerr(f"Fehler im Callback: {e}") + LidarDistance.cluster_buffer = [] + + # Veröffentliche die kombinierten Cluster + self.publish_clusters(combined_clusters, data.header) + + def publish_clusters(self, combined_clusters, data_header): + """ + Veröffentlicht kombinierte Cluster als ROS PointCloud2-Nachricht. - # Center + :param combined_clusters: Kombinierte Punktwolken der Cluster als strukturiertes NumPy-Array. + :param data_header: Header-Informationen der ROS-Nachricht. + """ + # Konvertiere zu PointCloud2-Nachricht + pointcloud_msg = ros_numpy.point_cloud2.array_to_pointcloud2(combined_clusters) + pointcloud_msg.header = data_header + pointcloud_msg.header.stamp = rospy.Time.now() + # Cluster veröffentlichen + self.dist_array_lidar_publisher.publish(pointcloud_msg) + + def start_image_calculation(self, data): + """ + Berechnet Distanzbilder basierend auf LiDAR-Daten und veröffentlicht sie. + + :param data: LiDAR-Punktwolken im ROS PointCloud2-Format. + """ + coordinates = ros_numpy.point_cloud2.pointcloud2_to_array(data) + # Bildverarbeitung auf den Koordinaten durchführen + processed_images = { + "Center": None, + "Back": None, + "Left": None, + "Right": None, + } + processed_images["Center"] = self.calculate_image_center(coordinates) + processed_images["Back"] = self.calculate_image_back(coordinates) + processed_images["Left"] = self.calculate_image_left(coordinates) + processed_images["Right"] = self.calculate_image_right(coordinates) + + self.publish_images(processed_images, data.header) + + def calculate_image_center(self, coordinates): + """ + Berechnet ein Distanzbild für die zentrale Ansicht aus LiDAR-Koordinaten. + + :param coordinates: Gefilterte LiDAR-Koordinaten als NumPy-Array. + :return: Distanzbild als 2D-Array. + """ reconstruct_bit_mask_center = lidar_filter_utility.bounding_box( coordinates, max_x=np.inf, @@ -62,12 +186,9 @@ def callback(self, data): dist_array_center = self.reconstruct_img_from_lidar( reconstruct_coordinates_xyz_center, focus="Center" ) - dist_array_center_msg = self.bridge.cv2_to_imgmsg( - dist_array_center, encoding="passthrough" - ) - dist_array_center_msg.header = data.header - self.dist_array_center_publisher.publish(dist_array_center_msg) + return dist_array_center + def calculate_image_back(self, coordinates): # Back reconstruct_bit_mask_back = lidar_filter_utility.bounding_box( coordinates, @@ -84,12 +205,9 @@ def callback(self, data): dist_array_back = self.reconstruct_img_from_lidar( reconstruct_coordinates_xyz_back, focus="Back" ) - dist_array_back_msg = self.bridge.cv2_to_imgmsg( - dist_array_back, encoding="passthrough" - ) - dist_array_back_msg.header = data.header - self.dist_array_back_publisher.publish(dist_array_back_msg) + return dist_array_back + def calculate_image_left(self, coordinates): # Left reconstruct_bit_mask_left = lidar_filter_utility.bounding_box( coordinates, @@ -106,12 +224,9 @@ def callback(self, data): dist_array_left = self.reconstruct_img_from_lidar( reconstruct_coordinates_xyz_left, focus="Left" ) - dist_array_left_msg = self.bridge.cv2_to_imgmsg( - dist_array_left, encoding="passthrough" - ) - dist_array_left_msg.header = data.header - self.dist_array_left_publisher.publish(dist_array_left_msg) + return dist_array_left + def calculate_image_right(self, coordinates): # Right reconstruct_bit_mask_right = lidar_filter_utility.bounding_box( coordinates, max_y=-0.0, min_y=-np.inf, min_z=-1.6 @@ -125,124 +240,78 @@ def callback(self, data): dist_array_right = self.reconstruct_img_from_lidar( reconstruct_coordinates_xyz_right, focus="Right" ) - dist_array_right_msg = self.bridge.cv2_to_imgmsg( - dist_array_right, encoding="passthrough" - ) - dist_array_right_msg.header = data.header - self.dist_array_right_publisher.publish(dist_array_right_msg) + return dist_array_right - def listener(self): + def publish_images(self, processed_images, data_header): """ - Initializes the node and it's publishers - """ - # run simultaneously. - rospy.init_node("lidar_distance") - self.bridge = CvBridge() - - self.pub_pointcloud = rospy.Publisher( - rospy.get_param( - "~point_cloud_topic", - "/carla/hero/" + rospy.get_namespace() + "_filtered", - ), - PointCloud2, - queue_size=10, - ) - - # publisher for dist_array - self.dist_array_center_publisher = rospy.Publisher( - rospy.get_param("~image_distance_topic", "/paf/hero/Center/dist_array"), - ImageMsg, - queue_size=10, - ) - - # publisher for dist_array - self.dist_array_back_publisher = rospy.Publisher( - rospy.get_param("~image_distance_topic", "/paf/hero/Back/dist_array"), - ImageMsg, - queue_size=10, - ) + Veröffentlicht Distanzbilder für verschiedene Richtungen als ROS-Bildnachrichten. + :param processed_images: Dictionary mit Richtungen ("Center", "Back", etc.) als Schlüssel und Bildarrays als Werte. + :param data_header: Header der ROS-Bildnachrichten. """ - try: - self.dist_array_lidar_publisher = rospy.Publisher( - rospy.get_param( - "~image_distance_topic_cluster", "/paf/hero/Lidar/dist_clustered" - ), - String, - queue_size=10, + # Nur gültige NumPy-Arrays weiterverarbeiten + for direction, image_array in processed_images.items(): + if not isinstance(image_array, np.ndarray): + continue + + # Konvertiere das Bild in eine ROS-Image-Nachricht + dist_array_msg = self.bridge.cv2_to_imgmsg( + image_array, encoding="passthrough" ) - rospy.loginfo("dist_array_lidar_publisher erfolgreich erstellt.") - except Exception as e: - rospy.logerr(f"Fehler beim Erstellen von dist_array_lidar_publisher: {e}") - """ + dist_array_msg.header = data_header - # publisher for dist_array - self.dist_array_left_publisher = rospy.Publisher( - rospy.get_param("~image_distance_topic", "/paf/hero/Left/dist_array"), - ImageMsg, - queue_size=10, - ) - - # publisher for dist_array - self.dist_array_right_publisher = rospy.Publisher( - rospy.get_param("~image_distance_topic", "/paf/hero/Right/dist_array"), - ImageMsg, - queue_size=10, - ) - - rospy.Subscriber( - rospy.get_param("~source_topic", "/carla/hero/LIDAR"), - PointCloud2, - self.callback, - ) - - rospy.loginfo("Lidar Processor Node gestartet.") - rospy.spin() + if direction == "Center": + self.dist_array_center_publisher.publish(dist_array_msg) + if direction == "Back": + self.dist_array_back_publisher.publish(dist_array_msg) + if direction == "Left": + self.dist_array_left_publisher.publish(dist_array_msg) + if direction == "Right": + self.dist_array_right_publisher.publish(dist_array_msg) def reconstruct_img_from_lidar(self, coordinates_xyz, focus): """ - reconstruct 3d LIDAR-Data and calculate 2D Pixel - according to Camera-World - - Args: - coordinates_xyz (np.array): filtered lidar points - focus (String): Camera Angle + Rekonstruiert ein 2D-Bild aus 3D-LiDAR-Daten für eine gegebene Kameraansicht. - Returns: - image: depth image for camera angle + :param coordinates_xyz: 3D-Koordinaten der gefilterten LiDAR-Punkte. + :param focus: Kameraansicht (z. B. "Center", "Back"). + :return: Rekonstruiertes Bild als 2D-Array. """ - # intrinsic matrix for camera: - # width -> 300, height -> 200, fov -> 100 (agent.py) + # Erstelle die intrinsische Kamera-Matrix basierend auf den Bildparametern (FOV, Auflösung) im = np.identity(3) - im[0, 2] = 1280 / 2.0 - im[1, 2] = 720 / 2.0 - im[0, 0] = im[1, 1] = 1280 / (2.0 * np.tan(100 * np.pi / 360.0)) + im[0, 2] = 1280 / 2.0 # x-Verschiebung (Bildmitte) + im[1, 2] = 720 / 2.0 # y-Verschiebung (Bildmitte) + im[0, 0] = im[1, 1] = 1280 / ( + 2.0 * np.tan(100 * np.pi / 360.0) + ) # Skalierungsfaktor basierend auf FOV - # extrinsic matrix for camera + # Erstelle die extrinsische Kamera-Matrix (Identität für keine Transformation) ex = np.zeros(shape=(3, 4)) - ex[0][0] = 1 - ex[1][1] = 1 - ex[2][2] = 1 - m = np.matmul(im, ex) + ex[0][0] = ex[1][1] = ex[2][2] = 1 + m = np.matmul(im, ex) # Kombiniere intrinsische und extrinsische Matrix - # reconstruct camera image with LIDAR-Data + # Initialisiere leere Bilder für die Rekonstruktion img = np.zeros(shape=(720, 1280), dtype=np.float32) dist_array = np.zeros(shape=(720, 1280, 3), dtype=np.float32) + + # Verarbeite jeden Punkt in der Punktwolke for c in coordinates_xyz: - # center depth image if focus == "Center": point = np.array([c[1], c[2], c[0], 1]) - pixel = np.matmul(m, point) - x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) - if x >= 0 and x <= 1280 and y >= 0 and y <= 720: - img[719 - y][1279 - x] = c[0] + pixel = np.matmul( + m, point + ) # Projiziere 3D-Punkt auf 2D-Bildkoordinaten + x, y = int(pixel[0] / pixel[2]), int( + pixel[1] / pixel[2] + ) # Normalisiere Koordinaten + if x >= 0 and x <= 1280 and y >= 0 and y <= 720: # Prüfe Bildgrenzen + img[719 - y][1279 - x] = c[0] # Setze Tiefenwert dist_array[719 - y][1279 - x] = np.array( [c[0], c[1], c[2]], dtype=np.float32 ) - # back depth image - if focus == "Back": + if focus == "Back": # Berechne Bild für die Rückansicht point = np.array([c[1], c[2], c[0], 1]) pixel = np.matmul(m, point) x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) @@ -252,8 +321,7 @@ def reconstruct_img_from_lidar(self, coordinates_xyz, focus): [-c[0], c[1], c[2]], dtype=np.float32 ) - # left depth image - if focus == "Left": + if focus == "Left": # Berechne Bild für die linke Ansicht point = np.array([c[0], c[2], c[1], 1]) pixel = np.matmul(m, point) x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) @@ -263,8 +331,7 @@ def reconstruct_img_from_lidar(self, coordinates_xyz, focus): [c[0], c[1], c[2]], dtype=np.float32 ) - # right depth image - if focus == "Right": + if focus == "Right": # Berechne Bild für die rechte Ansicht point = np.array([c[0], c[2], c[1], 1]) pixel = np.matmul(m, point) x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) @@ -277,15 +344,154 @@ def reconstruct_img_from_lidar(self, coordinates_xyz, focus): return dist_array -def cluster_lidar_data_from_pointcloud(coordinates, eps=1.0, min_samples=2): +def array_to_pointcloud2(points, header="hero/Lidar"): + """ + Konvertiert ein Array von Punkten in eine ROS PointCloud2-Nachricht. + + :param points: Array von Punkten mit [x, y, z]-Koordinaten. + :param header: Header-Informationen der ROS PointCloud2-Nachricht. + :return: ROS PointCloud2-Nachricht. + """ + # Sicherstellen, dass die Eingabe ein NumPy-Array ist + points_array = np.array(points) + + # Konvertiere die Punkte in ein strukturiertes Array mit Feldern "x", "y", "z" + points_structured = np.array( + [(p[0], p[1], p[2]) for p in points_array], + dtype=[("x", "f4"), ("y", "f4"), ("z", "f4")], + ) + + # Erstelle eine PointCloud2-Nachricht aus dem strukturierten Array + pointcloud_msg = ros_numpy.point_cloud2.array_to_pointcloud2(points_structured) + + # Setze den Zeitstempel und den Header der Nachricht + pointcloud_msg.header.stamp = rospy.Time.now() + pointcloud_msg.header = header + + return pointcloud_msg + + +def get_largest_cluster(clustered_points, return_structured=True): + """ + Ermittelt das größte Cluster aus gegebenen Punktwolken-Clustern. + + :param clustered_points: Dictionary mit Cluster-IDs und zugehörigen Punktwolken. + :param return_structured: Gibt ein strukturiertes NumPy-Array zurück, falls True. + :return: Größtes Cluster als NumPy-Array (roh oder strukturiert). + """ + # Prüfen, ob es Cluster gibt + if not clustered_points: + return np.array([]) + + # Identifiziere das größte Cluster basierend auf der Anzahl der Punkte + largest_cluster_id, largest_cluster = max( + clustered_points.items(), key=lambda item: len(item[1]) + ) + + # Sicherstellen, dass das größte Cluster nicht leer ist + if largest_cluster.size == 0: + return np.array([]) + + rospy.loginfo( + f"Largest cluster: {largest_cluster_id} with {largest_cluster.shape[0]} points" + ) + + # Rohdaten zurückgeben, wenn kein strukturiertes Array benötigt wird + if not return_structured: + return largest_cluster + + # Konvertiere das größte Cluster in ein strukturiertes Array + points_structured = np.empty( + largest_cluster.shape[0], dtype=[("x", "f4"), ("y", "f4"), ("z", "f4")] + ) + points_structured["x"] = largest_cluster[:, 0] + points_structured["y"] = largest_cluster[:, 1] + points_structured["z"] = largest_cluster[:, 2] + + return points_structured + - clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(coordinates) +def combine_clusters(cluster_buffer): + """ + Kombiniert Cluster aus mehreren Punktwolken zu einem strukturierten NumPy-Array. + + :param cluster_buffer: Liste von Dictionaries mit Cluster-IDs und Punktwolken. + :return: Kombiniertes strukturiertes NumPy-Array mit Feldern "x", "y", "z", "cluster_id". + """ + points_list = [] + cluster_ids_list = [] + + for clustered_points in cluster_buffer: + for cluster_id, points in clustered_points.items(): + if points.size > 0: # Ignoriere leere Cluster + points_list.append(points) + # Erstelle ein Array mit der Cluster-ID für alle Punkte des Clusters + cluster_ids_list.append( + np.full(points.shape[0], cluster_id, dtype=np.float32) + ) + + if not points_list: # Falls keine Punkte vorhanden sind + return np.array( + [], dtype=[("x", "f4"), ("y", "f4"), ("z", "f4"), ("cluster_id", "f4")] + ) + + # Kombiniere alle Punkte und Cluster-IDs in zwei separate Arrays + all_points = np.vstack(points_list) + all_cluster_ids = np.concatenate(cluster_ids_list) + + # Erstelle ein strukturiertes Array für die kombinierten Daten + combined_points = np.zeros( + all_points.shape[0], + dtype=[("x", "f4"), ("y", "f4"), ("z", "f4"), ("cluster_id", "f4")], + ) + combined_points["x"] = all_points[:, 0] + combined_points["y"] = all_points[:, 1] + combined_points["z"] = all_points[:, 2] + combined_points["cluster_id"] = all_cluster_ids + + return combined_points + + +def cluster_lidar_data_from_pointcloud(coordinates, eps=0.3, min_samples=10): + """ + Führt Clustering auf LiDAR-Daten mit DBSCAN durch und gibt Cluster zurück. + + :param coordinates: LiDAR-Punktwolken als NumPy-Array mit "x", "y", "z". + :param eps: Maximaler Abstand zwischen Punkten, um sie zu einem Cluster zuzuordnen. + :param min_samples: Minimale Anzahl von Punkten, um ein Cluster zu bilden. + :return: Dictionary mit Cluster-IDs und zugehörigen Punktwolken. + """ + if coordinates.shape[0] == 0: + rospy.logerr("Das Eingabe-Array 'coordinates' ist leer.") + return {} + + # Extrahiere x, y und z aus den Koordinaten für die DBSCAN-Berechnung + xyz = np.column_stack((coordinates["x"], coordinates["y"], coordinates["z"])) + + if xyz.shape[0] == 0: + rospy.logwarn("Keine Datenpunkte für DBSCAN verfügbar. Überspringe Clustering.") + return {} + + # Wende DBSCAN an, um Cluster-Labels für die Punktwolke zu berechnen + clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(xyz) labels = clustering.labels_ - clustered_points = {label: list(labels).count(label) for label in set(labels)} - clustered_points = {int(label): count for label, count in clustered_points.items()} - return clustered_points + + # Entferne Rauschen (Cluster-ID: -1) und bestimme gültige Cluster-IDs + unique_labels = np.unique(labels) + valid_labels = unique_labels[unique_labels != -1] + + # Erstelle ein Dictionary mit Cluster-IDs und den zugehörigen Punkten + clusters = Parallel(n_jobs=-1)( + delayed(lambda l: (l, xyz[labels == l]))(label) for label in valid_labels + ) + clusters = dict(clusters) + + return clusters if __name__ == "__main__": + """ + Initialisiert die LidarDistance-Klasse und startet die Listener-Methode. + """ lidar_distance = LidarDistance() lidar_distance.listener() diff --git a/code/perception/src/lidar_distance_neu.py b/code/perception/src/lidar_distance_neu.py new file mode 100755 index 00000000..57892ed6 --- /dev/null +++ b/code/perception/src/lidar_distance_neu.py @@ -0,0 +1,516 @@ +#!/usr/bin/env python +import rospy +import ros_numpy +import numpy as np +import lidar_filter_utility +from sensor_msgs.msg import PointCloud2, Image as ImageMsg +from sklearn.cluster import DBSCAN +from cv_bridge import CvBridge +from joblib import Parallel, delayed +import queue +import threading + +# from mpl_toolkits.mplot3d import Axes3D +# from itertools import combinations +# from matplotlib.colors import LinearSegmentedColormap + + +class LidarDistance: + """See doc/perception/lidar_distance_utility.md on + how to configute this node + """ + + # Klassenattribute + cluster_queue = queue.Queue() + image_queue = queue.Queue() + workers_initialized = False + cluster_data_list = [] + + def callback(self, data): + """Callback function, filters a PontCloud2 message + by restrictions defined in the launchfile. + + Publishes a Depth image for the specified camera angle. + Each angle has do be delt with differently since the signs of the + coordinate system change with the view angle. + + :param data: a PointCloud2 + """ + + if not LidarDistance.workers_initialized: + LidarDistance.workers_initialized = True + self.start_image_worker() + self.start_cluster_worker() + + if not self.cluster_thread.is_alive: + rospy.logwarn("Cluster-Worker abgestürzt. Neustart...") + self.start_cluster_worker() + + if not self.image_thread.is_alive: + rospy.logwarn("Image-Worker abgestürzt. Neustart...") + self.start_image_worker() + + LidarDistance.cluster_queue.put(data) + if LidarDistance.cluster_queue.qsize() > 10: + rospy.logwarn( + "Cluster-Worker kommt nicht hinterher. Queue size:" + + str(LidarDistance.cluster_queue.qsize()), + ) + # Dasselbe Koordinatenset für die Bildberechnung in die Bild-Warteschlange legen + LidarDistance.image_queue.put(data) + if LidarDistance.image_queue.qsize() > 10: + rospy.logwarn( + f"Image-Worker ({self.image_thread.is_alive}) kommt nicht hinterher. Queue size: {str(LidarDistance.image_queue.qsize())}" + ) + + def listener(self): + """ + Initializes the node and it's publishers + """ + # run simultaneously. + rospy.init_node("lidar_distance") + self.bridge = CvBridge() + + self.pub_pointcloud = rospy.Publisher( + rospy.get_param( + "~point_cloud_topic", + "/carla/hero/" + rospy.get_namespace() + "_filtered", + ), + PointCloud2, + queue_size=10, + ) + + # publisher for dist_array + self.dist_array_center_publisher = rospy.Publisher( + rospy.get_param("~image_distance_topic", "/paf/hero/Center/dist_array"), + ImageMsg, + queue_size=10, + ) + + # publisher for dist_array + self.dist_array_back_publisher = rospy.Publisher( + rospy.get_param("~image_distance_topic", "/paf/hero/Back/dist_array"), + ImageMsg, + queue_size=10, + ) + + self.dist_array_lidar_publisher = rospy.Publisher( + rospy.get_param( + "~image_distance_topic_cluster", "/paf/hero/dist_clustered" + ), + PointCloud2, + queue_size=10, + ) + rospy.loginfo("dist_array_lidar_publisher erfolgreich erstellt.") + + # publisher for dist_array + self.dist_array_left_publisher = rospy.Publisher( + rospy.get_param("~image_distance_topic", "/paf/hero/Left/dist_array"), + ImageMsg, + queue_size=10, + ) + + # publisher for dist_array + self.dist_array_right_publisher = rospy.Publisher( + rospy.get_param("~image_distance_topic", "/paf/hero/Right/dist_array"), + ImageMsg, + queue_size=10, + ) + + rospy.Subscriber( + rospy.get_param("~source_topic", "/carla/hero/LIDAR"), + PointCloud2, + self.callback, + ) + + rospy.loginfo("Lidar Processor Node gestartet.") + rospy.spin() + + def cluster_worker(self): + """ + Worker für das Clustern von Punkten. + """ + while True: + try: + # Neueste Aufgabe holen + data = LidarDistance.cluster_queue.get(timeout=1) + + # Restliche Aufgaben aus der Queue entfernen + discarded_tasks = 0 + while not LidarDistance.cluster_queue.empty(): + LidarDistance.cluster_queue.get() + discarded_tasks += 1 + + # Anzahl verworfener Einträge loggen + if discarded_tasks > 0: + rospy.logwarn(f"{discarded_tasks} Einträge wurden verworfen.") + + # Stop-Signal überprüfen + if data is None: # Stop-Signal + break + + # Punktwolken-Daten verarbeiten + coordinates = ros_numpy.point_cloud2.pointcloud2_to_array(data) + filtered_coordinates = coordinates[(coordinates["x"] >= 2)] + clustered_points = cluster_lidar_data_from_pointcloud( + coordinates=filtered_coordinates + ) + + # Kombinierte Cluster erstellen + cluster_structured = combine_clusters(clustered_points) + + # Konvertiere zu PointCloud2-Nachricht + pointcloud_msg = ros_numpy.point_cloud2.array_to_pointcloud2( + cluster_structured + ) + pointcloud_msg.header = data.header + pointcloud_msg.header.stamp = rospy.Time.now() + + # Cluster veröffentlichen + self.dist_array_lidar_publisher.publish(pointcloud_msg) + + except queue.Empty: + rospy.loginfo("Cluster-Queue ist leer. Warte auf neue Aufgaben.") + continue # Zurück zur Queue-Warte + + except Exception as e: + rospy.logerr(f"Fehler im Cluster-Worker: {e}") + + def image_worker(self): + """ + Worker für die Berechnung von Bildern. + """ + while True: + data = LidarDistance.image_queue.get() + if data is None: # Stop-Signal + break + + coordinates = ros_numpy.point_cloud2.pointcloud2_to_array(data) + # Bildverarbeitung auf den Koordinaten durchführen + processed_images = { + "Center": None, + "Back": None, + "Left": None, + "Right": None, + } + processed_images["Center"] = self.calculate_image_center(coordinates) + processed_images["Back"] = self.calculate_image_back(coordinates) + processed_images["Left"] = self.calculate_image_left(coordinates) + processed_images["Right"] = self.calculate_image_right(coordinates) + + self.publish_images(processed_images, data.header) + + def start_cluster_worker(self): + self.cluster_thread = threading.Thread(target=self.cluster_worker, daemon=True) + self.cluster_thread.start() + + def start_image_worker(self): + self.image_thread = threading.Thread(target=self.image_worker, daemon=True) + self.image_thread.start() + + def calculate_image_center(self, coordinates): + reconstruct_bit_mask_center = lidar_filter_utility.bounding_box( + coordinates, + max_x=np.inf, + min_x=0.0, + min_z=-1.6, + ) + reconstruct_coordinates_center = coordinates[reconstruct_bit_mask_center] + reconstruct_coordinates_xyz_center = np.array( + lidar_filter_utility.remove_field_name( + reconstruct_coordinates_center, "intensity" + ).tolist() + ) + dist_array_center = self.reconstruct_img_from_lidar( + reconstruct_coordinates_xyz_center, focus="Center" + ) + return dist_array_center + + def calculate_image_back(self, coordinates): + # Back + reconstruct_bit_mask_back = lidar_filter_utility.bounding_box( + coordinates, + max_x=0.0, + min_x=-np.inf, + min_z=-1.6, + ) + reconstruct_coordinates_back = coordinates[reconstruct_bit_mask_back] + reconstruct_coordinates_xyz_back = np.array( + lidar_filter_utility.remove_field_name( + reconstruct_coordinates_back, "intensity" + ).tolist() + ) + dist_array_back = self.reconstruct_img_from_lidar( + reconstruct_coordinates_xyz_back, focus="Back" + ) + return dist_array_back + + def calculate_image_left(self, coordinates): + # Left + reconstruct_bit_mask_left = lidar_filter_utility.bounding_box( + coordinates, + max_y=np.inf, + min_y=0.0, + min_z=-1.6, + ) + reconstruct_coordinates_left = coordinates[reconstruct_bit_mask_left] + reconstruct_coordinates_xyz_left = np.array( + lidar_filter_utility.remove_field_name( + reconstruct_coordinates_left, "intensity" + ).tolist() + ) + dist_array_left = self.reconstruct_img_from_lidar( + reconstruct_coordinates_xyz_left, focus="Left" + ) + return dist_array_left + + def calculate_image_right(self, coordinates): + # Right + reconstruct_bit_mask_right = lidar_filter_utility.bounding_box( + coordinates, max_y=-0.0, min_y=-np.inf, min_z=-1.6 + ) + reconstruct_coordinates_right = coordinates[reconstruct_bit_mask_right] + reconstruct_coordinates_xyz_right = np.array( + lidar_filter_utility.remove_field_name( + reconstruct_coordinates_right, "intensity" + ).tolist() + ) + dist_array_right = self.reconstruct_img_from_lidar( + reconstruct_coordinates_xyz_right, focus="Right" + ) + return dist_array_right + + def publish_images(self, processed_images, data_header): + for direction, image_array in processed_images.items(): + if not isinstance(image_array, np.ndarray): + continue + dist_array_msg = self.bridge.cv2_to_imgmsg( + image_array, encoding="passthrough" + ) + dist_array_msg.header = data_header + + if direction == "Center": + self.dist_array_center_publisher.publish(dist_array_msg) + if direction == "Back": + self.dist_array_back_publisher.publish(dist_array_msg) + if direction == "Left": + self.dist_array_left_publisher.publish(dist_array_msg) + if direction == "Right": + self.dist_array_right_publisher.publish(dist_array_msg) + + def reconstruct_img_from_lidar(self, coordinates_xyz, focus): + """ + reconstruct 3d LIDAR-Data and calculate 2D Pixel + according to Camera-World + + Args: + coordinates_xyz (np.array): filtered lidar points + focus (String): Camera Angle + + Returns: + image: depth image for camera angle + """ + + # intrinsic matrix for camera: + # width -> 300, height -> 200, fov -> 100 (agent.py) + im = np.identity(3) + im[0, 2] = 1280 / 2.0 + im[1, 2] = 720 / 2.0 + im[0, 0] = im[1, 1] = 1280 / (2.0 * np.tan(100 * np.pi / 360.0)) + + # extrinsic matrix for camera + ex = np.zeros(shape=(3, 4)) + ex[0][0] = 1 + ex[1][1] = 1 + ex[2][2] = 1 + m = np.matmul(im, ex) + + # reconstruct camera image with LIDAR-Data + img = np.zeros(shape=(720, 1280), dtype=np.float32) + dist_array = np.zeros(shape=(720, 1280, 3), dtype=np.float32) + for c in coordinates_xyz: + # center depth image + if focus == "Center": + point = np.array([c[1], c[2], c[0], 1]) + pixel = np.matmul(m, point) + x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) + if x >= 0 and x <= 1280 and y >= 0 and y <= 720: + img[719 - y][1279 - x] = c[0] + dist_array[719 - y][1279 - x] = np.array( + [c[0], c[1], c[2]], dtype=np.float32 + ) + + # back depth image + if focus == "Back": + point = np.array([c[1], c[2], c[0], 1]) + pixel = np.matmul(m, point) + x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) + if x >= 0 and x <= 1280 and y >= 0 and y < 720: + img[y][1279 - x] = -c[0] + dist_array[y][1279 - x] = np.array( + [-c[0], c[1], c[2]], dtype=np.float32 + ) + + # left depth image + if focus == "Left": + point = np.array([c[0], c[2], c[1], 1]) + pixel = np.matmul(m, point) + x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) + if x >= 0 and x <= 1280 and y >= 0 and y <= 720: + img[719 - y][1279 - x] = c[1] + dist_array[y][1279 - x] = np.array( + [c[0], c[1], c[2]], dtype=np.float32 + ) + + # right depth image + if focus == "Right": + point = np.array([c[0], c[2], c[1], 1]) + pixel = np.matmul(m, point) + x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) + if x >= 0 and x < 1280 and y >= 0 and y < 720: + img[y][1279 - x] = -c[1] + dist_array[y][1279 - x] = np.array( + [c[0], c[1], c[2]], dtype=np.float32 + ) + + return dist_array + + +def array_to_pointcloud2(points, header="hero/Lidar"): + points_array = np.array(points) + + points_structured = np.array( + [(p[0], p[1], p[2]) for p in points_array], + dtype=[("x", "f4"), ("y", "f4"), ("z", "f4")], + ) + + pointcloud_msg = ros_numpy.point_cloud2.array_to_pointcloud2(points_structured) + + pointcloud_msg.header.stamp = rospy.Time.now() + pointcloud_msg.header = header + + return pointcloud_msg + + +def get_largest_cluster(clustered_points, return_structured=True): + """ + Bestimmt das größte Cluster aus den gegebenen Punkten und gibt es zurück. + + :param clustered_points: Dictionary mit Cluster-IDs als Schlüssel und Punktwolken als Werte. + :param return_structured: Gibt ein strukturiertes Array zurück, falls True. + :return: Größtes Cluster als Array (entweder raw oder strukturiert). + """ + if not clustered_points: + rospy.logerr( + "Clustered points are empty. Cannot determine the largest cluster." + ) + return np.array([]) + + # Finde das größte Cluster direkt + largest_cluster_id, largest_cluster = max( + clustered_points.items(), key=lambda item: len(item[1]) + ) + + if largest_cluster.size == 0: + rospy.logerr(f"Largest cluster (ID {largest_cluster_id}) is empty.") + return np.array([]) + + rospy.loginfo( + f"Largest cluster: {largest_cluster_id} with {largest_cluster.shape[0]} points" + ) + + # Falls kein strukturiertes Array benötigt wird, direkt zurückgeben + if not return_structured: + return largest_cluster + + # Andernfalls Punkte in ein strukturiertes Array konvertieren + points_structured = np.empty( + largest_cluster.shape[0], dtype=[("x", "f4"), ("y", "f4"), ("z", "f4")] + ) + points_structured["x"] = largest_cluster[:, 0] + points_structured["y"] = largest_cluster[:, 1] + points_structured["z"] = largest_cluster[:, 2] + + return points_structured + + +def combine_clusters(clustered_points): + """ + Kombiniert Cluster-Daten in ein strukturiertes Array. + + :param clustered_points: Dictionary mit Cluster-IDs als Schlüssel und 3D-Punktwolken als Werten. + :return: Kombiniertes strukturiertes NumPy-Array mit Feldern "x", "y", "z", "cluster_id". + """ + + # Vorab die Gesamtanzahl der Punkte berechnen + total_points = sum(points.shape[0] for points in clustered_points.values()) + + # Erstelle ein leeres strukturiertes Array + combined_points = np.empty( + total_points, + dtype=[("x", "f4"), ("y", "f4"), ("z", "f4"), ("cluster_id", "f4")], + ) + + # Fülle das Array direkt + index = 0 + for cluster_id, points in clustered_points.items(): + num_points = points.shape[0] + combined_points["x"][index : index + num_points] = points[:, 0] + combined_points["y"][index : index + num_points] = points[:, 1] + combined_points["z"][index : index + num_points] = points[:, 2] + combined_points["cluster_id"][index : index + num_points] = cluster_id + index += num_points + + return combined_points + + +def fuse_clusters(cluster_data_list): + """ + Fusioniert eine Liste von Cluster-Daten und entfernt redundante Punkte basierend auf (x, y, z). + + :param cluster_data_list: Liste von NumPy-Arrays mit Feldern (x, y, z, intensity) + :return: Fusioniertes NumPy-Array mit einzigartigen Punkten + """ + # Kombiniere alle Arrays in ein großes Array + concatenated_points = np.concatenate(cluster_data_list, axis=0) + + # Erstelle eine flache Ansicht für die Felder (x, y, z) + unique_xyz, indices = np.unique( + concatenated_points[["x", "y", "z"]].view(np.float32).reshape(-1, 3), + axis=0, + return_index=True, + ) + + # Behalte die entsprechenden `intensity`-Werte basierend auf den eindeutigen Indizes + unique_points = concatenated_points[indices] + + return unique_points + + +def cluster_lidar_data_from_pointcloud(coordinates, eps=0.1, min_samples=10): + """ + Cluster LiDAR-Daten effizient mit DBSCAN. + """ + # Erstelle Matrix direkt mit column_stack (schneller als vstack) + xyz = np.column_stack((coordinates["x"], coordinates["y"], coordinates["z"])) + + # DBSCAN-Cluster-Berechnung + clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(xyz) + labels = clustering.labels_ + + # Nur eindeutige Cluster (Rauschen entfernen) + unique_labels = np.unique(labels) + valid_labels = unique_labels[unique_labels != -1] + + # Parallelisiertes Extrahieren der Cluster + clusters = Parallel(n_jobs=-1)( + delayed(lambda l: (l, xyz[labels == l]))(label) for label in valid_labels + ) + clusters = dict(clusters) + + return clusters + + +if __name__ == "__main__": + lidar_distance = LidarDistance() + lidar_distance.listener() From 2aae9be231e3cde0e545ea4b1adf733c942598c9 Mon Sep 17 00:00:00 2001 From: michalal7 Date: Wed, 11 Dec 2024 14:49:52 +0100 Subject: [PATCH 25/55] Deleted file duplicate. Refactored the existing node code by modularizing it into functions for better readability and maintainability. --- code/perception/src/lidar_distance_neu.py | 516 ---------------------- 1 file changed, 516 deletions(-) delete mode 100755 code/perception/src/lidar_distance_neu.py diff --git a/code/perception/src/lidar_distance_neu.py b/code/perception/src/lidar_distance_neu.py deleted file mode 100755 index 57892ed6..00000000 --- a/code/perception/src/lidar_distance_neu.py +++ /dev/null @@ -1,516 +0,0 @@ -#!/usr/bin/env python -import rospy -import ros_numpy -import numpy as np -import lidar_filter_utility -from sensor_msgs.msg import PointCloud2, Image as ImageMsg -from sklearn.cluster import DBSCAN -from cv_bridge import CvBridge -from joblib import Parallel, delayed -import queue -import threading - -# from mpl_toolkits.mplot3d import Axes3D -# from itertools import combinations -# from matplotlib.colors import LinearSegmentedColormap - - -class LidarDistance: - """See doc/perception/lidar_distance_utility.md on - how to configute this node - """ - - # Klassenattribute - cluster_queue = queue.Queue() - image_queue = queue.Queue() - workers_initialized = False - cluster_data_list = [] - - def callback(self, data): - """Callback function, filters a PontCloud2 message - by restrictions defined in the launchfile. - - Publishes a Depth image for the specified camera angle. - Each angle has do be delt with differently since the signs of the - coordinate system change with the view angle. - - :param data: a PointCloud2 - """ - - if not LidarDistance.workers_initialized: - LidarDistance.workers_initialized = True - self.start_image_worker() - self.start_cluster_worker() - - if not self.cluster_thread.is_alive: - rospy.logwarn("Cluster-Worker abgestürzt. Neustart...") - self.start_cluster_worker() - - if not self.image_thread.is_alive: - rospy.logwarn("Image-Worker abgestürzt. Neustart...") - self.start_image_worker() - - LidarDistance.cluster_queue.put(data) - if LidarDistance.cluster_queue.qsize() > 10: - rospy.logwarn( - "Cluster-Worker kommt nicht hinterher. Queue size:" - + str(LidarDistance.cluster_queue.qsize()), - ) - # Dasselbe Koordinatenset für die Bildberechnung in die Bild-Warteschlange legen - LidarDistance.image_queue.put(data) - if LidarDistance.image_queue.qsize() > 10: - rospy.logwarn( - f"Image-Worker ({self.image_thread.is_alive}) kommt nicht hinterher. Queue size: {str(LidarDistance.image_queue.qsize())}" - ) - - def listener(self): - """ - Initializes the node and it's publishers - """ - # run simultaneously. - rospy.init_node("lidar_distance") - self.bridge = CvBridge() - - self.pub_pointcloud = rospy.Publisher( - rospy.get_param( - "~point_cloud_topic", - "/carla/hero/" + rospy.get_namespace() + "_filtered", - ), - PointCloud2, - queue_size=10, - ) - - # publisher for dist_array - self.dist_array_center_publisher = rospy.Publisher( - rospy.get_param("~image_distance_topic", "/paf/hero/Center/dist_array"), - ImageMsg, - queue_size=10, - ) - - # publisher for dist_array - self.dist_array_back_publisher = rospy.Publisher( - rospy.get_param("~image_distance_topic", "/paf/hero/Back/dist_array"), - ImageMsg, - queue_size=10, - ) - - self.dist_array_lidar_publisher = rospy.Publisher( - rospy.get_param( - "~image_distance_topic_cluster", "/paf/hero/dist_clustered" - ), - PointCloud2, - queue_size=10, - ) - rospy.loginfo("dist_array_lidar_publisher erfolgreich erstellt.") - - # publisher for dist_array - self.dist_array_left_publisher = rospy.Publisher( - rospy.get_param("~image_distance_topic", "/paf/hero/Left/dist_array"), - ImageMsg, - queue_size=10, - ) - - # publisher for dist_array - self.dist_array_right_publisher = rospy.Publisher( - rospy.get_param("~image_distance_topic", "/paf/hero/Right/dist_array"), - ImageMsg, - queue_size=10, - ) - - rospy.Subscriber( - rospy.get_param("~source_topic", "/carla/hero/LIDAR"), - PointCloud2, - self.callback, - ) - - rospy.loginfo("Lidar Processor Node gestartet.") - rospy.spin() - - def cluster_worker(self): - """ - Worker für das Clustern von Punkten. - """ - while True: - try: - # Neueste Aufgabe holen - data = LidarDistance.cluster_queue.get(timeout=1) - - # Restliche Aufgaben aus der Queue entfernen - discarded_tasks = 0 - while not LidarDistance.cluster_queue.empty(): - LidarDistance.cluster_queue.get() - discarded_tasks += 1 - - # Anzahl verworfener Einträge loggen - if discarded_tasks > 0: - rospy.logwarn(f"{discarded_tasks} Einträge wurden verworfen.") - - # Stop-Signal überprüfen - if data is None: # Stop-Signal - break - - # Punktwolken-Daten verarbeiten - coordinates = ros_numpy.point_cloud2.pointcloud2_to_array(data) - filtered_coordinates = coordinates[(coordinates["x"] >= 2)] - clustered_points = cluster_lidar_data_from_pointcloud( - coordinates=filtered_coordinates - ) - - # Kombinierte Cluster erstellen - cluster_structured = combine_clusters(clustered_points) - - # Konvertiere zu PointCloud2-Nachricht - pointcloud_msg = ros_numpy.point_cloud2.array_to_pointcloud2( - cluster_structured - ) - pointcloud_msg.header = data.header - pointcloud_msg.header.stamp = rospy.Time.now() - - # Cluster veröffentlichen - self.dist_array_lidar_publisher.publish(pointcloud_msg) - - except queue.Empty: - rospy.loginfo("Cluster-Queue ist leer. Warte auf neue Aufgaben.") - continue # Zurück zur Queue-Warte - - except Exception as e: - rospy.logerr(f"Fehler im Cluster-Worker: {e}") - - def image_worker(self): - """ - Worker für die Berechnung von Bildern. - """ - while True: - data = LidarDistance.image_queue.get() - if data is None: # Stop-Signal - break - - coordinates = ros_numpy.point_cloud2.pointcloud2_to_array(data) - # Bildverarbeitung auf den Koordinaten durchführen - processed_images = { - "Center": None, - "Back": None, - "Left": None, - "Right": None, - } - processed_images["Center"] = self.calculate_image_center(coordinates) - processed_images["Back"] = self.calculate_image_back(coordinates) - processed_images["Left"] = self.calculate_image_left(coordinates) - processed_images["Right"] = self.calculate_image_right(coordinates) - - self.publish_images(processed_images, data.header) - - def start_cluster_worker(self): - self.cluster_thread = threading.Thread(target=self.cluster_worker, daemon=True) - self.cluster_thread.start() - - def start_image_worker(self): - self.image_thread = threading.Thread(target=self.image_worker, daemon=True) - self.image_thread.start() - - def calculate_image_center(self, coordinates): - reconstruct_bit_mask_center = lidar_filter_utility.bounding_box( - coordinates, - max_x=np.inf, - min_x=0.0, - min_z=-1.6, - ) - reconstruct_coordinates_center = coordinates[reconstruct_bit_mask_center] - reconstruct_coordinates_xyz_center = np.array( - lidar_filter_utility.remove_field_name( - reconstruct_coordinates_center, "intensity" - ).tolist() - ) - dist_array_center = self.reconstruct_img_from_lidar( - reconstruct_coordinates_xyz_center, focus="Center" - ) - return dist_array_center - - def calculate_image_back(self, coordinates): - # Back - reconstruct_bit_mask_back = lidar_filter_utility.bounding_box( - coordinates, - max_x=0.0, - min_x=-np.inf, - min_z=-1.6, - ) - reconstruct_coordinates_back = coordinates[reconstruct_bit_mask_back] - reconstruct_coordinates_xyz_back = np.array( - lidar_filter_utility.remove_field_name( - reconstruct_coordinates_back, "intensity" - ).tolist() - ) - dist_array_back = self.reconstruct_img_from_lidar( - reconstruct_coordinates_xyz_back, focus="Back" - ) - return dist_array_back - - def calculate_image_left(self, coordinates): - # Left - reconstruct_bit_mask_left = lidar_filter_utility.bounding_box( - coordinates, - max_y=np.inf, - min_y=0.0, - min_z=-1.6, - ) - reconstruct_coordinates_left = coordinates[reconstruct_bit_mask_left] - reconstruct_coordinates_xyz_left = np.array( - lidar_filter_utility.remove_field_name( - reconstruct_coordinates_left, "intensity" - ).tolist() - ) - dist_array_left = self.reconstruct_img_from_lidar( - reconstruct_coordinates_xyz_left, focus="Left" - ) - return dist_array_left - - def calculate_image_right(self, coordinates): - # Right - reconstruct_bit_mask_right = lidar_filter_utility.bounding_box( - coordinates, max_y=-0.0, min_y=-np.inf, min_z=-1.6 - ) - reconstruct_coordinates_right = coordinates[reconstruct_bit_mask_right] - reconstruct_coordinates_xyz_right = np.array( - lidar_filter_utility.remove_field_name( - reconstruct_coordinates_right, "intensity" - ).tolist() - ) - dist_array_right = self.reconstruct_img_from_lidar( - reconstruct_coordinates_xyz_right, focus="Right" - ) - return dist_array_right - - def publish_images(self, processed_images, data_header): - for direction, image_array in processed_images.items(): - if not isinstance(image_array, np.ndarray): - continue - dist_array_msg = self.bridge.cv2_to_imgmsg( - image_array, encoding="passthrough" - ) - dist_array_msg.header = data_header - - if direction == "Center": - self.dist_array_center_publisher.publish(dist_array_msg) - if direction == "Back": - self.dist_array_back_publisher.publish(dist_array_msg) - if direction == "Left": - self.dist_array_left_publisher.publish(dist_array_msg) - if direction == "Right": - self.dist_array_right_publisher.publish(dist_array_msg) - - def reconstruct_img_from_lidar(self, coordinates_xyz, focus): - """ - reconstruct 3d LIDAR-Data and calculate 2D Pixel - according to Camera-World - - Args: - coordinates_xyz (np.array): filtered lidar points - focus (String): Camera Angle - - Returns: - image: depth image for camera angle - """ - - # intrinsic matrix for camera: - # width -> 300, height -> 200, fov -> 100 (agent.py) - im = np.identity(3) - im[0, 2] = 1280 / 2.0 - im[1, 2] = 720 / 2.0 - im[0, 0] = im[1, 1] = 1280 / (2.0 * np.tan(100 * np.pi / 360.0)) - - # extrinsic matrix for camera - ex = np.zeros(shape=(3, 4)) - ex[0][0] = 1 - ex[1][1] = 1 - ex[2][2] = 1 - m = np.matmul(im, ex) - - # reconstruct camera image with LIDAR-Data - img = np.zeros(shape=(720, 1280), dtype=np.float32) - dist_array = np.zeros(shape=(720, 1280, 3), dtype=np.float32) - for c in coordinates_xyz: - # center depth image - if focus == "Center": - point = np.array([c[1], c[2], c[0], 1]) - pixel = np.matmul(m, point) - x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) - if x >= 0 and x <= 1280 and y >= 0 and y <= 720: - img[719 - y][1279 - x] = c[0] - dist_array[719 - y][1279 - x] = np.array( - [c[0], c[1], c[2]], dtype=np.float32 - ) - - # back depth image - if focus == "Back": - point = np.array([c[1], c[2], c[0], 1]) - pixel = np.matmul(m, point) - x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) - if x >= 0 and x <= 1280 and y >= 0 and y < 720: - img[y][1279 - x] = -c[0] - dist_array[y][1279 - x] = np.array( - [-c[0], c[1], c[2]], dtype=np.float32 - ) - - # left depth image - if focus == "Left": - point = np.array([c[0], c[2], c[1], 1]) - pixel = np.matmul(m, point) - x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) - if x >= 0 and x <= 1280 and y >= 0 and y <= 720: - img[719 - y][1279 - x] = c[1] - dist_array[y][1279 - x] = np.array( - [c[0], c[1], c[2]], dtype=np.float32 - ) - - # right depth image - if focus == "Right": - point = np.array([c[0], c[2], c[1], 1]) - pixel = np.matmul(m, point) - x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) - if x >= 0 and x < 1280 and y >= 0 and y < 720: - img[y][1279 - x] = -c[1] - dist_array[y][1279 - x] = np.array( - [c[0], c[1], c[2]], dtype=np.float32 - ) - - return dist_array - - -def array_to_pointcloud2(points, header="hero/Lidar"): - points_array = np.array(points) - - points_structured = np.array( - [(p[0], p[1], p[2]) for p in points_array], - dtype=[("x", "f4"), ("y", "f4"), ("z", "f4")], - ) - - pointcloud_msg = ros_numpy.point_cloud2.array_to_pointcloud2(points_structured) - - pointcloud_msg.header.stamp = rospy.Time.now() - pointcloud_msg.header = header - - return pointcloud_msg - - -def get_largest_cluster(clustered_points, return_structured=True): - """ - Bestimmt das größte Cluster aus den gegebenen Punkten und gibt es zurück. - - :param clustered_points: Dictionary mit Cluster-IDs als Schlüssel und Punktwolken als Werte. - :param return_structured: Gibt ein strukturiertes Array zurück, falls True. - :return: Größtes Cluster als Array (entweder raw oder strukturiert). - """ - if not clustered_points: - rospy.logerr( - "Clustered points are empty. Cannot determine the largest cluster." - ) - return np.array([]) - - # Finde das größte Cluster direkt - largest_cluster_id, largest_cluster = max( - clustered_points.items(), key=lambda item: len(item[1]) - ) - - if largest_cluster.size == 0: - rospy.logerr(f"Largest cluster (ID {largest_cluster_id}) is empty.") - return np.array([]) - - rospy.loginfo( - f"Largest cluster: {largest_cluster_id} with {largest_cluster.shape[0]} points" - ) - - # Falls kein strukturiertes Array benötigt wird, direkt zurückgeben - if not return_structured: - return largest_cluster - - # Andernfalls Punkte in ein strukturiertes Array konvertieren - points_structured = np.empty( - largest_cluster.shape[0], dtype=[("x", "f4"), ("y", "f4"), ("z", "f4")] - ) - points_structured["x"] = largest_cluster[:, 0] - points_structured["y"] = largest_cluster[:, 1] - points_structured["z"] = largest_cluster[:, 2] - - return points_structured - - -def combine_clusters(clustered_points): - """ - Kombiniert Cluster-Daten in ein strukturiertes Array. - - :param clustered_points: Dictionary mit Cluster-IDs als Schlüssel und 3D-Punktwolken als Werten. - :return: Kombiniertes strukturiertes NumPy-Array mit Feldern "x", "y", "z", "cluster_id". - """ - - # Vorab die Gesamtanzahl der Punkte berechnen - total_points = sum(points.shape[0] for points in clustered_points.values()) - - # Erstelle ein leeres strukturiertes Array - combined_points = np.empty( - total_points, - dtype=[("x", "f4"), ("y", "f4"), ("z", "f4"), ("cluster_id", "f4")], - ) - - # Fülle das Array direkt - index = 0 - for cluster_id, points in clustered_points.items(): - num_points = points.shape[0] - combined_points["x"][index : index + num_points] = points[:, 0] - combined_points["y"][index : index + num_points] = points[:, 1] - combined_points["z"][index : index + num_points] = points[:, 2] - combined_points["cluster_id"][index : index + num_points] = cluster_id - index += num_points - - return combined_points - - -def fuse_clusters(cluster_data_list): - """ - Fusioniert eine Liste von Cluster-Daten und entfernt redundante Punkte basierend auf (x, y, z). - - :param cluster_data_list: Liste von NumPy-Arrays mit Feldern (x, y, z, intensity) - :return: Fusioniertes NumPy-Array mit einzigartigen Punkten - """ - # Kombiniere alle Arrays in ein großes Array - concatenated_points = np.concatenate(cluster_data_list, axis=0) - - # Erstelle eine flache Ansicht für die Felder (x, y, z) - unique_xyz, indices = np.unique( - concatenated_points[["x", "y", "z"]].view(np.float32).reshape(-1, 3), - axis=0, - return_index=True, - ) - - # Behalte die entsprechenden `intensity`-Werte basierend auf den eindeutigen Indizes - unique_points = concatenated_points[indices] - - return unique_points - - -def cluster_lidar_data_from_pointcloud(coordinates, eps=0.1, min_samples=10): - """ - Cluster LiDAR-Daten effizient mit DBSCAN. - """ - # Erstelle Matrix direkt mit column_stack (schneller als vstack) - xyz = np.column_stack((coordinates["x"], coordinates["y"], coordinates["z"])) - - # DBSCAN-Cluster-Berechnung - clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(xyz) - labels = clustering.labels_ - - # Nur eindeutige Cluster (Rauschen entfernen) - unique_labels = np.unique(labels) - valid_labels = unique_labels[unique_labels != -1] - - # Parallelisiertes Extrahieren der Cluster - clusters = Parallel(n_jobs=-1)( - delayed(lambda l: (l, xyz[labels == l]))(label) for label in valid_labels - ) - clusters = dict(clusters) - - return clusters - - -if __name__ == "__main__": - lidar_distance = LidarDistance() - lidar_distance.listener() From 279944884dd72fe2a21180768d402da77bdce794 Mon Sep 17 00:00:00 2001 From: Ralf Date: Wed, 11 Dec 2024 15:12:52 +0100 Subject: [PATCH 26/55] added code documentation --- code/perception/src/radar_node.py | 315 +++++++++++++++++------------- 1 file changed, 174 insertions(+), 141 deletions(-) diff --git a/code/perception/src/radar_node.py b/code/perception/src/radar_node.py index ddff6511..975af2b1 100755 --- a/code/perception/src/radar_node.py +++ b/code/perception/src/radar_node.py @@ -27,34 +27,29 @@ def callback(self, data): Args: data: Point2Cloud message containing radar data """ - # clustered_points = cluster_radar_data_from_pointcloud(data, 10) - # clustered_points_json = json.dumps(clustered_points) - # self.dist_array_radar_publisher.publish(clustered_points_json) - # output array [x, y, z, distance] dataarray = pointcloud2_to_array(data) - # input array [x, y, z, distance], max_distance, output: filtered data - # dataarray = filter_data(dataarray, 10) + # radar position z=0.7 + dataarray = filter_data(dataarray, min_z=-0.6) - # input array [x, y, z, distance], output: dict clustered clustered_data = cluster_data(dataarray) # transformed_data = transform_data_to_2d(dataarray) - # input array [x, y, z, distance], clustered labels cloud = create_pointcloud2(dataarray, clustered_data.labels_) self.visualization_radar_publisher.publish(cloud) - points_with_labels = np.hstack((dataarray, clustered_data.labels_.reshape(-1, 1))) + points_with_labels = np.hstack((dataarray, + clustered_data.labels_.reshape(-1, 1))) bounding_boxes = generate_bounding_boxes(points_with_labels) marker_array = MarkerArray() - # marker_array = clear_old_markers(marker_array) for label, bbox in bounding_boxes: if label != -1: marker = create_bounding_box_marker(label, bbox) marker_array.markers.append(marker) + # can be used for extra debugging # min_marker, max_marker = create_min_max_markers(label, bbox) # marker_array.markers.append(min_marker) # marker_array.markers.append(max_marker) @@ -63,7 +58,8 @@ def callback(self, data): self.marker_visualization_radar_publisher.publish(marker_array) - cluster_info = generate_cluster_labels_and_colors(clustered_data, dataarray, marker_array, bounding_boxes) + cluster_info = generate_cluster_info(clustered_data, dataarray, marker_array, + bounding_boxes) self.cluster_info_radar_publisher.publish(cluster_info) def listener(self): @@ -117,93 +113,73 @@ def pointcloud2_to_array(pointcloud_msg): Returns: - np.ndarray A 2D array where each row corresponds to a point in the point cloud: - [x, y, z, distance], where "distance" is the distance from the origin. + [x, y, z, Velocity] """ cloud_array = ros_numpy.point_cloud2.pointcloud2_to_array(pointcloud_msg) - # distances = np.sqrt( - # cloud_array["x"] ** 2 + cloud_array["y"] ** 2 + cloud_array["z"] ** 2 - # ) - # return np.column_stack( - # (cloud_array["x"], cloud_array["y"], cloud_array["z"], distances) - # ) return np.column_stack( (cloud_array["x"], cloud_array["y"], cloud_array["z"], cloud_array["Velocity"]) ) -def cluster_radar_data_from_pointcloud( - pointcloud_msg, max_distance, eps=1.0, min_samples=2 -): +def filter_data(data, min_x=-100, max_x=100, min_y=-100, max_y=100, min_z=-1, max_z=100, + max_distance=100): """ - Filters and clusters points from a ROS PointCloud2 message based on DBSCAN - clustering. + Filters radar data based on specified spatial and distance constraints. - Parameters: - - pointcloud_msg: sensor_msgs/PointCloud2 - The ROS PointCloud2 message containing the 3D points. - - max_distance: float - Maximum distance to consider points. Points beyond this distance are - discarded. - - eps: float, optional (default: 1.0) - The maximum distance between two points for them to be considered in - the same cluster. - - min_samples: int, optional (default: 2) - The minimum number of points required to form a cluster. + This function applies multiple filtering criteria to the input radar data. + Points outside these bounds are excluded from the output. + + Args: + data (np.ndarray): A 2D numpy array containing radar data, where each row + represents a data point with the format [x, y, z, distance]. The array + shape is (N, 4), where N is the number of points. + min_x (float, optional): Minimum value for the x-coordinate. Default is -1. + max_x (float, optional): Maximum value for the x-coordinate. Default is 1. + min_y (float, optional): Minimum value for the y-coordinate. Default is 1. + max_y (float, optional): Maximum value for the y-coordinate. Default is 1. + min_z (float, optional): Minimum value for the z-coordinate. Default is -0.7. + max_z (float, optional): Maximum value for the z-coordinate. Default is 1.3. + max_distance (float, optional): Maximum allowable distance of the point from + the sensor. Default is 100. Returns: - - dict - A dictionary where the keys are cluster labels (int) and the values - are the number of points in each cluster. Returns an empty dictionary - if no points are available. + np.ndarray: A numpy array containing only the filtered data points that meet + the specified criteria. """ - data = pointcloud2_to_array(pointcloud_msg) + filtered_data = data[data[:, 3] < max_distance] filtered_data = filtered_data[ - (filtered_data[:, 1] >= -1) - & (filtered_data[:, 1] <= 1) - & (filtered_data[:, 2] <= 1.3) - & (filtered_data[:, 2] >= -0.7) + (filtered_data[:, 0] >= min_x) + & (filtered_data[:, 0] <= max_x) + & (filtered_data[:, 1] >= min_y) + & (filtered_data[:, 1] <= max_y) + & (filtered_data[:, 2] <= max_z) + & (filtered_data[:, 2] >= min_z) ] - if len(filtered_data) == 0: - return {} - coordinates = filtered_data[:, :2] - clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(coordinates) - labels = clustering.labels_ - clustered_points = {label: list(labels).count(label) for label in set(labels)} - clustered_points = {int(label): count for label, count in clustered_points.items()} - return clustered_points - + return filtered_data -# filters radar data in distance, y, z direction -def filter_data(data, max_distance): - # filtered_data = data[data[:, 3] < max_distance] - filtered_data = data - filtered_data = filtered_data[ - # (filtered_data[:, 1] >= -1) - # & (filtered_data[:, 1] <= 1) - # & (filtered_data[:, 2] <= 1.3) - (filtered_data[:, 2] <= 1.3) - & (filtered_data[:, 2] >= -0.6) # -0.7 - ] - return filtered_data +def cluster_data(data, eps=0.8, min_samples=3): + """_summary_ + Args: + data (np.ndarray): data array which should be clustered + eps (float, optional): maximum distance of points. Defaults to 0.8. + min_samples (int, optional): min samples for 1 cluster. Defaults to 3. -# clusters data with DBSCAN -def cluster_data(filtered_data, eps=0.8, min_samples=5): + Returns: + dict: A dictionary where the keys are cluster labels (int) and the values + are the number of points in each cluster. Returns an empty dictionary + if no points are available. + """ - if len(filtered_data) == 0: + if len(data) == 0: return {} - # coordinates = filtered_data[:, :2] - # clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(coordinates) - - # worse than without scaling scaler = StandardScaler() - data_scaled = scaler.fit_transform(filtered_data) - clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(data_scaled) + data_scaled = scaler.fit_transform(data) + clustered_points = DBSCAN(eps=eps, min_samples=min_samples).fit(data_scaled) - # clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(filtered_data) - return clustering + return clustered_points # generates random color for cluster @@ -213,8 +189,16 @@ def generate_color_map(num_clusters): return colors -# creates pointcloud2 for publishing clustered radar data def create_pointcloud2(clustered_points, cluster_labels): + """_summary_ + + Args: + clustered_points (dict): clustered points after dbscan + cluster_labels (_type_): _description_ + + Returns: + PointCloud2: pointcloud which can be published + """ header = Header() header.stamp = rospy.Time.now() header.frame_id = "hero/RADAR" @@ -224,7 +208,6 @@ def create_pointcloud2(clustered_points, cluster_labels): colors = generate_color_map(len(unique_labels)) for i, point in enumerate(clustered_points): - # x, y, z, _ = point x, y, z, v = point label = cluster_labels[i] @@ -247,6 +230,14 @@ def create_pointcloud2(clustered_points, cluster_labels): def transform_data_to_2d(clustered_data): + """_summary_ + + Args: + clustered_data (np.ndarray): clustered 3d data points + + Returns: + _np.ndarray: clustered points, every z value is set to 0 + """ transformed_points = clustered_data transformed_points[:, 0] = clustered_data[:, 0] @@ -258,13 +249,23 @@ def transform_data_to_2d(clustered_data): def calculate_aabb(cluster_points): - """_summary_ + """ + Calculates the axis-aligned bounding box (AABB) for a set of 3D points. + + This function computes the minimum and maximum values along each axis (x, y, z) + for a given set of 3D points, which defines the bounding box that contains + all points in the cluster. Args: - cluster_points (_type_): _description_ + cluster_points (numpy.ndarray): + A 2D array where each row represents a 3D point (x, y, z). + The array should have shape (N, 3) where N is the number of points. Returns: - _type_: _description_ + tuple: A tuple of the form (x_min, x_max, y_min, y_max, z_min, z_max), + which represents the axis-aligned bounding box (AABB) for the given + set of points. The values are the minimum and maximum coordinates + along the x, y, and z axes. """ # for 2d (top-down) boxes @@ -272,7 +273,7 @@ def calculate_aabb(cluster_points): # x_max = np.max(cluster_points[:, 0]) # y_min = np.min(cluster_points[:, 1]) # y_max = np.max(cluster_points[:, 1]) - + # rospy.loginfo(f"Bounding box: X({x_min}, {x_max}), Y({y_min}, {y_max})") # return x_min, x_max, y_min, y_max # for 3d boxes @@ -282,18 +283,27 @@ def calculate_aabb(cluster_points): y_max = np.max(cluster_points[:, 1]) z_min = np.min(cluster_points[:, 2]) z_max = np.max(cluster_points[:, 2]) - rospy.loginfo(f"Bounding box for label: X({x_min}, {x_max}), Y({y_min}, {y_max}), Z({z_min}, {z_max})") return x_min, x_max, y_min, y_max, z_min, z_max def generate_bounding_boxes(points_with_labels): - """_summary_ + """ + Generates bounding boxes for clustered points. + + This function processes a set of points, each associated with a cluster label, + and generates an axis-aligned bounding box (AABB) for each unique cluster label. Args: - points_with_labels (_type_): _description_ + points_with_labels (numpy.ndarray): + A 2D array of shape (N, 4) where each row contains + the coordinates (x, y, z) of a point along with its + corresponding cluster label in the last column. + The array should have the structure [x, y, z, label]. Returns: - _type_: _description_ + list: A list of tuples, where each tuple contains a cluster label and the + corresponding bounding box (bbox). The bbox is represented by a tuple + of the form (x_min, x_max, y_min, y_max, z_min, z_max). """ bounding_boxes = [] unique_labels = np.unique(points_with_labels[:, -1]) @@ -307,10 +317,23 @@ def generate_bounding_boxes(points_with_labels): def create_bounding_box_marker(label, bbox): - """_summary_ + """ + Creates an RViz Marker for visualizing a 3D bounding box. + + This function generates a Marker object for RViz to visualize a 3D bounding box + based on the provided label and bounding box dimensions. The marker is + represented as a series of lines connecting the corners of the box. + + Args: + label (int): The unique identifier for the cluster or object to which the + bounding box belongs. This label is used as the Marker ID. + bbox (tuple): A tuple containing the min and max coordinates of the bounding box + in the format (x_min, x_max, y_min, y_max, z_min, z_max). Returns: - _type_: _description_ + Marker: A Marker object that can be published to RViz to display the + 3D bounding box. The marker is of type LINE_LIST, + representing the edges of the bounding box. """ # for 2d (top-down) boxes # x_min, x_max, y_min, y_max = bbox @@ -321,8 +344,8 @@ def create_bounding_box_marker(label, bbox): marker = Marker() marker.header.frame_id = "hero/RADAR" marker.id = int(label) - # marker.type = Marker.LINE_STRIP - marker.type = Marker.LINE_LIST + # marker.type = Marker.LINE_STRIP # 2d boxes + marker.type = Marker.LINE_LIST # 3d boxes marker.action = Marker.ADD marker.scale.x = 0.1 marker.color.r = 1.0 @@ -342,32 +365,20 @@ def create_bounding_box_marker(label, bbox): # for 3d boxes points = [ - Point(x_min, y_min, z_min), # Ecke 0 - Point(x_max, y_min, z_min), # Ecke 1 - Point(x_max, y_max, z_min), # Ecke 2 - Point(x_min, y_max, z_min), # Ecke 3 - Point(x_min, y_min, z_max), # Ecke 4 - Point(x_max, y_min, z_max), # Ecke 5 - Point(x_max, y_max, z_max), # Ecke 6 - Point(x_min, y_max, z_max), # Ecke 7 - - # Point(x_min, y_min, z_min), # Verbinde z_min zu z_max - # Point(x_min, y_min, z_max), - - # Point(x_max, y_min, z_min), - # Point(x_max, y_min, z_max), - - # Point(x_max, y_max, z_min), - # Point(x_max, y_max, z_max), - - # Point(x_min, y_max, z_min), - # Point(x_min, y_max, z_max), + Point(x_min, y_min, z_min), + Point(x_max, y_min, z_min), + Point(x_max, y_max, z_min), + Point(x_min, y_max, z_min), + Point(x_min, y_min, z_max), + Point(x_max, y_min, z_max), + Point(x_max, y_max, z_max), + Point(x_min, y_max, z_max), ] - # marker.points = points + lines = [ - (0, 1), (1, 2), (2, 3), (3, 0), # Boden - (4, 5), (5, 6), (6, 7), (7, 4), # Deckel - (0, 4), (1, 5), (2, 6), (3, 7), # Vertikale Kanten + (0, 1), (1, 2), (2, 3), (3, 0), # Bottom + (4, 5), (5, 6), (6, 7), (7, 4), # Top + (0, 4), (1, 5), (2, 6), (3, 7), # Vertical Edges ] for start, end in lines: marker.points.append(points[start]) @@ -376,41 +387,33 @@ def create_bounding_box_marker(label, bbox): return marker -def create_min_max_markers(label, bbox, frame_id="hero/RADAR", min_color=(0.0, 1.0, 0.0, 1.0), max_color=(1.0, 0.0, 0.0, 1.0)): +# can be used for extra debugging +def create_min_max_markers(label, bbox, frame_id="hero/RADAR", + min_color=(0.0, 1.0, 0.0, 1.0), + max_color=(1.0, 0.0, 0.0, 1.0)): """ - Erstellt RViz-Marker für die Min- und Max-Punkte einer Bounding Box. + creates RViz-Markers for min- and max-points of a bounding box. Args: - label (int): Die ID des Clusters (wird als Marker-ID genutzt). - bbox (tuple): Min- und Max-Werte der Bounding Box (x_min, x_max, y_min, y_max, z_min, z_max). - frame_id (str): Frame ID, in dem die Marker gezeichnet werden. - min_color (tuple): RGBA-Farbwerte für den Min-Punkt-Marker. - max_color (tuple): RGBA-Farbwerte für den Max-Punkt-Marker. + label (int): cluster-id (used as marker-ID in rviz). + bbox (tuple): min- and max-values of bounding box + (x_min, x_max, y_min, y_max, z_min, z_max). + frame_id (str): frame ID for markers + min_color (tuple): RGBA-value for min-point-marker + max_color (tuple): RGBA-value for max-point-marker Returns: - tuple: Ein Paar von Markern (min_marker, max_marker). + tuple: pair of markers (min_marker, max_marker). """ x_min, x_max, y_min, y_max, z_min, z_max = bbox - # marker = Marker() - # marker.header.frame_id = "hero/RADAR" - # marker.id = int(label) - # # marker.type = Marker.LINE_STRIP - # marker.type = Marker.LINE_LIST - # marker.action = Marker.ADD - # marker.scale.x = 0.1 - # marker.color.r = 1.0 - # marker.color.g = 1.0 - # marker.color.b = 0.0 - # marker.color.a = 1.0 - - # Min-Punkt-Marker + # min-point-marker min_marker = Marker() min_marker.header.frame_id = frame_id - min_marker.id = int(label * 10) # ID für Min-Punkt + min_marker.id = int(label * 10) min_marker.type = Marker.SPHERE min_marker.action = Marker.ADD - min_marker.scale.x = 0.2 # Größe des Punktes + min_marker.scale.x = 0.2 min_marker.scale.y = 0.2 min_marker.scale.z = 0.2 min_marker.color.r = min_color[0] @@ -421,10 +424,10 @@ def create_min_max_markers(label, bbox, frame_id="hero/RADAR", min_color=(0.0, 1 min_marker.pose.position.y = y_min min_marker.pose.position.z = z_min - # Max-Punkt-Marker + # max-point-marker max_marker = Marker() max_marker.header.frame_id = frame_id - max_marker.id = int(label * 10 + 1) # ID für Max-Punkt + max_marker.id = int(label * 10 + 1) max_marker.type = Marker.SPHERE max_marker.action = Marker.ADD max_marker.scale.x = 0.2 @@ -442,15 +445,45 @@ def create_min_max_markers(label, bbox, frame_id="hero/RADAR", min_color=(0.0, 1 def clear_old_markers(marker_array, max_id): - """Löscht alte Marker aus dem MarkerArray.""" + """ + Removes old markers from the given MarkerArray by setting the action + to DELETE for markers with an ID greater than or equal to max_id. + + Args: + marker_array (MarkerArray): The current MarkerArray containing all markers. + max_id (int): The highest ID of the new markers. Markers with an ID + greater than or equal to this value will be marked for deletion. + + Returns: + MarkerArray: The updated MarkerArray with old markers removed. + """ for marker in marker_array.markers: if marker.id >= max_id: marker.action = Marker.DELETE return marker_array -# generates string with label-id and cluster size -def generate_cluster_labels_and_colors(clusters, data, marker_array, bounding_boxes): +# generates string with label-id and cluster size, can be used for extra debugging +def generate_cluster_info(clusters, data, marker_array, bounding_boxes): + """ + Generates information about clusters, including the label, number of points, + markers, and bounding boxes. + + Args: + clusters (DBSCAN): The clustered data, containing the labels for each point. + data (numpy.ndarray): + The point cloud data, typically with columns [x, y, z, distance]. + marker_array (MarkerArray): + The array of RViz markers associated with the clusters. + bounding_boxes (list): The list of bounding boxes for each detected object. + + Returns: + str: A JSON string containing the information about each cluster, including: + - "label": The cluster label. + - "points_count": The number of points in the cluster. + - "Anzahl marker": The number of markers in the MarkerArray. + - "Anzahl Boundingboxen": The number of bounding boxes. + """ cluster_info = [] for label in set(clusters.labels_): From 57e720d6119685ac63d3e276dcbb11c9c4a59aaa Mon Sep 17 00:00:00 2001 From: Ralf Date: Wed, 11 Dec 2024 15:23:41 +0100 Subject: [PATCH 27/55] removed trailing white spaces --- code/perception/src/radar_node.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/code/perception/src/radar_node.py b/code/perception/src/radar_node.py index 975af2b1..aecbeca2 100755 --- a/code/perception/src/radar_node.py +++ b/code/perception/src/radar_node.py @@ -130,8 +130,8 @@ def filter_data(data, min_x=-100, max_x=100, min_y=-100, max_y=100, min_z=-1, ma Points outside these bounds are excluded from the output. Args: - data (np.ndarray): A 2D numpy array containing radar data, where each row - represents a data point with the format [x, y, z, distance]. The array + data (np.ndarray): A 2D numpy array containing radar data, where each row + represents a data point with the format [x, y, z, distance]. The array shape is (N, 4), where N is the number of points. min_x (float, optional): Minimum value for the x-coordinate. Default is -1. max_x (float, optional): Maximum value for the x-coordinate. Default is 1. @@ -143,7 +143,7 @@ def filter_data(data, min_x=-100, max_x=100, min_y=-100, max_y=100, min_z=-1, ma the sensor. Default is 100. Returns: - np.ndarray: A numpy array containing only the filtered data points that meet + np.ndarray: A numpy array containing only the filtered data points that meet the specified criteria. """ @@ -257,7 +257,7 @@ def calculate_aabb(cluster_points): all points in the cluster. Args: - cluster_points (numpy.ndarray): + cluster_points (numpy.ndarray): A 2D array where each row represents a 3D point (x, y, z). The array should have shape (N, 3) where N is the number of points. @@ -396,7 +396,7 @@ def create_min_max_markers(label, bbox, frame_id="hero/RADAR", Args: label (int): cluster-id (used as marker-ID in rviz). - bbox (tuple): min- and max-values of bounding box + bbox (tuple): min- and max-values of bounding box (x_min, x_max, y_min, y_max, z_min, z_max). frame_id (str): frame ID for markers min_color (tuple): RGBA-value for min-point-marker @@ -410,10 +410,10 @@ def create_min_max_markers(label, bbox, frame_id="hero/RADAR", # min-point-marker min_marker = Marker() min_marker.header.frame_id = frame_id - min_marker.id = int(label * 10) + min_marker.id = int(label * 10) min_marker.type = Marker.SPHERE min_marker.action = Marker.ADD - min_marker.scale.x = 0.2 + min_marker.scale.x = 0.2 min_marker.scale.y = 0.2 min_marker.scale.z = 0.2 min_marker.color.r = min_color[0] @@ -446,12 +446,12 @@ def create_min_max_markers(label, bbox, frame_id="hero/RADAR", def clear_old_markers(marker_array, max_id): """ - Removes old markers from the given MarkerArray by setting the action + Removes old markers from the given MarkerArray by setting the action to DELETE for markers with an ID greater than or equal to max_id. Args: marker_array (MarkerArray): The current MarkerArray containing all markers. - max_id (int): The highest ID of the new markers. Markers with an ID + max_id (int): The highest ID of the new markers. Markers with an ID greater than or equal to this value will be marked for deletion. Returns: From 78afbb6b82b7298674f15ab50584d6951c679f17 Mon Sep 17 00:00:00 2001 From: michalal7 Date: Wed, 11 Dec 2024 15:34:04 +0100 Subject: [PATCH 28/55] Applied some suggestions by CodeRabbit. Deleted function get_largest_cluster which was not used anywhere. --- code/perception/src/lidar_distance.py | 327 ++++++++++---------------- 1 file changed, 122 insertions(+), 205 deletions(-) diff --git a/code/perception/src/lidar_distance.py b/code/perception/src/lidar_distance.py index 260cbea8..fa8ce676 100644 --- a/code/perception/src/lidar_distance.py +++ b/code/perception/src/lidar_distance.py @@ -18,28 +18,28 @@ class LidarDistance: how to configute this node """ - cluster_buffer = [] + def __init__(self): + self.cluster_buffer = [] def callback(self, data): """ - Callback-Funktion, die LiDAR-Punktwolkendaten verarbeitet. + Callback function that processes LiDAR point cloud data. - Führt Clustering und Bildberechnungen für die Punktwolken aus. + Executes clustering and image calculations for the provided point cloud. - :param data: LiDAR-Punktwolken als ROS PointCloud2-Nachricht. + :param data: LiDAR point cloud as a ROS PointCloud2 message. """ - self.start_clustering(data) self.start_image_calculation(data) def listener(self): """ - Initialisiert die ROS-Node, erstellt Publisher/Subscriber und hält sie aktiv. + Initializes the ROS node, creates publishers/subscribers, and keeps it active. """ rospy.init_node("lidar_distance") - self.bridge = CvBridge() # OpenCV-Bridge für Bildkonvertierungen + self.bridge = CvBridge() # OpenCV bridge for image conversions - # Publisher für gefilterte Punktwolken + # Publisher for filtered point clouds self.pub_pointcloud = rospy.Publisher( rospy.get_param( "~point_cloud_topic", @@ -49,7 +49,7 @@ def listener(self): queue_size=1, ) - # Publisher für Distanzbilder in verschiedene Richtungen + # Publishers for distance images in various directions self.dist_array_center_publisher = rospy.Publisher( rospy.get_param("~image_distance_topic", "/paf/hero/Center/dist_array"), ImageMsg, @@ -67,7 +67,7 @@ def listener(self): PointCloud2, queue_size=10, ) - rospy.loginfo("dist_array_lidar_publisher erfolgreich erstellt.") + rospy.loginfo("dist_array_lidar_publisher successfully created.") self.dist_array_left_publisher = rospy.Publisher( rospy.get_param("~image_distance_topic", "/paf/hero/Left/dist_array"), ImageMsg, @@ -79,168 +79,125 @@ def listener(self): queue_size=10, ) - # Subscriber für LiDAR-Daten (Punktwolken) + # Subscriber for LiDAR data (point clouds) rospy.Subscriber( rospy.get_param("~source_topic", "/carla/hero/LIDAR"), PointCloud2, self.callback, ) - rospy.loginfo("Lidar Processor Node gestartet.") + rospy.loginfo("Lidar Processor Node started.") rospy.spin() def start_clustering(self, data): """ - Filtert LiDAR-Punktwolken, führt Clustering durch und veröffentlicht die kombinierten Cluster. + Filters LiDAR point clouds, performs clustering, and publishes the combined clusters. - :param data: LiDAR-Punktwolken im ROS PointCloud2-Format. + :param data: LiDAR point clouds in ROS PointCloud2 format. """ - # Punktwolken filtern, um irrelevante Daten zu entfernen + # Filter point clouds to remove irrelevant data coordinates = ros_numpy.point_cloud2.pointcloud2_to_array(data) filtered_coordinates = coordinates[ ~( - (-2 <= coordinates["x"]) + (coordinates["x"] >= -2) & (coordinates["x"] <= 2) - & (-1 <= coordinates["y"]) + & (coordinates["y"] >= -1) & (coordinates["y"] <= 1) - ) # Ausschluss von Punkten die das eigene Auto betreffen + ) # Exclude points related to the ego vehicle & ( coordinates["z"] > -1.7 + 0.05 - ) # Mindesthöhe in z, um die Straße nicht zu clustern + ) # Minimum height in z to exclude the road ] - # Cluster-Daten aus den gefilterten Koordinaten berechnen + # Compute cluster data from the filtered coordinates clustered_points = cluster_lidar_data_from_pointcloud( coordinates=filtered_coordinates ) - # Nur gültige Cluster-Daten speichern + # Only store valid cluster data if clustered_points: - LidarDistance.cluster_buffer.append(clustered_points) + self.cluster_buffer.append(clustered_points) else: - rospy.logwarn("Keine Cluster-Daten erzeugt.") + rospy.logwarn("No cluster data generated.") - # Cluster kombinieren - combined_clusters = combine_clusters(LidarDistance.cluster_buffer) + # Combine clusters + combined_clusters = combine_clusters(self.cluster_buffer) - LidarDistance.cluster_buffer = [] + self.cluster_buffer = [] - # Veröffentliche die kombinierten Cluster + # Publish the combined clusters self.publish_clusters(combined_clusters, data.header) def publish_clusters(self, combined_clusters, data_header): """ - Veröffentlicht kombinierte Cluster als ROS PointCloud2-Nachricht. + Publishes combined clusters as a ROS PointCloud2 message. - :param combined_clusters: Kombinierte Punktwolken der Cluster als strukturiertes NumPy-Array. - :param data_header: Header-Informationen der ROS-Nachricht. + :param combined_clusters: Combined point clouds of the clusters as a structured NumPy array. + :param data_header: Header information for the ROS message. """ - # Konvertiere zu PointCloud2-Nachricht + # Convert to a PointCloud2 message pointcloud_msg = ros_numpy.point_cloud2.array_to_pointcloud2(combined_clusters) pointcloud_msg.header = data_header pointcloud_msg.header.stamp = rospy.Time.now() - # Cluster veröffentlichen + # Publish the clusters self.dist_array_lidar_publisher.publish(pointcloud_msg) def start_image_calculation(self, data): """ - Berechnet Distanzbilder basierend auf LiDAR-Daten und veröffentlicht sie. + Computes distance images based on LiDAR data and publishes them. - :param data: LiDAR-Punktwolken im ROS PointCloud2-Format. + :param data: LiDAR point cloud in ROS PointCloud2 format. """ coordinates = ros_numpy.point_cloud2.pointcloud2_to_array(data) - # Bildverarbeitung auf den Koordinaten durchführen + + # Directions to process + directions = ["Center", "Back", "Left", "Right"] + + # Process images for all directions processed_images = { - "Center": None, - "Back": None, - "Left": None, - "Right": None, + direction: self.calculate_image(coordinates, focus=direction) + for direction in directions } - processed_images["Center"] = self.calculate_image_center(coordinates) - processed_images["Back"] = self.calculate_image_back(coordinates) - processed_images["Left"] = self.calculate_image_left(coordinates) - processed_images["Right"] = self.calculate_image_right(coordinates) + # Publish the processed images self.publish_images(processed_images, data.header) - def calculate_image_center(self, coordinates): + def calculate_image(self, coordinates, focus): """ - Berechnet ein Distanzbild für die zentrale Ansicht aus LiDAR-Koordinaten. + Calculates a distance image for a specific focus (view direction) from LiDAR coordinates. - :param coordinates: Gefilterte LiDAR-Koordinaten als NumPy-Array. - :return: Distanzbild als 2D-Array. + :param coordinates: Filtered LiDAR coordinates as a NumPy array. + :param focus: The focus direction ("Center", "Back", "Left", "Right"). + :return: Distance image as a 2D array. """ - reconstruct_bit_mask_center = lidar_filter_utility.bounding_box( - coordinates, - max_x=np.inf, - min_x=0.0, - min_z=-1.6, - ) - reconstruct_coordinates_center = coordinates[reconstruct_bit_mask_center] - reconstruct_coordinates_xyz_center = np.array( - lidar_filter_utility.remove_field_name( - reconstruct_coordinates_center, "intensity" - ).tolist() - ) - dist_array_center = self.reconstruct_img_from_lidar( - reconstruct_coordinates_xyz_center, focus="Center" - ) - return dist_array_center - - def calculate_image_back(self, coordinates): - # Back - reconstruct_bit_mask_back = lidar_filter_utility.bounding_box( - coordinates, - max_x=0.0, - min_x=-np.inf, - min_z=-1.6, - ) - reconstruct_coordinates_back = coordinates[reconstruct_bit_mask_back] - reconstruct_coordinates_xyz_back = np.array( - lidar_filter_utility.remove_field_name( - reconstruct_coordinates_back, "intensity" - ).tolist() - ) - dist_array_back = self.reconstruct_img_from_lidar( - reconstruct_coordinates_xyz_back, focus="Back" - ) - return dist_array_back - - def calculate_image_left(self, coordinates): - # Left - reconstruct_bit_mask_left = lidar_filter_utility.bounding_box( - coordinates, - max_y=np.inf, - min_y=0.0, - min_z=-1.6, - ) - reconstruct_coordinates_left = coordinates[reconstruct_bit_mask_left] - reconstruct_coordinates_xyz_left = np.array( - lidar_filter_utility.remove_field_name( - reconstruct_coordinates_left, "intensity" - ).tolist() - ) - dist_array_left = self.reconstruct_img_from_lidar( - reconstruct_coordinates_xyz_left, focus="Left" - ) - return dist_array_left + # Define bounding box parameters based on focus direction + bounding_box_params = { + "Center": {"max_x": np.inf, "min_x": 0.0, "min_z": -1.6}, + "Back": {"max_x": 0.0, "min_x": -np.inf, "min_z": -1.6}, + "Left": {"max_y": np.inf, "min_y": 0.0, "min_z": -1.6}, + "Right": {"max_y": -0.0, "min_y": -np.inf, "min_z": -1.6}, + } - def calculate_image_right(self, coordinates): - # Right - reconstruct_bit_mask_right = lidar_filter_utility.bounding_box( - coordinates, max_y=-0.0, min_y=-np.inf, min_z=-1.6 - ) - reconstruct_coordinates_right = coordinates[reconstruct_bit_mask_right] - reconstruct_coordinates_xyz_right = np.array( + # Select parameters for the given focus + params = bounding_box_params.get(focus) + if not params: + rospy.logwarn(f"Unknown focus: {focus}. Skipping image calculation.") + return None + + # Apply bounding box filter + reconstruct_bit_mask = lidar_filter_utility.bounding_box(coordinates, **params) + reconstruct_coordinates = coordinates[reconstruct_bit_mask] + + # Remove the "intensity" field and convert to a NumPy array + reconstruct_coordinates_xyz = np.array( lidar_filter_utility.remove_field_name( - reconstruct_coordinates_right, "intensity" + reconstruct_coordinates, "intensity" ).tolist() ) - dist_array_right = self.reconstruct_img_from_lidar( - reconstruct_coordinates_xyz_right, focus="Right" - ) - return dist_array_right + + # Reconstruct the image based on the focus + return self.reconstruct_img_from_lidar(reconstruct_coordinates_xyz, focus=focus) def publish_images(self, processed_images, data_header): """ @@ -271,71 +228,71 @@ def publish_images(self, processed_images, data_header): def reconstruct_img_from_lidar(self, coordinates_xyz, focus): """ - Rekonstruiert ein 2D-Bild aus 3D-LiDAR-Daten für eine gegebene Kameraansicht. + Reconstructs a 2D image from 3D LiDAR data for a given camera perspective. - :param coordinates_xyz: 3D-Koordinaten der gefilterten LiDAR-Punkte. - :param focus: Kameraansicht (z. B. "Center", "Back"). - :return: Rekonstruiertes Bild als 2D-Array. + :param coordinates_xyz: 3D coordinates of the filtered LiDAR points. + :param focus: Camera view (e.g., "Center", "Back"). + :return: Reconstructed image as a 2D array. """ - # Erstelle die intrinsische Kamera-Matrix basierend auf den Bildparametern (FOV, Auflösung) + # Create the intrinsic camera matrix based on image parameters (FOV, resolution) im = np.identity(3) - im[0, 2] = 1280 / 2.0 # x-Verschiebung (Bildmitte) - im[1, 2] = 720 / 2.0 # y-Verschiebung (Bildmitte) + im[0, 2] = 1280 / 2.0 # x-offset (image center) + im[1, 2] = 720 / 2.0 # y-offset (image center) im[0, 0] = im[1, 1] = 1280 / ( 2.0 * np.tan(100 * np.pi / 360.0) - ) # Skalierungsfaktor basierend auf FOV + ) # Scale factor based on FOV - # Erstelle die extrinsische Kamera-Matrix (Identität für keine Transformation) + # Create the extrinsic camera matrix (identity matrix for no transformation) ex = np.zeros(shape=(3, 4)) ex[0][0] = ex[1][1] = ex[2][2] = 1 - m = np.matmul(im, ex) # Kombiniere intrinsische und extrinsische Matrix + m = np.matmul(im, ex) # Combine intrinsic and extrinsic matrices - # Initialisiere leere Bilder für die Rekonstruktion + # Initialize empty images for reconstruction img = np.zeros(shape=(720, 1280), dtype=np.float32) dist_array = np.zeros(shape=(720, 1280, 3), dtype=np.float32) - # Verarbeite jeden Punkt in der Punktwolke + # Process each point in the point cloud for c in coordinates_xyz: - if focus == "Center": + if focus == "Center": # Compute image for the center view point = np.array([c[1], c[2], c[0], 1]) - pixel = np.matmul( - m, point - ) # Projiziere 3D-Punkt auf 2D-Bildkoordinaten + pixel = np.matmul(m, point) # Project 3D point to 2D image coordinates x, y = int(pixel[0] / pixel[2]), int( pixel[1] / pixel[2] - ) # Normalisiere Koordinaten - if x >= 0 and x <= 1280 and y >= 0 and y <= 720: # Prüfe Bildgrenzen - img[719 - y][1279 - x] = c[0] # Setze Tiefenwert + ) # Normalize coordinates + if ( + 0 <= x <= 1280 and 0 <= y <= 720 + ): # Check if coordinates are within image bounds + img[719 - y][1279 - x] = c[0] # Set depth value dist_array[719 - y][1279 - x] = np.array( [c[0], c[1], c[2]], dtype=np.float32 ) - if focus == "Back": # Berechne Bild für die Rückansicht + if focus == "Back": # Compute image for the rear view point = np.array([c[1], c[2], c[0], 1]) pixel = np.matmul(m, point) x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) - if x >= 0 and x <= 1280 and y >= 0 and y < 720: + if 0 <= x <= 1280 and 0 <= y < 720: img[y][1279 - x] = -c[0] dist_array[y][1279 - x] = np.array( [-c[0], c[1], c[2]], dtype=np.float32 ) - if focus == "Left": # Berechne Bild für die linke Ansicht + if focus == "Left": # Compute image for the left view point = np.array([c[0], c[2], c[1], 1]) pixel = np.matmul(m, point) x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) - if x >= 0 and x <= 1280 and y >= 0 and y <= 720: + if 0 <= x <= 1280 and 0 <= y <= 720: img[719 - y][1279 - x] = c[1] dist_array[y][1279 - x] = np.array( [c[0], c[1], c[2]], dtype=np.float32 ) - if focus == "Right": # Berechne Bild für die rechte Ansicht + if focus == "Right": # Compute image for the right view point = np.array([c[0], c[2], c[1], 1]) pixel = np.matmul(m, point) x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) - if x >= 0 and x < 1280 and y >= 0 and y < 720: + if 0 <= x < 1280 and 0 <= y < 720: img[y][1279 - x] = -c[1] dist_array[y][1279 - x] = np.array( [c[0], c[1], c[2]], dtype=np.float32 @@ -346,100 +303,60 @@ def reconstruct_img_from_lidar(self, coordinates_xyz, focus): def array_to_pointcloud2(points, header="hero/Lidar"): """ - Konvertiert ein Array von Punkten in eine ROS PointCloud2-Nachricht. + Converts an array of points into a ROS PointCloud2 message. - :param points: Array von Punkten mit [x, y, z]-Koordinaten. - :param header: Header-Informationen der ROS PointCloud2-Nachricht. - :return: ROS PointCloud2-Nachricht. + :param points: Array of points with [x, y, z] coordinates. + :param header: Header information for the ROS PointCloud2 message. + :return: ROS PointCloud2 message. """ - # Sicherstellen, dass die Eingabe ein NumPy-Array ist + # Ensure the input is a NumPy array points_array = np.array(points) - # Konvertiere die Punkte in ein strukturiertes Array mit Feldern "x", "y", "z" + # Convert the points into a structured array with fields "x", "y", "z" points_structured = np.array( [(p[0], p[1], p[2]) for p in points_array], dtype=[("x", "f4"), ("y", "f4"), ("z", "f4")], ) - # Erstelle eine PointCloud2-Nachricht aus dem strukturierten Array + # Create a PointCloud2 message from the structured array pointcloud_msg = ros_numpy.point_cloud2.array_to_pointcloud2(points_structured) - # Setze den Zeitstempel und den Header der Nachricht + # Set the timestamp and header for the message pointcloud_msg.header.stamp = rospy.Time.now() pointcloud_msg.header = header return pointcloud_msg -def get_largest_cluster(clustered_points, return_structured=True): - """ - Ermittelt das größte Cluster aus gegebenen Punktwolken-Clustern. - - :param clustered_points: Dictionary mit Cluster-IDs und zugehörigen Punktwolken. - :param return_structured: Gibt ein strukturiertes NumPy-Array zurück, falls True. - :return: Größtes Cluster als NumPy-Array (roh oder strukturiert). - """ - # Prüfen, ob es Cluster gibt - if not clustered_points: - return np.array([]) - - # Identifiziere das größte Cluster basierend auf der Anzahl der Punkte - largest_cluster_id, largest_cluster = max( - clustered_points.items(), key=lambda item: len(item[1]) - ) - - # Sicherstellen, dass das größte Cluster nicht leer ist - if largest_cluster.size == 0: - return np.array([]) - - rospy.loginfo( - f"Largest cluster: {largest_cluster_id} with {largest_cluster.shape[0]} points" - ) - - # Rohdaten zurückgeben, wenn kein strukturiertes Array benötigt wird - if not return_structured: - return largest_cluster - - # Konvertiere das größte Cluster in ein strukturiertes Array - points_structured = np.empty( - largest_cluster.shape[0], dtype=[("x", "f4"), ("y", "f4"), ("z", "f4")] - ) - points_structured["x"] = largest_cluster[:, 0] - points_structured["y"] = largest_cluster[:, 1] - points_structured["z"] = largest_cluster[:, 2] - - return points_structured - - def combine_clusters(cluster_buffer): """ - Kombiniert Cluster aus mehreren Punktwolken zu einem strukturierten NumPy-Array. + Combines clusters from multiple point clouds into a structured NumPy array. - :param cluster_buffer: Liste von Dictionaries mit Cluster-IDs und Punktwolken. - :return: Kombiniertes strukturiertes NumPy-Array mit Feldern "x", "y", "z", "cluster_id". + :param cluster_buffer: List of dictionaries containing cluster IDs and point clouds. + :return: Combined structured NumPy array with fields "x", "y", "z", "cluster_id". """ points_list = [] cluster_ids_list = [] for clustered_points in cluster_buffer: for cluster_id, points in clustered_points.items(): - if points.size > 0: # Ignoriere leere Cluster + if points.size > 0: # Ignore empty clusters points_list.append(points) - # Erstelle ein Array mit der Cluster-ID für alle Punkte des Clusters + # Create an array with the cluster ID for all points in the cluster cluster_ids_list.append( np.full(points.shape[0], cluster_id, dtype=np.float32) ) - if not points_list: # Falls keine Punkte vorhanden sind + if not points_list: # If no points are present return np.array( [], dtype=[("x", "f4"), ("y", "f4"), ("z", "f4"), ("cluster_id", "f4")] ) - # Kombiniere alle Punkte und Cluster-IDs in zwei separate Arrays + # Combine all points and cluster IDs into two separate arrays all_points = np.vstack(points_list) all_cluster_ids = np.concatenate(cluster_ids_list) - # Erstelle ein strukturiertes Array für die kombinierten Daten + # Create a structured array for the combined data combined_points = np.zeros( all_points.shape[0], dtype=[("x", "f4"), ("y", "f4"), ("z", "f4"), ("cluster_id", "f4")], @@ -454,33 +371,33 @@ def combine_clusters(cluster_buffer): def cluster_lidar_data_from_pointcloud(coordinates, eps=0.3, min_samples=10): """ - Führt Clustering auf LiDAR-Daten mit DBSCAN durch und gibt Cluster zurück. + Performs clustering on LiDAR data using DBSCAN and returns the clusters. - :param coordinates: LiDAR-Punktwolken als NumPy-Array mit "x", "y", "z". - :param eps: Maximaler Abstand zwischen Punkten, um sie zu einem Cluster zuzuordnen. - :param min_samples: Minimale Anzahl von Punkten, um ein Cluster zu bilden. - :return: Dictionary mit Cluster-IDs und zugehörigen Punktwolken. + :param coordinates: LiDAR point cloud as a NumPy array with "x", "y", "z". + :param eps: Maximum distance between points to group them into a cluster. + :param min_samples: Minimum number of points required to form a cluster. + :return: Dictionary with cluster IDs and their corresponding point clouds. """ if coordinates.shape[0] == 0: - rospy.logerr("Das Eingabe-Array 'coordinates' ist leer.") + rospy.logerr("The input array 'coordinates' is empty.") return {} - # Extrahiere x, y und z aus den Koordinaten für die DBSCAN-Berechnung + # Extract x, y, and z from the coordinates for DBSCAN clustering xyz = np.column_stack((coordinates["x"], coordinates["y"], coordinates["z"])) if xyz.shape[0] == 0: - rospy.logwarn("Keine Datenpunkte für DBSCAN verfügbar. Überspringe Clustering.") + rospy.logwarn("No data points available for DBSCAN. Skipping clustering.") return {} - # Wende DBSCAN an, um Cluster-Labels für die Punktwolke zu berechnen + # Apply DBSCAN to compute cluster labels for the point cloud clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(xyz) labels = clustering.labels_ - # Entferne Rauschen (Cluster-ID: -1) und bestimme gültige Cluster-IDs + # Remove noise (cluster ID: -1) and identify valid cluster IDs unique_labels = np.unique(labels) valid_labels = unique_labels[unique_labels != -1] - # Erstelle ein Dictionary mit Cluster-IDs und den zugehörigen Punkten + # Create a dictionary with cluster IDs and their corresponding points clusters = Parallel(n_jobs=-1)( delayed(lambda l: (l, xyz[labels == l]))(label) for label in valid_labels ) From ca171467c4eca354a36115257f89e7baa40c7557 Mon Sep 17 00:00:00 2001 From: michalal7 Date: Wed, 11 Dec 2024 15:38:28 +0100 Subject: [PATCH 29/55] Updated version to satisfy linter. --- code/perception/src/lidar_distance.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/code/perception/src/lidar_distance.py b/code/perception/src/lidar_distance.py index fa8ce676..939224c5 100644 --- a/code/perception/src/lidar_distance.py +++ b/code/perception/src/lidar_distance.py @@ -91,7 +91,8 @@ def listener(self): def start_clustering(self, data): """ - Filters LiDAR point clouds, performs clustering, and publishes the combined clusters. + Filters LiDAR point clouds, performs clustering, + and publishes the combined clusters. :param data: LiDAR point clouds in ROS PointCloud2 format. """ @@ -133,7 +134,8 @@ def publish_clusters(self, combined_clusters, data_header): """ Publishes combined clusters as a ROS PointCloud2 message. - :param combined_clusters: Combined point clouds of the clusters as a structured NumPy array. + :param combined_clusters: Combined point clouds of the clusters as a structured + NumPy array. :param data_header: Header information for the ROS message. """ # Convert to a PointCloud2 message @@ -165,7 +167,8 @@ def start_image_calculation(self, data): def calculate_image(self, coordinates, focus): """ - Calculates a distance image for a specific focus (view direction) from LiDAR coordinates. + Calculates a distance image for a specific focus (view direction) from + LiDAR coordinates. :param coordinates: Filtered LiDAR coordinates as a NumPy array. :param focus: The focus direction ("Center", "Back", "Left", "Right"). @@ -201,17 +204,18 @@ def calculate_image(self, coordinates, focus): def publish_images(self, processed_images, data_header): """ - Veröffentlicht Distanzbilder für verschiedene Richtungen als ROS-Bildnachrichten. + Publishes distance images for various directions as ROS image messages. - :param processed_images: Dictionary mit Richtungen ("Center", "Back", etc.) als Schlüssel und Bildarrays als Werte. - :param data_header: Header der ROS-Bildnachrichten. + :param processed_images: Dictionary with directions ("Center", "Back", etc.) + as keys and image arrays as values. + :param data_header: Header for the ROS image messages. """ - # Nur gültige NumPy-Arrays weiterverarbeiten + # Process only valid NumPy arrays for direction, image_array in processed_images.items(): if not isinstance(image_array, np.ndarray): continue - # Konvertiere das Bild in eine ROS-Image-Nachricht + # Convert the image into a ROS image message dist_array_msg = self.bridge.cv2_to_imgmsg( image_array, encoding="passthrough" ) @@ -399,8 +403,12 @@ def cluster_lidar_data_from_pointcloud(coordinates, eps=0.3, min_samples=10): # Create a dictionary with cluster IDs and their corresponding points clusters = Parallel(n_jobs=-1)( - delayed(lambda l: (l, xyz[labels == l]))(label) for label in valid_labels + delayed(lambda cluster_label: (cluster_label, xyz[labels == cluster_label]))( + label + ) + for label in valid_labels ) + clusters = dict(clusters) return clusters From 0f1a9f93b785d26cfa6945c2c7be8fb00a2b8358 Mon Sep 17 00:00:00 2001 From: Ralf Date: Wed, 11 Dec 2024 15:47:18 +0100 Subject: [PATCH 30/55] bug fixes --- code/perception/src/radar_node.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/code/perception/src/radar_node.py b/code/perception/src/radar_node.py index aecbeca2..246e86cf 100755 --- a/code/perception/src/radar_node.py +++ b/code/perception/src/radar_node.py @@ -74,21 +74,21 @@ def listener(self): ) self.visualization_radar_publisher = rospy.Publisher( rospy.get_param( - "~image_distance_topic", "/paf/hero/Radar/Visualization" + "~visualisation_topic", "/paf/hero/Radar/Visualization" ), PointCloud2, queue_size=10, ) self.marker_visualization_radar_publisher = rospy.Publisher( rospy.get_param( - "~image_distance_topic", "/paf/hero/Radar/Marker" + "~marker_topic", "/paf/hero/Radar/Marker" ), MarkerArray, queue_size=10, ) self.cluster_info_radar_publisher = rospy.Publisher( rospy.get_param( - "~image_distance_topic", "/paf/hero/Radar/ClusterInfo" + "~clusterInfo_topic_topic", "/paf/hero/Radar/ClusterInfo" ), String, queue_size=10, @@ -133,12 +133,12 @@ def filter_data(data, min_x=-100, max_x=100, min_y=-100, max_y=100, min_z=-1, ma data (np.ndarray): A 2D numpy array containing radar data, where each row represents a data point with the format [x, y, z, distance]. The array shape is (N, 4), where N is the number of points. - min_x (float, optional): Minimum value for the x-coordinate. Default is -1. - max_x (float, optional): Maximum value for the x-coordinate. Default is 1. - min_y (float, optional): Minimum value for the y-coordinate. Default is 1. - max_y (float, optional): Maximum value for the y-coordinate. Default is 1. - min_z (float, optional): Minimum value for the z-coordinate. Default is -0.7. - max_z (float, optional): Maximum value for the z-coordinate. Default is 1.3. + min_x (float, optional): Minimum value for the x-coordinate. Default is -100. + max_x (float, optional): Maximum value for the x-coordinate. Default is 100. + min_y (float, optional): Minimum value for the y-coordinate. Default is -100. + max_y (float, optional): Maximum value for the y-coordinate. Default is 100. + min_z (float, optional): Minimum value for the z-coordinate. Default is -1. + max_z (float, optional): Maximum value for the z-coordinate. Default is 100. max_distance (float, optional): Maximum allowable distance of the point from the sensor. Default is 100. @@ -160,7 +160,8 @@ def filter_data(data, min_x=-100, max_x=100, min_y=-100, max_y=100, min_z=-1, ma def cluster_data(data, eps=0.8, min_samples=3): - """_summary_ + """ + Clusters the radar data using the DBSCAN algorithm Args: data (np.ndarray): data array which should be clustered @@ -171,6 +172,7 @@ def cluster_data(data, eps=0.8, min_samples=3): dict: A dictionary where the keys are cluster labels (int) and the values are the number of points in each cluster. Returns an empty dictionary if no points are available. + DBSCAN: A DBSCAN clustering object containing labels and core sample indices """ if len(data) == 0: @@ -458,7 +460,7 @@ def clear_old_markers(marker_array, max_id): MarkerArray: The updated MarkerArray with old markers removed. """ for marker in marker_array.markers: - if marker.id >= max_id: + if marker.id > max_id: marker.action = Marker.DELETE return marker_array @@ -493,8 +495,8 @@ def generate_cluster_info(clusters, data, marker_array, bounding_boxes): cluster_info.append({ "label": int(label), "points_count": cluster_size, - "Anzahl marker": len(marker_array.markers), - "Anzahl Boundingboxen": len(bounding_boxes) + "num_marker": len(marker_array.markers), + "num_bounding_boxes": len(bounding_boxes) }) return json.dumps(cluster_info) From 40a4595c977687321b7b3d63d5b077b218b6cc86 Mon Sep 17 00:00:00 2001 From: Ralf Date: Wed, 11 Dec 2024 15:55:29 +0100 Subject: [PATCH 31/55] added black formatter extension --- code/perception/src/radar_node.py | 83 +++++++++++++++++++------------ 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/code/perception/src/radar_node.py b/code/perception/src/radar_node.py index 246e86cf..fcb32ddd 100755 --- a/code/perception/src/radar_node.py +++ b/code/perception/src/radar_node.py @@ -40,8 +40,9 @@ def callback(self, data): cloud = create_pointcloud2(dataarray, clustered_data.labels_) self.visualization_radar_publisher.publish(cloud) - points_with_labels = np.hstack((dataarray, - clustered_data.labels_.reshape(-1, 1))) + points_with_labels = np.hstack( + (dataarray, clustered_data.labels_.reshape(-1, 1)) + ) bounding_boxes = generate_bounding_boxes(points_with_labels) marker_array = MarkerArray() @@ -58,8 +59,9 @@ def callback(self, data): self.marker_visualization_radar_publisher.publish(marker_array) - cluster_info = generate_cluster_info(clustered_data, dataarray, marker_array, - bounding_boxes) + cluster_info = generate_cluster_info( + clustered_data, dataarray, marker_array, bounding_boxes + ) self.cluster_info_radar_publisher.publish(cluster_info) def listener(self): @@ -73,23 +75,17 @@ def listener(self): queue_size=10, ) self.visualization_radar_publisher = rospy.Publisher( - rospy.get_param( - "~visualisation_topic", "/paf/hero/Radar/Visualization" - ), + rospy.get_param("~visualisation_topic", "/paf/hero/Radar/Visualization"), PointCloud2, queue_size=10, ) self.marker_visualization_radar_publisher = rospy.Publisher( - rospy.get_param( - "~marker_topic", "/paf/hero/Radar/Marker" - ), + rospy.get_param("~marker_topic", "/paf/hero/Radar/Marker"), MarkerArray, queue_size=10, ) self.cluster_info_radar_publisher = rospy.Publisher( - rospy.get_param( - "~clusterInfo_topic_topic", "/paf/hero/Radar/ClusterInfo" - ), + rospy.get_param("~clusterInfo_topic_topic", "/paf/hero/Radar/ClusterInfo"), String, queue_size=10, ) @@ -121,8 +117,16 @@ def pointcloud2_to_array(pointcloud_msg): ) -def filter_data(data, min_x=-100, max_x=100, min_y=-100, max_y=100, min_z=-1, max_z=100, - max_distance=100): +def filter_data( + data, + min_x=-100, + max_x=100, + min_y=-100, + max_y=100, + min_z=-1, + max_z=100, + max_distance=100, +): """ Filters radar data based on specified spatial and distance constraints. @@ -218,14 +222,14 @@ def create_pointcloud2(clustered_points, cluster_labels): else: r, g, b = colors[label] - rgb = struct.unpack('f', struct.pack('I', (r << 16) | (g << 8) | b))[0] + rgb = struct.unpack("f", struct.pack("I", (r << 16) | (g << 8) | b))[0] points.append([x, y, z, rgb]) fields = [ - PointField('x', 0, PointField.FLOAT32, 1), - PointField('y', 4, PointField.FLOAT32, 1), - PointField('z', 8, PointField.FLOAT32, 1), - PointField('rgb', 12, PointField.FLOAT32, 1), + PointField("x", 0, PointField.FLOAT32, 1), + PointField("y", 4, PointField.FLOAT32, 1), + PointField("z", 8, PointField.FLOAT32, 1), + PointField("rgb", 12, PointField.FLOAT32, 1), ] return point_cloud2.create_cloud(header, fields, points) @@ -378,9 +382,18 @@ def create_bounding_box_marker(label, bbox): ] lines = [ - (0, 1), (1, 2), (2, 3), (3, 0), # Bottom - (4, 5), (5, 6), (6, 7), (7, 4), # Top - (0, 4), (1, 5), (2, 6), (3, 7), # Vertical Edges + (0, 1), + (1, 2), + (2, 3), + (3, 0), # Bottom + (4, 5), + (5, 6), + (6, 7), + (7, 4), # Top + (0, 4), + (1, 5), + (2, 6), + (3, 7), # Vertical Edges ] for start, end in lines: marker.points.append(points[start]) @@ -390,9 +403,13 @@ def create_bounding_box_marker(label, bbox): # can be used for extra debugging -def create_min_max_markers(label, bbox, frame_id="hero/RADAR", - min_color=(0.0, 1.0, 0.0, 1.0), - max_color=(1.0, 0.0, 0.0, 1.0)): +def create_min_max_markers( + label, + bbox, + frame_id="hero/RADAR", + min_color=(0.0, 1.0, 0.0, 1.0), + max_color=(1.0, 0.0, 0.0, 1.0), +): """ creates RViz-Markers for min- and max-points of a bounding box. @@ -492,12 +509,14 @@ def generate_cluster_info(clusters, data, marker_array, bounding_boxes): cluster_points = data[clusters.labels_ == label] cluster_size = len(cluster_points) if label != -1: - cluster_info.append({ - "label": int(label), - "points_count": cluster_size, - "num_marker": len(marker_array.markers), - "num_bounding_boxes": len(bounding_boxes) - }) + cluster_info.append( + { + "label": int(label), + "points_count": cluster_size, + "num_marker": len(marker_array.markers), + "num_bounding_boxes": len(bounding_boxes), + } + ) return json.dumps(cluster_info) From fc396341a8315d1fa5eaf97fa210a19dc9ccdaaf Mon Sep 17 00:00:00 2001 From: michalal7 Date: Wed, 11 Dec 2024 16:04:20 +0100 Subject: [PATCH 32/55] Restored standard settings in file '/code/agent/config/rviz_config.rviz' --- code/agent/config/rviz_config.rviz | 93 +++++++----------------------- 1 file changed, 20 insertions(+), 73 deletions(-) diff --git a/code/agent/config/rviz_config.rviz b/code/agent/config/rviz_config.rviz index 998284c8..2e419cc2 100644 --- a/code/agent/config/rviz_config.rviz +++ b/code/agent/config/rviz_config.rviz @@ -1,12 +1,14 @@ Panels: - Class: rviz/Displays - Help Height: 70 + Help Height: 78 Name: Displays Property Tree Widget: Expanded: - - /PointCloud2 dist_clustered1 + - /PointCloud23 + - /PointCloud24 + - /PointCloud25 Splitter Ratio: 0.5 - Tree Height: 325 + Tree Height: 308 - Class: rviz/Selection Name: Selection - Class: rviz/Tool Properties @@ -64,23 +66,26 @@ Visualization Manager: Grid: false Imu: false Path: false - PointCloud2: true - PointCloud2 dist_clustered: true + PointCloud2: false Value: true - VisonNode Output: true Zoom Factor: 1 - Class: rviz/Image Enabled: true + Image Rendering: background and overlay Image Topic: /paf/hero/Center/segmented_image - Max Value: 1 - Median window: 5 - Min Value: 0 Name: VisonNode Output - Normalize Range: true + Overlay Alpha: 0.5 Queue Size: 2 Transport Hint: raw Unreliable: false Value: true + Visibility: + Grid: true + Imu: true + Path: true + PointCloud2: true + Value: true + Zoom Factor: 1 - Alpha: 1 Class: rviz_plugin_tutorials/Imu Color: 204; 51; 204 @@ -255,62 +260,6 @@ Visualization Manager: Use Fixed Frame: true Use rainbow: true Value: true - - Alpha: 1 - Autocompute Intensity Bounds: true - Autocompute Value Bounds: - Max Value: 10 - Min Value: -10 - Value: true - Axis: Z - Channel Name: intensity - Class: rviz/PointCloud2 - Color: 255; 255; 255 - Color Transformer: "" - Decay Time: 0 - Enabled: true - Invert Rainbow: false - Max Color: 255; 255; 255 - Min Color: 0; 0; 0 - Name: PointCloud2 - Position Transformer: "" - Queue Size: 10 - Selectable: true - Size (Pixels): 3 - Size (m): 0.009999999776482582 - Style: Flat Squares - Topic: /paf/hero/dist_clustered - Unreliable: false - Use Fixed Frame: true - Use rainbow: true - Value: true - - Alpha: 1 - Autocompute Intensity Bounds: true - Autocompute Value Bounds: - Max Value: 10 - Min Value: -10 - Value: true - Axis: Z - Channel Name: intensity - Class: rviz/PointCloud2 - Color: 255; 255; 255 - Color Transformer: "" - Decay Time: 0 - Enabled: true - Invert Rainbow: false - Max Color: 255; 255; 255 - Min Color: 0; 0; 0 - Name: PointCloud2 dist_clustered - Position Transformer: "" - Queue Size: 10 - Selectable: true - Size (Pixels): 3 - Size (m): 0.009999999776482582 - Style: Flat Squares - Topic: /paf/hero/dist_clustered - Unreliable: false - Use Fixed Frame: true - Use rainbow: true - Value: true Enabled: true Global Options: Background Color: 48; 48; 48 @@ -339,7 +288,7 @@ Visualization Manager: Views: Current: Class: rviz/Orbit - Distance: 10.540092468261719 + Distance: 34.785499572753906 Enable Stereo Rendering: Stereo Eye Separation: 0.05999999865889549 Stereo Focal Distance: 1 @@ -355,9 +304,9 @@ Visualization Manager: Invert Z Axis: false Name: Current View Near Clip Distance: 0.009999999776482582 - Pitch: 0.3953982889652252 + Pitch: 0.19039836525917053 Target Frame: - Yaw: 3.255431652069092 + Yaw: 4.520427227020264 Saved: ~ Window Geometry: Camera: @@ -367,7 +316,7 @@ Window Geometry: Height: 1376 Hide Left Dock: false Hide Right Dock: false - QMainWindow State: 000000ff00000000fd000000040000000000000304000004c6fc020000000afb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003b000001c6000000c700fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000c00430061006d00650072006101000002070000021e0000001600fffffffb00000020005600690073006f006e004e006f006400650020004f00750074007000750074010000042b000000d60000001600ffffff000000010000010f000004c6fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000003b000004c6000000a000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000009b80000003efc0100000002fb0000000800540069006d00650100000000000009b80000030700fffffffb0000000800540069006d0065010000000000000450000000000000000000000599000004c600000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 + QMainWindow State: 000000ff00000000fd000000040000000000000304000004c6fc0200000009fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003b0000022a000000c700fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000c00430061006d006500720061010000026b000002960000001600ffffff000000010000010f000004c6fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000003b000004c6000000a000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000009b80000003efc0100000002fb0000000800540069006d00650100000000000009b80000030700fffffffb0000000800540069006d0065010000000000000450000000000000000000000599000004c600000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 Selection: collapsed: false Time: @@ -376,8 +325,6 @@ Window Geometry: collapsed: false Views: collapsed: false - VisonNode Output: - collapsed: false Width: 2488 X: 1992 - Y: 27 + Y: 27 \ No newline at end of file From d6615f519d366fbe11ff0437878556c5f48581b8 Mon Sep 17 00:00:00 2001 From: michalal7 Date: Wed, 11 Dec 2024 16:09:23 +0100 Subject: [PATCH 33/55] Removed debugger arguments. --- code/perception/launch/perception.launch | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/code/perception/launch/perception.launch b/code/perception/launch/perception.launch index 127ae9a4..0133a9d4 100644 --- a/code/perception/launch/perception.launch +++ b/code/perception/launch/perception.launch @@ -1,6 +1,6 @@ - + @@ -75,7 +75,7 @@ - + From 0fb8ae60b718c3bdd65d1996bea4342fc7be87e9 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 01:30:06 +0100 Subject: [PATCH 34/55] Add files via upload added new behaviour tree doc assets --- doc/assets/planning/behaviour_tree.PNG | Bin 0 -> 167731 bytes doc/assets/planning/behaviour_tree.drawio.xml | 230 ++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 doc/assets/planning/behaviour_tree.PNG create mode 100644 doc/assets/planning/behaviour_tree.drawio.xml diff --git a/doc/assets/planning/behaviour_tree.PNG b/doc/assets/planning/behaviour_tree.PNG new file mode 100644 index 0000000000000000000000000000000000000000..87ff5eb42a77a987b983eb6dd6bf156d9c3e6924 GIT binary patch literal 167731 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 reF+)A$kM{ZjKY4rhRJAL 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�BGhE2A(I~K&pje(Qr-zTQQ8iTgCrDA*Z&{p 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 95e7ce237e8e0b0d464f6134701a3c927b643787 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 02:20:50 +0100 Subject: [PATCH 35/55] Update Behavior_tree.md --- doc/planning/Behavior_tree.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/planning/Behavior_tree.md b/doc/planning/Behavior_tree.md index aae559df..bf78edb9 100644 --- a/doc/planning/Behavior_tree.md +++ b/doc/planning/Behavior_tree.md @@ -36,9 +36,9 @@ For visualization at runtime you might want to also install this [rqt-Plugin](ht ## Our behaviour tree The following section describes the behaviour tree we use for normal driving using all functionality provided by the agent. In the actual implementation this is part of a bigger tree, that handles things like writing topics to the blackboard, starting and finishing the decision tree. -The following tree is a simplification. +The following tree is a simplification. The draw.io xml file to update this diagram can be found inside /assets/planning/. -![Simple Tree](../assets/planning/simple_final_tree.png) +![Simple Tree](../assets/planning/behaviour_tree.PNG) ### Behavior From c4dd807dce32c4faaedbe8f62ca4a74c12149a77 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 02:22:27 +0100 Subject: [PATCH 36/55] Update README.md --- doc/planning/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/planning/README.md b/doc/planning/README.md index aff116ab..2fc268a0 100644 --- a/doc/planning/README.md +++ b/doc/planning/README.md @@ -25,7 +25,7 @@ The decision making collects most of the available information of the other comp the information. All possible traffic scenarios are covered in this component. The decision making uses a so called decision tree, which is easy to adapt and to expand. -![Simple Tree](../assets/planning/simple_final_tree.png) +![Simple Tree](../assets/planning/behaviour_tree.PNG) ### [Local Planning](./Local_Planning.md) From 0fef10c3e39296ecadd2c64c40c8712ef7a43d4c Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 02:26:05 +0100 Subject: [PATCH 37/55] Create Intersection.md --- doc/planning/behaviours/Intersection.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 doc/planning/behaviours/Intersection.md diff --git a/doc/planning/behaviours/Intersection.md b/doc/planning/behaviours/Intersection.md new file mode 100644 index 00000000..776928a9 --- /dev/null +++ b/doc/planning/behaviours/Intersection.md @@ -0,0 +1,6 @@ +# Intersection Behavior + +**Summary:** This file explains the Intersection behavior. + +- [Intersection Behavior](#intersection-behavior) + - [Explanation](#explanation) From e5b50194086954c9f6b715e6b0d5d148ebd59f31 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 02:26:48 +0100 Subject: [PATCH 38/55] Rename doc/planning/Unstuck_Behavior.md to doc/planning/behaviours/Unstuck_Behavior.md --- doc/planning/{ => behaviours}/Unstuck_Behavior.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/planning/{ => behaviours}/Unstuck_Behavior.md (100%) diff --git a/doc/planning/Unstuck_Behavior.md b/doc/planning/behaviours/Unstuck_Behavior.md similarity index 100% rename from doc/planning/Unstuck_Behavior.md rename to doc/planning/behaviours/Unstuck_Behavior.md From 083a0b0a29aea597fdf662033a855ba2e7239c1a Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 03:59:48 +0100 Subject: [PATCH 39/55] Update Intersection.md --- doc/planning/behaviours/Intersection.md | 38 ++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/doc/planning/behaviours/Intersection.md b/doc/planning/behaviours/Intersection.md index 776928a9..295b3679 100644 --- a/doc/planning/behaviours/Intersection.md +++ b/doc/planning/behaviours/Intersection.md @@ -3,4 +3,40 @@ **Summary:** This file explains the Intersection behavior. - [Intersection Behavior](#intersection-behavior) - - [Explanation](#explanation) + - [General](#general) + +## General + +The Intersection behaviour is used to control the vehicle when it encounters a intersection. It handles stop signs as well as traffic lights. +The Intersection sequence consists of the sub-behaviours "Approach", "Wait", "Enter" and "Leave". + +To enter the Intersection sequence "Intersection ahead" must firstly be successful. + +## Intersection ahead + +Successful when there is a stop line within a set distance. + +## Approach + +Handles approaching the intersection by slowing down the vehicle. Returns RUNNING while still far from the intersection, SUCCESS when the vehicle has stopped or has already entered the intersection and FAILURE when the path is faulty. + +Calculates a virtual stopline based on whether a stopline or only a stop sign has been detected and publishes a distance to it. While the vehicle is not stopped at the virtual stopline nor has entered the intersection, int_app_to_stop is published as the current behaviour. +This is used inside motion_planning to calculate a stopping velocity. + +A green light is approached with 8 m/s. + +## Wait + +Handles wating at the stop line until the vehicle is allowed to drive. + +If the light is green or when there isn't a traffic light returns SUCCESS otherwise RUNNING. + +## Enter + +Handles driving through the intersection. + +Returns SUCCESS once the next waypoint is not this intersection anymore. + +## Leave + +Signifies that the vehicle has left the intersection and simply returns FAILURE to leave the Intersection sequence. From 6efa6c2f7b8b90c54b792c3d52fba1be86d7f1de Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 04:01:18 +0100 Subject: [PATCH 40/55] Update Intersection.md --- doc/planning/behaviours/Intersection.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/planning/behaviours/Intersection.md b/doc/planning/behaviours/Intersection.md index 295b3679..dd47c544 100644 --- a/doc/planning/behaviours/Intersection.md +++ b/doc/planning/behaviours/Intersection.md @@ -4,6 +4,11 @@ - [Intersection Behavior](#intersection-behavior) - [General](#general) + - [Intersection Ahead](#intersection-ahead) + - [Approach](#approach) + - [Wait](#wait) + - [Enter](#enter) + - [Leave](#leave) ## General From ecc18384a42cdcb128aa904cb3da63e1fb527017 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:33:13 +0100 Subject: [PATCH 41/55] Create Overtake.md --- doc/planning/behaviours/Overtake.md | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 doc/planning/behaviours/Overtake.md diff --git a/doc/planning/behaviours/Overtake.md b/doc/planning/behaviours/Overtake.md new file mode 100644 index 00000000..a8df0dd0 --- /dev/null +++ b/doc/planning/behaviours/Overtake.md @@ -0,0 +1,45 @@ +# Overtake Behavior + +**Summary:** This file explains the Overtake behavior. + +- [Overtake Behavior](#overtake-behavior) + - [General](#general) + - [Overtake Ahead](#overtake-ahead) + - [Approach](#approach) + - [Wait](#wait) + - [Enter](#enter) + - [Leave](#leave) + +## General + +This behaviour is used to overtake an object in close proximity. This behaviour is currently not working and more like a initial prototype. + +## Overtake ahead + +Checks whether there is a object in front of the car that needs overtaking. + +Estimates whether the car would collide with the object soon. If that is the case a counter gets incremented. When that counter reaches 4 SUCCESS is returned. If the object is not blocking the trajectory, FAILURE is returned. + +## Approach + +This is running while the obstacle is still in front of the car. + +Checks whether the oncoming traffic is far away or clear, if that is the case then ot_app_free is published as the current behaviour for the motion_planner and returns SUCCESS. Otherwise ot_app_blocked is published for the car to slow down. + +If the car stops behind the obstacle SUCCESS is also returned. + +## Wait + +This handles wating for clear oncoming traffic if the car has stopped behind the obstacle. If the overtake is clear ot_wait_free gets published and SUCCESS is returned. Otherwise ot_wait_stopped gets published and the behaviour stays in RUNNING. + +If the obstacle in front is gone the behaviour is aborted with FAILURE. + +## Enter + +Handles switching the lane for overtaking. + +Waits for motion planner to finish the trajectory changes and for it to set the overtake_success flag. + +## Leave + +Runs until the overtake is fully finished and then leaves the behaviour. From 735c971baea137a2e712fe05019412112b0258e9 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:11:11 +0100 Subject: [PATCH 42/55] Update overtake.py Updated comments and log messages for better understanding/debugging. No logic changes. --- .../src/behavior_agent/behaviours/overtake.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/code/planning/src/behavior_agent/behaviours/overtake.py b/code/planning/src/behavior_agent/behaviours/overtake.py index f90ec1ae..133e3c86 100644 --- a/code/planning/src/behavior_agent/behaviours/overtake.py +++ b/code/planning/src/behavior_agent/behaviours/overtake.py @@ -75,8 +75,8 @@ def update(self): - Set a feedback message - return a py_trees.common.Status.[RUNNING, SUCCESS, FAILURE] - Gets the current distance to overtake, the current lane status and the - distance to collsion object. + Gets the current distance to overtake, the current oncoming lane status and the + distance to collsion object. Slows down while oncoming blocked until stopped or oncoming clear. :return: py_trees.common.Status.RUNNING, if too far from overtaking py_trees.common.Status.SUCCESS, if stopped behind the blocking object or entered the process. @@ -110,7 +110,7 @@ def update(self): self.curr_behavior_pub.publish(bs.ot_app_free.name) return py_trees.common.Status.SUCCESS else: - rospy.loginfo("Overtake blocked slowing down") + rospy.loginfo("Overtake Approach: oncoming blocked slowing down") self.curr_behavior_pub.publish(bs.ot_app_blocked.name) # get speed @@ -123,11 +123,11 @@ def update(self): if self.ot_distance > 20.0: # too far - rospy.loginfo("still approaching") + rospy.loginfo("Overtake Approach: still approaching obstacle, distance: {self.ot_distance}") return py_trees.common.Status.RUNNING elif speed < convert_to_ms(2.0) and self.ot_distance < TARGET_DISTANCE_TO_STOP: # stopped - rospy.loginfo("stopped") + rospy.loginfo("Overtake Approach: stopped behind obstacle") return py_trees.common.Status.SUCCESS else: # still approaching @@ -216,7 +216,7 @@ def update(self): clear_distance = 30 obstacle_msg = self.blackboard.get("/paf/hero/collision") if obstacle_msg is None: - rospy.logerr("No OBSTACLE") + rospy.logerr("No OBSTACLE in overtake wait") return py_trees.common.Status.FAILURE data = self.blackboard.get("/paf/hero/oncoming") @@ -231,14 +231,14 @@ def update(self): self.curr_behavior_pub.publish(bs.ot_wait_free.name) return py_trees.common.Status.SUCCESS else: - rospy.loginfo(f"Overtake still blocked: {distance_oncoming}") + rospy.loginfo(f"Overtake still blocked, distance to oncoming: {distance_oncoming}") self.curr_behavior_pub.publish(bs.ot_wait_stopped.name) return py_trees.common.Status.RUNNING elif obstacle_msg.data[0] == np.inf: - rospy.loginf("No OBSTACLE") + rospy.loginf("No OBSTACLE in overtake wait") return py_trees.common.Status.FAILURE else: - rospy.loginfo("No Lidar Distance") + rospy.loginfo("No Lidar Distance in overtake wait") return py_trees.common.Status.SUCCESS def terminate(self, new_status): @@ -312,7 +312,7 @@ def update(self): - Set a feedback message - return a py_trees.common.Status.[RUNNING, SUCCESS, FAILURE] - + Waits for motion_planner to finish the new trajectory. :return: py_trees.common.Status.RUNNING, py_trees.common.Status.SUCCESS, py_trees.common.Status.FAILURE, @@ -320,17 +320,17 @@ def update(self): status = self.blackboard.get("/paf/hero/overtake_success") if status is not None: if status.data == 1: - rospy.loginfo("Overtake: Trajectory planned") + rospy.loginfo("Overtake Enter: Trajectory planned") return py_trees.common.Status.SUCCESS elif status.data == 0: self.curr_behavior_pub.publish(bs.ot_enter_slow.name) - rospy.loginfo("Overtake: Slowing down") + rospy.loginfo("Overtake Enter: Slowing down") return py_trees.common.Status.RUNNING else: - rospy.loginfo("OvertakeEnter: Abort") + rospy.loginfo("Overtake Enter: Abort") return py_trees.common.Status.FAILURE else: - rospy.loginfo("Overtake: Waiting for status update") + rospy.loginfo("Overtake Enter: Waiting for status update") return py_trees.common.Status.RUNNING def terminate(self, new_status): @@ -393,7 +393,7 @@ def initialise(self): self.curr_behavior_pub.publish(bs.ot_leave.name) data = self.blackboard.get("/paf/hero/current_pos") self.first_pos = np.array([data.pose.position.x, data.pose.position.y]) - rospy.loginfo(f"Leave Overtake: {self.first_pos}") + rospy.loginfo(f"Init Leave Overtake: {self.first_pos}") return True def update(self): @@ -412,7 +412,7 @@ def update(self): self.current_pos = np.array([data.pose.position.x, data.pose.position.y]) distance = np.linalg.norm(self.first_pos - self.current_pos) if distance > OVERTAKE_EXECUTING + NUM_WAYPOINTS: - rospy.loginfo(f"Left Overtake: {self.current_pos}") + rospy.loginfo(f"Overtake executed: {self.current_pos}") return py_trees.common.Status.FAILURE else: return py_trees.common.Status.RUNNING From 486e5bf62e4f8112c1dd6f805e89e80c6d31b04a Mon Sep 17 00:00:00 2001 From: SirMDA Date: Thu, 12 Dec 2024 17:16:24 +0100 Subject: [PATCH 43/55] fixed lidar_distance node speed by removing for loop --- code/perception/src/lidar_distance.py | 129 ++++++++++++++++---------- 1 file changed, 79 insertions(+), 50 deletions(-) mode change 100644 => 100755 code/perception/src/lidar_distance.py diff --git a/code/perception/src/lidar_distance.py b/code/perception/src/lidar_distance.py old mode 100644 new mode 100755 index 939224c5..bea1d550 --- a/code/perception/src/lidar_distance.py +++ b/code/perception/src/lidar_distance.py @@ -3,7 +3,7 @@ import rospy import ros_numpy import numpy as np -import lidar_filter_utility +from lidar_filter_utility import bounding_box, remove_field_name from sensor_msgs.msg import PointCloud2, Image as ImageMsg from sklearn.cluster import DBSCAN from cv_bridge import CvBridge @@ -189,14 +189,12 @@ def calculate_image(self, coordinates, focus): return None # Apply bounding box filter - reconstruct_bit_mask = lidar_filter_utility.bounding_box(coordinates, **params) + reconstruct_bit_mask = bounding_box(coordinates, **params) reconstruct_coordinates = coordinates[reconstruct_bit_mask] # Remove the "intensity" field and convert to a NumPy array reconstruct_coordinates_xyz = np.array( - lidar_filter_utility.remove_field_name( - reconstruct_coordinates, "intensity" - ).tolist() + remove_field_name(reconstruct_coordinates, "intensity").tolist() ) # Reconstruct the image based on the focus @@ -256,51 +254,82 @@ def reconstruct_img_from_lidar(self, coordinates_xyz, focus): img = np.zeros(shape=(720, 1280), dtype=np.float32) dist_array = np.zeros(shape=(720, 1280, 3), dtype=np.float32) - # Process each point in the point cloud - for c in coordinates_xyz: - if focus == "Center": # Compute image for the center view - point = np.array([c[1], c[2], c[0], 1]) - pixel = np.matmul(m, point) # Project 3D point to 2D image coordinates - x, y = int(pixel[0] / pixel[2]), int( - pixel[1] / pixel[2] - ) # Normalize coordinates - if ( - 0 <= x <= 1280 and 0 <= y <= 720 - ): # Check if coordinates are within image bounds - img[719 - y][1279 - x] = c[0] # Set depth value - dist_array[719 - y][1279 - x] = np.array( - [c[0], c[1], c[2]], dtype=np.float32 - ) - - if focus == "Back": # Compute image for the rear view - point = np.array([c[1], c[2], c[0], 1]) - pixel = np.matmul(m, point) - x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) - if 0 <= x <= 1280 and 0 <= y < 720: - img[y][1279 - x] = -c[0] - dist_array[y][1279 - x] = np.array( - [-c[0], c[1], c[2]], dtype=np.float32 - ) - - if focus == "Left": # Compute image for the left view - point = np.array([c[0], c[2], c[1], 1]) - pixel = np.matmul(m, point) - x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) - if 0 <= x <= 1280 and 0 <= y <= 720: - img[719 - y][1279 - x] = c[1] - dist_array[y][1279 - x] = np.array( - [c[0], c[1], c[2]], dtype=np.float32 - ) - - if focus == "Right": # Compute image for the right view - point = np.array([c[0], c[2], c[1], 1]) - pixel = np.matmul(m, point) - x, y = int(pixel[0] / pixel[2]), int(pixel[1] / pixel[2]) - if 0 <= x < 1280 and 0 <= y < 720: - img[y][1279 - x] = -c[1] - dist_array[y][1279 - x] = np.array( - [c[0], c[1], c[2]], dtype=np.float32 - ) + # Prepare points based on focus + if focus == "Center": + points = np.column_stack( + ( + coordinates_xyz[:, 1], + coordinates_xyz[:, 2], + coordinates_xyz[:, 0], + np.ones(coordinates_xyz.shape[0]), + ) + ) + elif focus == "Back": + points = np.column_stack( + ( + coordinates_xyz[:, 1], + coordinates_xyz[:, 2], + coordinates_xyz[:, 0], + np.ones(coordinates_xyz.shape[0]), + ) + ) + elif focus == "Left": + points = np.column_stack( + ( + coordinates_xyz[:, 0], + coordinates_xyz[:, 2], + coordinates_xyz[:, 1], + np.ones(coordinates_xyz.shape[0]), + ) + ) + elif focus == "Right": + points = np.column_stack( + ( + coordinates_xyz[:, 0], + coordinates_xyz[:, 2], + coordinates_xyz[:, 1], + np.ones(coordinates_xyz.shape[0]), + ) + ) + else: + rospy.logwarn(f"Unknown focus: {focus}. Skipping image calculation.") + return None + + # Project 3D points to 2D image coordinates + pixels = np.dot(m, points.T).T + x = (pixels[:, 0] / pixels[:, 2]).astype(int) + y = (pixels[:, 1] / pixels[:, 2]).astype(int) + + # Filter valid coordinates + valid_indices = (x >= 0) & (x < 1280) & (y >= 0) & (y < 720) + x = x[valid_indices] + y = y[valid_indices] + valid_coordinates = coordinates_xyz[valid_indices] + + if focus == "Center": + img[719 - y, 1279 - x] = valid_coordinates[:, 0] + dist_array[719 - y, 1279 - x] = valid_coordinates + elif focus == "Back": + img[y, 1279 - x] = -valid_coordinates[:, 0] + dist_array[y, 1279 - x] = np.column_stack( + ( + -valid_coordinates[:, 0], + valid_coordinates[:, 1], + valid_coordinates[:, 2], + ) + ) + elif focus == "Left": + img[719 - y, 1279 - x] = valid_coordinates[:, 1] + dist_array[719 - y, 1279 - x] = valid_coordinates + elif focus == "Right": + img[y, 1279 - x] = -valid_coordinates[:, 1] + dist_array[y, 1279 - x] = np.column_stack( + ( + valid_coordinates[:, 0], + -valid_coordinates[:, 1], + valid_coordinates[:, 2], + ) + ) return dist_array From eefcd29f6690efbfa765c140a77261c161386f05 Mon Sep 17 00:00:00 2001 From: SirMDA Date: Thu, 12 Dec 2024 17:33:37 +0100 Subject: [PATCH 44/55] optimized focus if clause --- code/perception/src/lidar_distance.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/code/perception/src/lidar_distance.py b/code/perception/src/lidar_distance.py index bea1d550..5c937be0 100755 --- a/code/perception/src/lidar_distance.py +++ b/code/perception/src/lidar_distance.py @@ -255,16 +255,7 @@ def reconstruct_img_from_lidar(self, coordinates_xyz, focus): dist_array = np.zeros(shape=(720, 1280, 3), dtype=np.float32) # Prepare points based on focus - if focus == "Center": - points = np.column_stack( - ( - coordinates_xyz[:, 1], - coordinates_xyz[:, 2], - coordinates_xyz[:, 0], - np.ones(coordinates_xyz.shape[0]), - ) - ) - elif focus == "Back": + if focus in ["Center", "Back"]: points = np.column_stack( ( coordinates_xyz[:, 1], @@ -273,16 +264,7 @@ def reconstruct_img_from_lidar(self, coordinates_xyz, focus): np.ones(coordinates_xyz.shape[0]), ) ) - elif focus == "Left": - points = np.column_stack( - ( - coordinates_xyz[:, 0], - coordinates_xyz[:, 2], - coordinates_xyz[:, 1], - np.ones(coordinates_xyz.shape[0]), - ) - ) - elif focus == "Right": + elif focus in ["Left", "Right"]: points = np.column_stack( ( coordinates_xyz[:, 0], From dc053903ed2c618d2a57ff8065d240a77b783b7c Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:02:17 +0100 Subject: [PATCH 45/55] Create LaneChange.md --- doc/planning/behaviours/LaneChange.md | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 doc/planning/behaviours/LaneChange.md diff --git a/doc/planning/behaviours/LaneChange.md b/doc/planning/behaviours/LaneChange.md new file mode 100644 index 00000000..f8440e9d --- /dev/null +++ b/doc/planning/behaviours/LaneChange.md @@ -0,0 +1,39 @@ +# Lane Change Behavior + +**Summary:** This file explains the Lane Change behavior. + +- [Lane Change Behavior](#lanechange-behavior) + - [General](#general) + - [Lane Change Ahead](#lanechange-ahead) + - [Approach](#approach) + - [Wait](#wait) + - [Enter](#enter) + - [Leave](#leave) + +## General + +This behaviour executes a lane change. It slows the vehicle down until the lane change point is reached and then proceeds to switch lanes. + +## Lane Change ahead + +Checks whether the next waypoint is a lane change and inititates the lane change sequence accordingly. + +## Approach + +Calculates a virtual stop line at the lane change point and publishes lc_app_blocked for motion planner to slow down while too far away. + +If the lane change is not blocked (currently not implemented) the car does not slow down (30 km/h). + +Once the car is within a set distance of the virtual stop line and not blocked it returns SUCCESS. SUCCESS is also returned when the car stops at the stop line. + +## Wait + +Waits at the lane change point until the lane change is not blocked (not implemented). + +## Enter + +Inititates the lane change with 20 km/h and continues driving on the next lane until the lane change waypoint is far enough away. + +## Leave + +Simply exits the behaviour. From 53f678c4f8c9215b3a242cb61df757abfbf9f284 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:20:21 +0100 Subject: [PATCH 46/55] Create LeaveParkingSpace.md --- doc/planning/behaviours/LeaveParkingSpace.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 doc/planning/behaviours/LeaveParkingSpace.md diff --git a/doc/planning/behaviours/LeaveParkingSpace.md b/doc/planning/behaviours/LeaveParkingSpace.md new file mode 100644 index 00000000..716ade84 --- /dev/null +++ b/doc/planning/behaviours/LeaveParkingSpace.md @@ -0,0 +1,13 @@ +# Leave Parking Space Behavior + +**Summary:** This file explains the Leave Parking Space behavior. + +- [Leave Parking Space Behavior](#leaveparkingspace-behavior) + - [General](#general) + +## General + +The leave parking space behaviour is only executed at the beginning of the simulation to leave the parking space. + +The behaviour calculates the euclidian distance between the starting position and the current position to determine whether the car has fully left the parking space. If that is the case a "called" +flag is set to true so that this behaviour is never executed again and FAILURE is returned to end the behaviour. Otherwise it stays in RUNNING. From ac930a289ce47dc002b1fc8a4756b9962eb594f4 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 21:11:00 +0100 Subject: [PATCH 47/55] Update lane_change.py Refined comments and log messages, this does not change any logic --- .../behavior_agent/behaviours/lane_change.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/code/planning/src/behavior_agent/behaviours/lane_change.py b/code/planning/src/behavior_agent/behaviours/lane_change.py index 49ce0d6f..87e18cb9 100755 --- a/code/planning/src/behavior_agent/behaviours/lane_change.py +++ b/code/planning/src/behavior_agent/behaviours/lane_change.py @@ -27,7 +27,7 @@ def __init__(self, name): :param name: name of the behaviour """ super(Approach, self).__init__(name) - rospy.loginfo("Approach started") + rospy.loginfo("Lane Change Approach started") def setup(self, timeout): """ @@ -72,7 +72,8 @@ def update(self): - Triggering, checking, monitoring. Anything...but do not block! - Set a feedback message - return a py_trees.common.Status.[RUNNING, SUCCESS, FAILURE] - Gets the current lane change distance. + Calculates a virtual stop line and slows down while approaching unless + lane change is not blocked. :return: py_trees.common.Status.RUNNING, if too far from lane change py_trees.common.Status.SUCCESS, if stopped in front of lane change or entered the lane change @@ -91,15 +92,15 @@ def update(self): if self.change_distance != np.inf and self.change_detected: self.virtual_change_distance = self.change_distance - # ADD FEATURE: Check for Traffic + # TODO: ADD FEATURE Check for Traffic distance_lidar = 20 if distance_lidar is not None and distance_lidar > 15.0: - rospy.loginfo("Change is free not slowing down!") + rospy.loginfo("Lane Change is free not slowing down!") self.curr_behavior_pub.publish(bs.lc_app_free.name) self.blocked = False else: - rospy.loginfo("Change blocked slowing down") + rospy.loginfo("Lane Change blocked slowing down") self.blocked = True # get speed @@ -110,11 +111,11 @@ def update(self): if speedometer is not None: speed = speedometer.speed else: - rospy.logwarn("no speedometer connected") + rospy.logwarn("Lane Change: no speedometer connected") return py_trees.common.Status.RUNNING if self.virtual_change_distance > target_dis and self.blocked: # too far - rospy.loginfo("still approaching") + rospy.loginfo("Lane Change: still approaching") self.curr_behavior_pub.publish(bs.lc_app_blocked.name) return py_trees.common.Status.RUNNING elif ( @@ -123,7 +124,7 @@ def update(self): and self.blocked ): # stopped - rospy.loginfo("stopped") + rospy.loginfo("Lane Change: stopped at virtual stop line") return py_trees.common.Status.SUCCESS elif ( speed > convert_to_ms(5.0) @@ -191,7 +192,7 @@ def initialise(self): Any initialisation you need before putting your behaviour to work. This just prints a state status message. """ - rospy.loginfo("Wait Change") + rospy.loginfo("Lane Change Wait") return True def update(self): @@ -213,14 +214,14 @@ def update(self): if speedometer is not None: speed = speedometer.speed else: - rospy.logwarn("no speedometer connected") + rospy.logwarn("Lane change wait: no speedometer connected") return py_trees.common.Status.RUNNING if speed > convert_to_ms(10): - rospy.loginfo("Forward to enter") + rospy.loginfo("Lane change wait: Was not blocked, proceed to drive forward") return py_trees.common.Status.SUCCESS - # ADD FEATURE: Check for Traffic + # TODO: ADD FEATURE Check for Traffic distance_lidar = 20 change_clear = False @@ -231,11 +232,11 @@ def update(self): else: change_clear = True if not change_clear: - rospy.loginfo("Change blocked") + rospy.loginfo("Lane Change Wait: blocked") self.curr_behavior_pub.publish(bs.lc_wait.name) return py_trees.common.Status.RUNNING else: - rospy.loginfo("Change clear") + rospy.loginfo("Lane Change Wait: Change clear") return py_trees.common.Status.SUCCESS def terminate(self, new_status): @@ -256,9 +257,8 @@ def terminate(self, new_status): class Enter(py_trees.behaviour.Behaviour): """ - This behavior handles the driving through an intersection, it initially - sets a speed and finishes if the ego vehicle is close to the end of the - intersection. + This behavior inititates the lane change and waits until the + lane change is finished. """ def __init__(self, name): @@ -297,7 +297,7 @@ def initialise(self): This prints a state status message and changes the driving speed for the lane change. """ - rospy.loginfo("Enter next Lane") + rospy.loginfo("Lane Change: Enter next Lane") self.curr_behavior_pub.publish(bs.lc_enter_init.name) def update(self): @@ -321,7 +321,7 @@ def update(self): if next_waypoint_msg is None: return py_trees.common.Status.FAILURE if next_waypoint_msg.distance < 5: - rospy.loginfo("Drive on the next lane!") + rospy.loginfo("Lane Change Enter: Drive on the next lane!") return py_trees.common.Status.RUNNING else: return py_trees.common.Status.SUCCESS @@ -345,7 +345,7 @@ def terminate(self, new_status): class Leave(py_trees.behaviour.Behaviour): """ This behaviour defines the leaf of this subtree, if this behavior is - reached, the vehicle left the intersection. + reached, the vehicle has finished the lane change. """ def __init__(self, name): @@ -384,7 +384,7 @@ def initialise(self): This prints a state status message and changes the driving speed to the street speed limit. """ - rospy.loginfo("Leave Change") + rospy.loginfo("Lane Change Finished") self.curr_behavior_pub.publish(bs.lc_exit.name) return True From 727f1d5dad110ae194d40ca7f3549943be9e4840 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 21:53:40 +0100 Subject: [PATCH 48/55] 529 - Implement service for planning - acting communication (#549) * implemented acting-planning service * Create RequestBehaviourChangeService.md * Update RequestBehaviourChangeService.md * Update RequestBehaviourChangeService.md * Update RequestBehaviourChangeService.py small comment fix --- code/planning/CMakeLists.txt | 7 +- code/planning/launch/planning.launch | 7 +- .../RequestBehaviourChangeService.py | 82 +++++++++++++++++++ code/planning/srv/RequestBehaviourChange.srv | 3 + doc/planning/RequestBehaviourChangeService.md | 26 ++++++ 5 files changed, 121 insertions(+), 4 deletions(-) create mode 100755 code/planning/src/behavior_agent/RequestBehaviourChangeService.py create mode 100644 code/planning/srv/RequestBehaviourChange.srv create mode 100644 doc/planning/RequestBehaviourChangeService.md diff --git a/code/planning/CMakeLists.txt b/code/planning/CMakeLists.txt index c6288e03..d8cf2bff 100755 --- a/code/planning/CMakeLists.txt +++ b/code/planning/CMakeLists.txt @@ -58,11 +58,12 @@ catkin_python_setup() ) ## Generate services in the 'srv' folder -# add_service_files( -# FILES +add_service_files( + FILES # Service1.srv # Service2.srv -# ) + RequestBehaviourChange.srv +) ## Generate actions in the 'action' folder # add_action_files( diff --git a/code/planning/launch/planning.launch b/code/planning/launch/planning.launch index 6662d9a3..d5803ddd 100644 --- a/code/planning/launch/planning.launch +++ b/code/planning/launch/planning.launch @@ -1,7 +1,7 @@ - + @@ -35,4 +35,9 @@ + + + + + diff --git a/code/planning/src/behavior_agent/RequestBehaviourChangeService.py b/code/planning/src/behavior_agent/RequestBehaviourChangeService.py new file mode 100755 index 00000000..aa058ab3 --- /dev/null +++ b/code/planning/src/behavior_agent/RequestBehaviourChangeService.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# import BehaviourEnum +import ros_compatibility as roscomp +from ros_compatibility.node import CompatibleNode +from rospy import Subscriber, Publisher +import rospy +from planning.srv import RequestBehaviourChange, RequestBehaviourChangeResponse +from std_msgs.msg import String, Int8 + + +class RequestBehaviourChangeService(CompatibleNode): + def __init__(self): + super(RequestBehaviourChangeService, self).__init__( + "RequestBehaviourChangeService" + ) + self.role_name = self.get_param("role_name", "hero") + self.control_loop_rate = self.get_param("control_loop_rate", 1) + self.__curr_behavior = None + + self.service = rospy.Service( + "RequestBehaviourChange", + RequestBehaviourChange, + self.handle_request_behaviour_change, + ) + + self.behaviour_pub: Publisher = self.new_publisher( + Int8, + f"/paf/{self.role_name}/behaviour_request", + qos_profile=1, + ) + + self.curr_behavior_sub: Subscriber = self.new_subscription( + String, + f"/paf/{self.role_name}/curr_behavior", + self.__set_curr_behavior, + qos_profile=1, + ) + + self.behaviour_pub.publish(0) + rospy.spin() + + def __set_curr_behavior(self, data: String): + """ + Sets the received current behavior of the vehicle. + """ + self.__curr_behavior = data.data + + def handle_request_behaviour_change(self, req): + if ( + self.__curr_behavior == "us_unstuck" + or self.__curr_behavior == "us_stop" + or self.__curr_behavior == "us_overtake" + or self.__curr_behavior == "Cruise" + ): + self.behaviour_pub.publish(req.request) + return RequestBehaviourChangeResponse(True) + else: + return RequestBehaviourChangeResponse(False) + + def run(self): + """ + Control loop + + :return: + """ + + self.spin() + + +if __name__ == "__main__": + """ + main function starts the RequestBehaviourChangeService node + :param args: + """ + roscomp.init("RequestBehaviourChangeService") + try: + node = RequestBehaviourChangeService() + node.run() + except KeyboardInterrupt: + pass + finally: + roscomp.shutdown() diff --git a/code/planning/srv/RequestBehaviourChange.srv b/code/planning/srv/RequestBehaviourChange.srv new file mode 100644 index 00000000..d4da3b97 --- /dev/null +++ b/code/planning/srv/RequestBehaviourChange.srv @@ -0,0 +1,3 @@ +uint8 request +--- +bool answer \ No newline at end of file diff --git a/doc/planning/RequestBehaviourChangeService.md b/doc/planning/RequestBehaviourChangeService.md new file mode 100644 index 00000000..fe0a3751 --- /dev/null +++ b/doc/planning/RequestBehaviourChangeService.md @@ -0,0 +1,26 @@ +# Request Behaviour Change Service + +This service is hosted in the node RequestBehaviourChangeService. + +Calling it requires a behaviour ID integer (based on the corresponding enum) and returns a bool depending on whether the request is granted. + +To use it, import RequestBehaviourChange from planning.srv. + +To call it, create a callable instance with: + +```python +rospy.wait_for_service('RequestBehaviourChange') + +name = rospy.ServiceProxy('RequestBehaviourChange', RequestBehaviourChange) +``` + +Then, you can just use this instance in a try setup: + +```python +try: + response = name(input) +except rospy.ServiceException as e: + # handle exception +``` + +For communication with the behaviour tree this node publishes a granted request to a topic behaviour_request. From e1036408238054f5e103c0f42aaaca443c774d8d Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:15:02 +0100 Subject: [PATCH 49/55] Update Intersection.md --- doc/planning/behaviours/Intersection.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/planning/behaviours/Intersection.md b/doc/planning/behaviours/Intersection.md index dd47c544..106a4a80 100644 --- a/doc/planning/behaviours/Intersection.md +++ b/doc/planning/behaviours/Intersection.md @@ -28,7 +28,7 @@ Handles approaching the intersection by slowing down the vehicle. Returns RUNNIN Calculates a virtual stopline based on whether a stopline or only a stop sign has been detected and publishes a distance to it. While the vehicle is not stopped at the virtual stopline nor has entered the intersection, int_app_to_stop is published as the current behaviour. This is used inside motion_planning to calculate a stopping velocity. -A green light is approached with 8 m/s. +A green light is approached with 30 km/h. ## Wait @@ -38,7 +38,7 @@ If the light is green or when there isn't a traffic light returns SUCCESS otherw ## Enter -Handles driving through the intersection. +Handles driving through the intersection. Uses 50 km/h as target speed. Returns SUCCESS once the next waypoint is not this intersection anymore. From 4a8178396b24be04a493049da09a7efddfef1bd9 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:57:30 +0100 Subject: [PATCH 50/55] Update intersection.py Improved comments and logging messages, no logic changes --- .../behavior_agent/behaviours/intersection.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/code/planning/src/behavior_agent/behaviours/intersection.py b/code/planning/src/behavior_agent/behaviours/intersection.py index c19d3386..4ac8aad9 100755 --- a/code/planning/src/behavior_agent/behaviours/intersection.py +++ b/code/planning/src/behavior_agent/behaviours/intersection.py @@ -97,7 +97,9 @@ def update(self): - Set a feedback message - return a py_trees.common.Status.[RUNNING, SUCCESS, FAILURE] Gets the current traffic light status, stop sign status - and the stop line distance + and the stop line distance. Calcualtes a virtual stop line and + publishes a distance to it. Slows down car until virtual stop line + is reached when there is a red traffic light or a stop sign. :return: py_trees.common.Status.RUNNING, if too far from intersection py_trees.common.Status.SUCCESS, if stopped in front of inter- section or entered the intersection @@ -145,7 +147,8 @@ def update(self): or (self.stop_sign_detected and not self.traffic_light_detected) ): - rospy.loginfo("slowing down!") + rospy.loginfo("Intersection Approach: slowing down! Stop sign: + {self.stop_sign_detected}, Light: {self.traffic_light_status}") self.curr_behavior_pub.publish(bs.int_app_to_stop.name) # approach slowly when traffic light is green as traffic lights are @@ -164,14 +167,14 @@ def update(self): self.traffic_light_distance > 150 ): # too far - print("still approaching") + rospy.loginfo("Intersection still approaching") return py_trees.common.Status.RUNNING elif speed < convert_to_ms(2.0) and ( (self.virtual_stopline_distance < target_distance) or (self.traffic_light_distance < 150) ): # stopped - print("stopped") + print("Intersection Approach: stopped") return py_trees.common.Status.SUCCESS elif ( speed > convert_to_ms(5.0) @@ -180,6 +183,7 @@ def update(self): ): # drive through intersection even if traffic light turns yellow + rospy.loginfo("Intersection Approach Light is green") return py_trees.common.Status.SUCCESS elif speed > convert_to_ms(5.0) and self.virtual_stopline_distance < 3.5: # running over line @@ -189,7 +193,7 @@ def update(self): self.virtual_stopline_distance < target_distance and not self.stopline_detected ): - rospy.loginfo("Leave intersection!") + rospy.loginfo("Intersection Approach: Leave intersection!") return py_trees.common.Status.SUCCESS else: return py_trees.common.Status.RUNNING @@ -276,7 +280,7 @@ def update(self): """ light_status_msg = self.blackboard.get("/paf/hero/Center/traffic_light_state") - # ADD FEATURE: Check if intersection is clear + # TODO: ADD FEATURE Check if intersection is clear lidar_data = None intersection_clear = True if lidar_data is not None: @@ -292,7 +296,7 @@ def update(self): # Wait at traffic light self.red_light_flag = True self.green_light_time = rospy.get_rostime() - rospy.loginfo(f"Light Status: {traffic_light_status}") + rospy.loginfo(f"Intersection Wait Light Status: {traffic_light_status}") self.curr_behavior_pub.publish(bs.int_wait.name) return py_trees.common.Status.RUNNING elif ( @@ -300,7 +304,7 @@ def update(self): and traffic_light_status == "green" ): # Wait approx 1s for confirmation - rospy.loginfo("Confirm green light!") + rospy.loginfo("Intersection Wait Confirm green light!") return py_trees.common.Status.RUNNING elif self.red_light_flag and traffic_light_status != "green": rospy.loginfo(f"Light Status: {traffic_light_status}" "-> prev was red") @@ -310,7 +314,7 @@ def update(self): rospy.get_rostime() - self.green_light_time > rospy.Duration(1) and traffic_light_status == "green" ): - rospy.loginfo(f"Light Status: {traffic_light_status}") + rospy.loginfo(f"Driving through Intersection Light Status: {traffic_light_status}") # Drive through intersection return py_trees.common.Status.SUCCESS else: From 0adbfc2d46ba98279045a276f8678278261fc5bd Mon Sep 17 00:00:00 2001 From: seefelke Date: Thu, 12 Dec 2024 23:43:44 +0100 Subject: [PATCH 51/55] small logging fix --- .../src/behavior_agent/behaviours/intersection.py | 12 ++++++++---- .../src/behavior_agent/behaviours/overtake.py | 12 +++++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/code/planning/src/behavior_agent/behaviours/intersection.py b/code/planning/src/behavior_agent/behaviours/intersection.py index 4ac8aad9..68954e09 100755 --- a/code/planning/src/behavior_agent/behaviours/intersection.py +++ b/code/planning/src/behavior_agent/behaviours/intersection.py @@ -147,8 +147,10 @@ def update(self): or (self.stop_sign_detected and not self.traffic_light_detected) ): - rospy.loginfo("Intersection Approach: slowing down! Stop sign: - {self.stop_sign_detected}, Light: {self.traffic_light_status}") + rospy.loginfo( + "Intersection Approach: slowing down! Stop sign: " + "{self.stop_sign_detected}, Light: {self.traffic_light_status}" + ) self.curr_behavior_pub.publish(bs.int_app_to_stop.name) # approach slowly when traffic light is green as traffic lights are @@ -174,7 +176,7 @@ def update(self): or (self.traffic_light_distance < 150) ): # stopped - print("Intersection Approach: stopped") + rospy.loginfo("Intersection Approach: stopped") return py_trees.common.Status.SUCCESS elif ( speed > convert_to_ms(5.0) @@ -314,7 +316,9 @@ def update(self): rospy.get_rostime() - self.green_light_time > rospy.Duration(1) and traffic_light_status == "green" ): - rospy.loginfo(f"Driving through Intersection Light Status: {traffic_light_status}") + rospy.loginfo( + f"Driving through Intersection Light Status: {traffic_light_status}" + ) # Drive through intersection return py_trees.common.Status.SUCCESS else: diff --git a/code/planning/src/behavior_agent/behaviours/overtake.py b/code/planning/src/behavior_agent/behaviours/overtake.py index 133e3c86..447b4d79 100644 --- a/code/planning/src/behavior_agent/behaviours/overtake.py +++ b/code/planning/src/behavior_agent/behaviours/overtake.py @@ -76,7 +76,8 @@ def update(self): - return a py_trees.common.Status.[RUNNING, SUCCESS, FAILURE] Gets the current distance to overtake, the current oncoming lane status and the - distance to collsion object. Slows down while oncoming blocked until stopped or oncoming clear. + distance to collsion object. Slows down while oncoming blocked until stopped + or oncoming clear. :return: py_trees.common.Status.RUNNING, if too far from overtaking py_trees.common.Status.SUCCESS, if stopped behind the blocking object or entered the process. @@ -123,7 +124,10 @@ def update(self): if self.ot_distance > 20.0: # too far - rospy.loginfo("Overtake Approach: still approaching obstacle, distance: {self.ot_distance}") + rospy.loginfo( + "Overtake Approach: still approaching obstacle, " + "distance: {self.ot_distance}" + ) return py_trees.common.Status.RUNNING elif speed < convert_to_ms(2.0) and self.ot_distance < TARGET_DISTANCE_TO_STOP: # stopped @@ -231,7 +235,9 @@ def update(self): self.curr_behavior_pub.publish(bs.ot_wait_free.name) return py_trees.common.Status.SUCCESS else: - rospy.loginfo(f"Overtake still blocked, distance to oncoming: {distance_oncoming}") + rospy.loginfo( + f"Overtake still blocked, distance to oncoming: {distance_oncoming}" + ) self.curr_behavior_pub.publish(bs.ot_wait_stopped.name) return py_trees.common.Status.RUNNING elif obstacle_msg.data[0] == np.inf: From 0d423a6e6e2d0e19b038f218c1c60496ee0a2f60 Mon Sep 17 00:00:00 2001 From: seefelke Date: Fri, 13 Dec 2024 00:08:06 +0100 Subject: [PATCH 52/55] some f string fixes --- .../src/behavior_agent/behaviours/intersection.py | 9 ++++++--- .../src/behavior_agent/behaviours/lane_change.py | 4 +++- code/planning/src/behavior_agent/behaviours/overtake.py | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/code/planning/src/behavior_agent/behaviours/intersection.py b/code/planning/src/behavior_agent/behaviours/intersection.py index 68954e09..6c4d4470 100755 --- a/code/planning/src/behavior_agent/behaviours/intersection.py +++ b/code/planning/src/behavior_agent/behaviours/intersection.py @@ -148,8 +148,8 @@ def update(self): ): rospy.loginfo( - "Intersection Approach: slowing down! Stop sign: " - "{self.stop_sign_detected}, Light: {self.traffic_light_status}" + f"Intersection Approach: slowing down! Stop sign: " + f"{self.stop_sign_detected}, Light: {self.traffic_light_status}" ) self.curr_behavior_pub.publish(bs.int_app_to_stop.name) @@ -185,7 +185,10 @@ def update(self): ): # drive through intersection even if traffic light turns yellow - rospy.loginfo("Intersection Approach Light is green") + rospy.loginfo( + f"Intersection Approach Light is green, light:" + f"{self.traffic_light_status}" + ) return py_trees.common.Status.SUCCESS elif speed > convert_to_ms(5.0) and self.virtual_stopline_distance < 3.5: # running over line diff --git a/code/planning/src/behavior_agent/behaviours/lane_change.py b/code/planning/src/behavior_agent/behaviours/lane_change.py index 87e18cb9..c6605308 100755 --- a/code/planning/src/behavior_agent/behaviours/lane_change.py +++ b/code/planning/src/behavior_agent/behaviours/lane_change.py @@ -115,7 +115,9 @@ def update(self): return py_trees.common.Status.RUNNING if self.virtual_change_distance > target_dis and self.blocked: # too far - rospy.loginfo("Lane Change: still approaching") + rospy.loginfo( + f"Lane Change: still approaching, distance:{self.virtual_change_distance}" + ) self.curr_behavior_pub.publish(bs.lc_app_blocked.name) return py_trees.common.Status.RUNNING elif ( diff --git a/code/planning/src/behavior_agent/behaviours/overtake.py b/code/planning/src/behavior_agent/behaviours/overtake.py index 447b4d79..41ad24f4 100644 --- a/code/planning/src/behavior_agent/behaviours/overtake.py +++ b/code/planning/src/behavior_agent/behaviours/overtake.py @@ -125,8 +125,8 @@ def update(self): if self.ot_distance > 20.0: # too far rospy.loginfo( - "Overtake Approach: still approaching obstacle, " - "distance: {self.ot_distance}" + f"Overtake Approach: still approaching obstacle, " + f"distance: {self.ot_distance}" ) return py_trees.common.Status.RUNNING elif speed < convert_to_ms(2.0) and self.ot_distance < TARGET_DISTANCE_TO_STOP: From 80aa8bb8a0382f881af87184c355c889b029e350 Mon Sep 17 00:00:00 2001 From: seefelke <33551476+seefelke@users.noreply.github.com> Date: Fri, 13 Dec 2024 00:30:42 +0100 Subject: [PATCH 53/55] Rename Unstuck_Behavior.md to Unstuck.md --- doc/planning/behaviours/{Unstuck_Behavior.md => Unstuck.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/planning/behaviours/{Unstuck_Behavior.md => Unstuck.md} (100%) diff --git a/doc/planning/behaviours/Unstuck_Behavior.md b/doc/planning/behaviours/Unstuck.md similarity index 100% rename from doc/planning/behaviours/Unstuck_Behavior.md rename to doc/planning/behaviours/Unstuck.md From ec3b6f3fb1061850d4ed51e2bb9429468e600624 Mon Sep 17 00:00:00 2001 From: seefelke Date: Fri, 13 Dec 2024 15:24:38 +0100 Subject: [PATCH 54/55] linter --- doc/planning/behaviours/Intersection.md | 2 +- doc/planning/behaviours/LaneChange.md | 2 +- doc/planning/behaviours/LeaveParkingSpace.md | 2 +- doc/planning/behaviours/Overtake.md | 17 ++++++++--------- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/doc/planning/behaviours/Intersection.md b/doc/planning/behaviours/Intersection.md index 106a4a80..7d17dd90 100644 --- a/doc/planning/behaviours/Intersection.md +++ b/doc/planning/behaviours/Intersection.md @@ -9,7 +9,7 @@ - [Wait](#wait) - [Enter](#enter) - [Leave](#leave) - + ## General The Intersection behaviour is used to control the vehicle when it encounters a intersection. It handles stop signs as well as traffic lights. diff --git a/doc/planning/behaviours/LaneChange.md b/doc/planning/behaviours/LaneChange.md index f8440e9d..341b40a4 100644 --- a/doc/planning/behaviours/LaneChange.md +++ b/doc/planning/behaviours/LaneChange.md @@ -9,7 +9,7 @@ - [Wait](#wait) - [Enter](#enter) - [Leave](#leave) - + ## General This behaviour executes a lane change. It slows the vehicle down until the lane change point is reached and then proceeds to switch lanes. diff --git a/doc/planning/behaviours/LeaveParkingSpace.md b/doc/planning/behaviours/LeaveParkingSpace.md index 716ade84..51192638 100644 --- a/doc/planning/behaviours/LeaveParkingSpace.md +++ b/doc/planning/behaviours/LeaveParkingSpace.md @@ -4,7 +4,7 @@ - [Leave Parking Space Behavior](#leaveparkingspace-behavior) - [General](#general) - + ## General The leave parking space behaviour is only executed at the beginning of the simulation to leave the parking space. diff --git a/doc/planning/behaviours/Overtake.md b/doc/planning/behaviours/Overtake.md index a8df0dd0..3058995a 100644 --- a/doc/planning/behaviours/Overtake.md +++ b/doc/planning/behaviours/Overtake.md @@ -2,21 +2,20 @@ **Summary:** This file explains the Overtake behavior. -- [Overtake Behavior](#overtake-behavior) - - [General](#general) - - [Overtake Ahead](#overtake-ahead) - - [Approach](#approach) - - [Wait](#wait) - - [Enter](#enter) - - [Leave](#leave) - +- [General](#general) +- [Overtake ahead](#overtake-ahead) +- [Approach](#approach) +- [Wait](#wait) +- [Enter](#enter) +- [Leave](#leave) + ## General This behaviour is used to overtake an object in close proximity. This behaviour is currently not working and more like a initial prototype. ## Overtake ahead -Checks whether there is a object in front of the car that needs overtaking. +Checks whether there is a object in front of the car that needs overtaking. Estimates whether the car would collide with the object soon. If that is the case a counter gets incremented. When that counter reaches 4 SUCCESS is returned. If the object is not blocking the trajectory, FAILURE is returned. From 1f4c75373f4cd62507615f001e2c3585792dae8d Mon Sep 17 00:00:00 2001 From: seefelke Date: Fri, 13 Dec 2024 15:27:24 +0100 Subject: [PATCH 55/55] linter --- code/planning/src/behavior_agent/behaviours/lane_change.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/planning/src/behavior_agent/behaviours/lane_change.py b/code/planning/src/behavior_agent/behaviours/lane_change.py index c6605308..b6c67eae 100755 --- a/code/planning/src/behavior_agent/behaviours/lane_change.py +++ b/code/planning/src/behavior_agent/behaviours/lane_change.py @@ -116,7 +116,8 @@ def update(self): if self.virtual_change_distance > target_dis and self.blocked: # too far rospy.loginfo( - f"Lane Change: still approaching, distance:{self.virtual_change_distance}" + f"Lane Change: still approaching, " + f"distance:{self.virtual_change_distance}" ) self.curr_behavior_pub.publish(bs.lc_app_blocked.name) return py_trees.common.Status.RUNNING