diff --git a/leo_example_follow_aruco_marker/CMakeLists.txt b/leo_example_follow_aruco_marker/CMakeLists.txt new file mode 100644 index 0000000..dcf0279 --- /dev/null +++ b/leo_example_follow_aruco_marker/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.5) +project(leo_example_follow_aruco_marker) + +# Default to C++17 +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) +endif() + +find_package(ament_cmake REQUIRED) +find_package(ament_cmake_python REQUIRED) +find_package(generate_parameter_library REQUIRED) + +generate_parameter_module(follower_parameters + leo_follow_aruco/follower_parameters.yaml +) + +install(DIRECTORY + config + launch + DESTINATION share/${PROJECT_NAME}/ +) + +ament_python_install_package(leo_follow_aruco) + +# Install Python executables +install(PROGRAMS + scripts/aruco_follower + DESTINATION lib/${PROJECT_NAME} +) + +ament_package() \ No newline at end of file diff --git a/leo_example_follow_aruco_marker/config/follower.yaml b/leo_example_follow_aruco_marker/config/follower.yaml new file mode 100644 index 0000000..1bad714 --- /dev/null +++ b/leo_example_follow_aruco_marker/config/follower.yaml @@ -0,0 +1,19 @@ +/**: + ros__parameters: + follow_id: 0 + marker_timeout: 1.0 + + min_ang_vel: 0.1 + max_ang_vel: 1.0 + angle_min: 0.1 + angle_max: 0.7 + + min_lin_vel_forward: 0.1 + max_lin_vel_forward: 0.4 + distance_min_forward: 0.7 + distance_max_forward: 1.2 + + min_lin_vel_reverse: 0.1 + max_lin_vel_reverse: 0.2 + distance_min_reverse: 0.4 + distance_max_reverse: 0.5 \ No newline at end of file diff --git a/leo_example_follow_aruco_marker/config/tracker.yaml b/leo_example_follow_aruco_marker/config/tracker.yaml new file mode 100644 index 0000000..2d838f2 --- /dev/null +++ b/leo_example_follow_aruco_marker/config/tracker.yaml @@ -0,0 +1,14 @@ +/**: + ros__parameters: + cam_base_topic: camera/image_color + output_frame: 'base_link' + + marker_dict: 4X4_50 + + publish_tf: true + marker_size: 0.15 + + aruco: + adaptiveThreshWinSizeMin: 3 + adaptiveThreshWinSizeMax: 23 + adaptiveThreshWinSizeStep: 10 diff --git a/leo_example_follow_aruco_marker/launch/follow_aruco_marker.launch.xml b/leo_example_follow_aruco_marker/launch/follow_aruco_marker.launch.xml new file mode 100644 index 0000000..78c693d --- /dev/null +++ b/leo_example_follow_aruco_marker/launch/follow_aruco_marker.launch.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/leo_example_follow_aruco_marker/leo_follow_aruco/__init__.py b/leo_example_follow_aruco_marker/leo_follow_aruco/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leo_example_follow_aruco_marker/leo_follow_aruco/aruco_follower_node.py b/leo_example_follow_aruco_marker/leo_follow_aruco/aruco_follower_node.py new file mode 100644 index 0000000..461663a --- /dev/null +++ b/leo_example_follow_aruco_marker/leo_follow_aruco/aruco_follower_node.py @@ -0,0 +1,195 @@ +import math +from typing import Optional + +from rclpy.duration import Duration +from rclpy.node import Node +from rclpy.publisher import Publisher +from rclpy.subscription import Subscription +from rclpy.time import Time +from rclpy.timer import Timer + +from geometry_msgs.msg import Twist, Point, Pose +from nav_msgs.msg import Odometry + +from aruco_opencv_msgs.msg import ArucoDetection, MarkerPose + +from leo_example_follow_aruco_marker.follower_parameters import follower_parameters +from leo_follow_aruco.utils import pose_delta, translate + + +class ArucoMarkerFollower(Node): + """A ROS node following specified Aruco Marker.""" + + running: bool = False + marker_angle: float = 0.0 + marker_distance: float = 0.0 + last_marker_position: Optional[Point] = None + twist_cmd: Twist = Twist() + last_odom_ts: Optional[Time] = None + odom_marker_pose: Pose = Pose() + odom_current_pose: Pose = Pose() + + def __init__(self) -> None: + super().__init__("aruco_follower") + + self.param_listener = follower_parameters.ParamListener(self) + self.params = self.param_listener.get_params() + self.param_listener.set_user_callback(self.reconfigure_callback) + + self.last_marker_ts: Time = self.get_clock().now() - Duration( + seconds=self.params.marker_timeout + 1.0 + ) + + self.cmd_vel_pub: Publisher = self.create_publisher(Twist, "cmd_vel", 1) + + self.marker_pose_sub: Subscription = self.create_subscription( + ArucoDetection, + "aruco_detections", + self.marker_callback, + 1, + ) + + self.merged_odom_sub: Subscription = self.create_subscription( + Odometry, "merged_odom", self.odom_callback, 1 + ) + + self.run_timer: Timer = self.create_timer(0.1, self.run) + + def reconfigure_callback(self, parameters: follower_parameters.Params) -> None: + """Callback for the parameters change. + Updates the values of parameters. + + Args: + parameters (follower_parameters.Params): The struct with current parameters. + """ + self.params = parameters + + def run(self) -> None: + """Function controlling the rover. Sends velocity command based on + marker position and odometry calculations. + """ + if not self.params.follow_enabled: + return + + self.update_vel_cmd() + self.cmd_vel_pub.publish(self.twist_cmd) + + def update_vel_cmd(self) -> None: + """Updates current velocity command based on recent marker detections and odometry data.""" + # Check for a timeout + if ( + self.last_marker_ts + Duration(seconds=self.params.marker_timeout) + < self.get_clock().now() + ): + self.twist_cmd.linear.x = 0.0 + self.twist_cmd.angular.z = 0.0 + return + + # Get the absolute angle to the marker + angle = math.fabs(self.marker_angle) + + # Get the direction multiplier + dir = -1.0 if self.marker_angle < 0.0 else 1.0 + + # Calculate angular command + if angle < self.params.angle_min: + ang_cmd = 0.0 + else: + ang_cmd = translate( + angle, + self.params.angle_min, + self.params.angle_max, + self.params.min_ang_vel, + self.params.max_ang_vel, + ) + + # Calculate linear command + if self.marker_distance >= self.params.distance_min_forward: + lin_cmd = translate( + self.marker_distance, + self.params.distance_min_forward, + self.params.distance_max_forward, + self.params.min_lin_vel_forward, + self.params.max_lin_vel_forward, + ) + elif self.marker_distance <= self.params.distance_max_reverse: + lin_cmd = -translate( + self.marker_distance, + self.params.distance_min_reverse, + self.params.distance_max_reverse, + self.params.max_lin_vel_reverse, + self.params.min_lin_vel_reverse, + ) + else: + lin_cmd = 0.0 + + self.twist_cmd.angular.z = dir * ang_cmd + self.twist_cmd.linear.x = lin_cmd + + def update_marker_angle_and_distance(self) -> None: + """Updates current distance and angle from the rover to the marker.""" + if self.last_marker_position: + current_pos_x, current_pos_y, current_yaw = pose_delta( + self.odom_marker_pose, self.odom_current_pose + ) + + position_x: float = self.last_marker_position.x - current_pos_x + position_y: float = self.last_marker_position.y - current_pos_y + + self.marker_angle = math.atan(position_y / position_x) - current_yaw + self.marker_distance = math.sqrt(position_x**2 + position_y**2) + + def marker_callback(self, msg: ArucoDetection) -> None: + """Callback for marker detection subscriber. Searches for the right marker + and updates the calculation needed variables. + + Args: + msg (ArucoDetection): Message received on the topic. + """ + marker: MarkerPose + for marker in msg.markers: + if marker.marker_id != self.params.follow_id: + continue + if Time.from_msg(msg.header.stamp) < self.last_marker_ts: + self.get_logger().warning( + "Got marker position with an older timestamp.", + throttle_duration_sec=3.0, + ) + continue + + self.last_marker_ts = Time.from_msg(msg.header.stamp) + self.last_marker_position = marker.pose.position + self.odom_marker_pose = self.odom_current_pose + + self.update_marker_angle_and_distance() + return + + def odom_callback(self, msg: Odometry) -> None: + """Callback for odometry subscriber. Updates stored poses needed for + distance and angle calculations. + + Args: + msg (Odometry): Message received on the topic. + """ + if self.last_odom_ts: + start_ts: Time = max(self.last_odom_ts, self.last_marker_ts) + + end_ts: Time = Time.from_msg(msg.header.stamp) + if end_ts < start_ts: + self.get_logger().warning( + "Reveived odometry has timestamp older than last marker position." + ) + + self.odom_current_pose = msg.pose + + self.update_marker_angle_and_distance() + + self.last_odom_ts = Time.from_msg(msg.header.stamp) + + def cleanup(self) -> None: + """Cleans ROS entities.""" + self.params.follow_enabled = False + self.destroy_timer(self.run_timer) + self.destroy_subscription(self.marker_pose_sub) + self.destroy_subscription(self.merged_odom_sub) + self.destroy_publisher(self.cmd_vel_pub) diff --git a/leo_example_follow_aruco_marker/leo_follow_aruco/follower_parameters.yaml b/leo_example_follow_aruco_marker/leo_follow_aruco/follower_parameters.yaml new file mode 100644 index 0000000..3bdcc20 --- /dev/null +++ b/leo_example_follow_aruco_marker/leo_follow_aruco/follower_parameters.yaml @@ -0,0 +1,89 @@ +follower_parameters: + follow_id: + type: int + default_value: 0 + description: "The ID of the marker to follow." + validation: + gt_eq<>: [0] + marker_timeout: + type: double + default_value: 1.0 + description: "Timeout for the marker detection (in seconds). When the specified time elapses with no detection the robot stops." + validation: + bounds<>: [0.1, 5.0] + min_ang_vel: + type: double + default_value: 0.1 + description: "Minimal angular velocity (in radians per second) the rover is going to rotate with." + validation: + bounds<>: [0.1, 1.0] + max_ang_vel: + type: double + default_value: 1.0 + description: "Maximal angular velocity (in radians per second) the rover is going to rotate with." + validation: + bounds<>: [1.0, 3.0] + angle_min: + type: double + default_value: 0.1 + description: "Minimal angle (in radians) to the marker, for the rover to start rotating." + validation: + bounds<>: [0.1, 1.0] + angle_max: + type: double + default_value: 0.7 + description: "Angle (in radians) to the marker at which rover will rotate with max speed." + validation: + bounds<>: [0.1, 1.0] + min_lin_vel_forward: + type: double + default_value: 0.1 + description: "Minimal linear velocity (in meters per second) the rover is going to move with driving forward." + validation: + bounds<>: [0.1, 0.5] + max_lin_vel_forward: + type: double + default_value: 0.4 + description: "Maximal linear velocity (in meters per second) the rover is going to move with driving forward." + validation: + bounds<>: [0.1, 0.5] + distance_min_forward: + type: double + default_value: 0.7 + description: "Minimal distance (in meters) to the marker, for the rover to start moving forward." + validation: + bounds<>: [0.5, 1.0] + distance_max_forward: + type: double + default_value: 1.2 + description: "Distance (in meters) to the marker, at which the rover will move forward with max speed." + validation: + bounds<>: [1.0, 2.0] + min_lin_vel_reverse: + type: double + default_value: 0.1 + description: "Minimal linear velocity (in meters per second) the rover is going to move with driving backward." + validation: + bounds<>: [0.1, 0.5] + max_lin_vel_reverse: + type: double + default_value: 0.2 + description: "Maximal linear velocity (in meters per second) the rover is going to move with driving backward." + validation: + bounds<>: [0.1, 0.5] + distance_min_reverse: + type: double + default_value: 0.4 + description: "Distance (in meters) to the marker, at which the rover will move backward with max speed." + validation: + bounds<>: [0.1, 1.0] + distance_max_reverse: + type: double + default_value: 0.5 + description: "Minimal distance (in meters) to the marker, for the rover to start moving backward." + validation: + bounds<>: [0.1, 2.0] + follow_enabled: + type: bool + default_value: true + description: "Whether to follow the marker or not." \ No newline at end of file diff --git a/leo_example_follow_aruco_marker/leo_follow_aruco/utils.py b/leo_example_follow_aruco_marker/leo_follow_aruco/utils.py new file mode 100644 index 0000000..e5d26d0 --- /dev/null +++ b/leo_example_follow_aruco_marker/leo_follow_aruco/utils.py @@ -0,0 +1,66 @@ +import math + +from geometry_msgs.msg import Pose +from tf_transformations import euler_from_quaternion + + +def translate( + value: float, leftMin: float, leftMax: float, rightMin: float, rightMax: float +) -> float: + """Proportionally projects given value from one interval onto antoher. + + Args: + value (float): Value to be projected. + leftMin (float): Minimal value from the source interval. + leftMax (float): Maximal value from the source interval. + rightMin (float): Minimal value from the target interval. + rightMax (float): Maximal value from the target interbal. + + Returns: + float: Proportionally projected value in the target interval. + """ + value = min(max(value, leftMin), leftMax) + + # Figure out how 'wide' each interval is. + leftSpan = leftMax - leftMin + rightSpan = rightMax - rightMin + + # Get the proportional placement of the value in the source interval. + valueScaled = float(value - leftMin) / float(leftSpan) + + # Cast the proportion on the target interval. + return rightMin + (valueScaled * rightSpan) + + +def pose_delta(pose1: Pose, pose2: Pose) -> tuple[float, float, float]: + """Calculates movement in linear x,y axes and yaw rotation between two odometry poses. + + Args: + pose1 (Pose): Old pose. + pose2 (Pose): Current pose. + + Returns: + tuple[float, float, float]: Movement in x axis, y axis, and yaw rotation. + """ + dx = pose2.position.x - pose1.position.x + dy = pose2.position.y - pose1.position.y + + q1 = [ + pose1.orientation.x, + pose1.orientation.y, + pose1.orientation.z, + pose1.orientation.w, + ] + q2 = [ + pose2.orientation.x, + pose2.orientation.y, + pose2.orientation.z, + pose2.orientation.w, + ] + + _, _, yaw1 = euler_from_quaternion(q1) + _, _, yaw2 = euler_from_quaternion(q2) + dyaw = yaw2 - yaw1 + dyaw = math.atan2(math.sin(dyaw), math.cos(dyaw)) + + return dx, dy, dyaw diff --git a/leo_example_follow_aruco_marker/package.xml b/leo_example_follow_aruco_marker/package.xml new file mode 100644 index 0000000..81b3353 --- /dev/null +++ b/leo_example_follow_aruco_marker/package.xml @@ -0,0 +1,34 @@ + + + + leo_example_follow_aruco_marker + 1.0.0 + + Follow Aruco Marker Example for Leo Rover. + + + Fictionlab + + MIT + + https://github.com/LeoRover/leo_examples-ros2/issues + https://github.com/LeoRover/leo_examples-ros2 + + Aleksander SzymaƄski + + ament_cmake + ament_cmake_python + + generate_parameter_library + + aruco_opencv + aruco_opencv_msgs + geometry_msgs + nav_msgs + rclpy + tf_transformations + + + ament_cmake + + \ No newline at end of file diff --git a/leo_example_follow_aruco_marker/scripts/aruco_follower b/leo_example_follow_aruco_marker/scripts/aruco_follower new file mode 100755 index 0000000..fb0beeb --- /dev/null +++ b/leo_example_follow_aruco_marker/scripts/aruco_follower @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +import rclpy + +from leo_follow_aruco.aruco_follower_node import ArucoMarkerFollower + +rclpy.init() + +node = ArucoMarkerFollower() + +try: + rclpy.spin(node) +except KeyboardInterrupt as e: + node.cleanup() + node.destroy_node() + rclpy.try_shutdown()