diff --git a/NaviGator/mission_control/navigator_launch/config/pcodar_vrx.yaml b/NaviGator/mission_control/navigator_launch/config/pcodar_vrx.yaml index 39c0d39fa..b9cfb71e1 100644 --- a/NaviGator/mission_control/navigator_launch/config/pcodar_vrx.yaml +++ b/NaviGator/mission_control/navigator_launch/config/pcodar_vrx.yaml @@ -12,8 +12,8 @@ cluster_min_points: 5 cluster_max_points: 1000 # Associator -associator_max_distance: 20.0 -associator_forget_unseen: true +associator_max_distance: 2.0 +associator_forget_unseen: false # Ogrid ogrid_height_meters: 500 diff --git a/NaviGator/mission_control/navigator_missions/navigator_missions/__init__.py b/NaviGator/mission_control/navigator_missions/navigator_missions/__init__.py index 45b0133b3..429f1706d 100644 --- a/NaviGator/mission_control/navigator_missions/navigator_missions/__init__.py +++ b/NaviGator/mission_control/navigator_missions/navigator_missions/__init__.py @@ -25,6 +25,7 @@ from .killed import Killed from .move import Move from .navigation import Navigation +from .navigation_gatefinder import NavigationGatefinder from .navigator import NaviGatorMission from .obstacle_avoid import ObstacleAvoid from .pinger import PingerMission diff --git a/NaviGator/mission_control/navigator_missions/navigator_missions/navigation_gatefinder.py b/NaviGator/mission_control/navigator_missions/navigator_missions/navigation_gatefinder.py new file mode 100644 index 000000000..aea2603c2 --- /dev/null +++ b/NaviGator/mission_control/navigator_missions/navigator_missions/navigation_gatefinder.py @@ -0,0 +1,394 @@ +from __future__ import annotations + +import math +from dataclasses import dataclass +from enum import Enum, auto +from typing import ClassVar + +import axros +import numpy as np +import rospy +from geometry_msgs.msg import Pose +from mil_msgs.msg import PerceptionObject +from std_srvs.srv import SetBoolRequest +from tf.transformations import quaternion_from_euler + +from .navigator import NaviGatorMission + +FAILURE_THRESHOLD = 2000 + + +class BuoyColor(Enum): + RED = auto() + GREEN = auto() + WHITE = auto() + UNKNOWN = auto() + + @classmethod + def from_label(cls, label: str): + if "red" in label.lower(): + return cls.RED + if "green" in label.lower(): + return cls.GREEN + if "white" in label.lower(): + return cls.WHITE + return cls.UNKNOWN + + +@dataclass +class Buoy: + color: BuoyColor + pose: Pose + id: int + + @classmethod + def from_perception_object(cls, perception_object: PerceptionObject): + return cls( + color=BuoyColor.from_label(perception_object.labeled_classification), + pose=perception_object.pose, + id=perception_object.id, + ) + + def xyz(self): + return np.array( + [self.pose.position.x, self.pose.position.y, self.pose.position.z], + ) + + def distance(self, pose: Pose) -> float: + return math.sqrt( + ((pose.position.x - self.pose.position.x) ** 2) + + ((pose.position.y - self.pose.position.y) ** 2), + ) + + +@dataclass +class Gate: + left_buoy: Buoy + right_buoy: Buoy + traversed: bool = False + required_left_color: ClassVar[BuoyColor] + required_right_color: ClassVar[BuoyColor] + + @property + def mid_pose(self): + # Calculate the midpoint of the found buoys + mid_x = (self.left_buoy.pose.position.x + self.right_buoy.pose.position.x) / 2 + mid_y = (self.left_buoy.pose.position.y + self.right_buoy.pose.position.y) / 2 + slope = (self.left_buoy.pose.position.x - self.right_buoy.pose.position.x) / ( + self.left_buoy.pose.position.y - self.right_buoy.pose.position.y + ) + angle = math.atan(slope) + math.pi / 2 + pose = Pose() + pose.position.x = mid_x + pose.position.y = mid_y + quat = quaternion_from_euler(0, 0, angle) + pose.orientation.x = quat[0] + pose.orientation.y = quat[1] + pose.orientation.z = quat[2] + pose.orientation.w = quat[3] + return (mid_x, mid_y, 0), quat + + @classmethod + def other_color(cls, color_one: BuoyColor) -> BuoyColor: + if color_one == cls.required_left_color: + return cls.required_right_color + if color_one == cls.required_right_color: + return cls.required_left_color + raise RuntimeError(f"Color {color_one} cannot form this gate.") + + +@dataclass +class MiddleGate(Gate): + required_left_color = BuoyColor.RED + required_right_color = BuoyColor.GREEN + + +@dataclass +class StartGate(Gate): + required_left_color = BuoyColor.RED + required_right_color = BuoyColor.GREEN + + +@dataclass +class EndGate(Gate): + required_left_color = BuoyColor.RED + required_right_color = BuoyColor.GREEN + + +class GateClassifier: + def __init__(self, mission: NavigationGatefinder): + self.mission = mission + self.trajectory = [] + self.ids_invesitigated = set() + self.failures_to_move = 0 + self.midpoint_lengths = [] + + def _nearby_buoys(self, buoy: Buoy) -> list[Buoy]: + """ + Returns a list of nearby buoys, sorted by distance. + """ + nearby = sorted(self.ordered_buoys, key=lambda b: b.distance(buoy.pose)) + return [n for n in nearby if n.distance(buoy.pose) < 20] + + def mark_completed(self, gate: Gate) -> None: + pass + + async def get_next_gate(self, boat_pose) -> Gate | int: + # Get all database objects + objects: list[PerceptionObject] = ( + await self.mission.database_query(name="all") + ).objects + + # get nearest red and nearest green buoys + nearest_red: None | Buoy = None + nearest_green: None | Buoy = None + for db_object in objects: + # Could we do this more efficiently with a different database request? + DB_BUOY = Buoy.from_perception_object(db_object) + if ( + db_object.labeled_classification == "red_cylinder" + and DB_BUOY.id not in self.ids_invesitigated + ): + if nearest_red: + distance = np.linalg.norm(DB_BUOY.xyz() - boat_pose) + nearest_red_distance = np.linalg.norm(nearest_red.xyz() - boat_pose) + nearest_red = ( + DB_BUOY if distance < nearest_red_distance else nearest_red + ) + else: + nearest_red = DB_BUOY + + if ( + db_object.labeled_classification == "green_cylinder" + and DB_BUOY.id not in self.ids_invesitigated + ): + if nearest_green: + distance = np.linalg.norm(DB_BUOY.xyz() - boat_pose) + nearest_green_distance = np.linalg.norm( + nearest_green.xyz() - boat_pose, + ) + nearest_green = ( + DB_BUOY if distance < nearest_green_distance else nearest_green + ) + else: + nearest_green = DB_BUOY + + # calculate midpoint between both points + # if ONLY green then go to a point 5 meters from the right of the green buoy + # if ONLY red then go to a point 5 meters from the left of the red buoy + + # mark those buoys as investigated + mid_point = None + if nearest_green and nearest_red: + self.ids_invesitigated.add(nearest_red.id) + self.ids_invesitigated.add(nearest_green.id) + mid_x = (nearest_green.pose.position.x + nearest_red.pose.position.x) / 2 + mid_y = (nearest_green.pose.position.y + nearest_red.pose.position.y) / 2 + mid_point = (mid_x, mid_y, 0) + self.trajectory.append(mid_point) + + # missing red + elif nearest_green: + rospy.logerr( + f"Could not find red for green({nearest_green.id}), here is the boats pos:\n{boat_pose}", + ) + # Get nearest red to the green + objects: list[PerceptionObject] = ( + await self.mission.database_query(name="all") + ).objects + nearest_red = None + for db_object in objects: + # Could we do this more efficiently with a different database request? + rospy.logerr(db_object.labeled_classification) + DB_BUOY = Buoy.from_perception_object(db_object) + if ( + db_object.labeled_classification == "red_cylinder" + and DB_BUOY.id not in self.ids_invesitigated + ): + if nearest_red: + distance = np.linalg.norm(DB_BUOY.xyz() - nearest_green.xyz()) + nearest_red_distance = np.linalg.norm( + nearest_red.xyz() - nearest_green.xyz(), + ) + nearest_red = ( + DB_BUOY if distance < nearest_red_distance else nearest_red + ) + else: + nearest_red = DB_BUOY + # Set midpoint if nearest red was found + if nearest_red: + mid_x = ( + nearest_green.pose.position.x + nearest_red.pose.position.x + ) / 2 + mid_y = ( + nearest_green.pose.position.y + nearest_red.pose.position.y + ) / 2 + mid_point = (mid_x, mid_y, 0) + self.trajectory.append(mid_point) + + # missing green + elif nearest_red: + rospy.logerr( + f"Could not find green for red({nearest_red.id}), here is the boats pos:\n{boat_pose}", + ) + # Get nearest green to the red + objects: list[PerceptionObject] = ( + await self.mission.database_query(name="all") + ).objects + nearest_green = None + for db_object in objects: + DB_BUOY = Buoy.from_perception_object(db_object) + if ( + db_object.labeled_classification == "green_cylinder" + and DB_BUOY.id not in self.ids_invesitigated + ): + if nearest_green: + distance = np.linalg.norm(DB_BUOY.xyz() - nearest_red.xyz()) + nearest_green_distance = np.linalg.norm( + nearest_green.xyz() - nearest_red.xyz(), + ) + nearest_green = ( + DB_BUOY + if distance < nearest_green_distance + else nearest_green + ) + else: + nearest_green = DB_BUOY + # Set midpoint if nearest red was found + if nearest_green: + mid_x = ( + nearest_green.pose.position.x + nearest_red.pose.position.x + ) / 2 + mid_y = ( + nearest_green.pose.position.y + nearest_red.pose.position.y + ) / 2 + mid_point = (mid_x, mid_y, 0) + self.trajectory.append(mid_point) + + rospy.logerr(mid_point) + + # return next move point + if mid_point and nearest_green and nearest_red: + self.failures_to_move = 0 + rospy.logerr(f"nearest red: {nearest_red.id}") + rospy.logerr(f"nearest green: {nearest_green.id}") + self.trajectory.append(mid_point) + self.midpoint_lengths.append( + math.sqrt( + (nearest_green.pose.position.x - mid_point[0]) ** 2 + + (nearest_green.pose.position.y - mid_point[1]) ** 2, + ), + ) + return Gate(nearest_red, nearest_green) + + if self.failures_to_move > FAILURE_THRESHOLD: + rospy.logerr("FAILURE THRESHOLD REACHED") + return -1 + # from sklearn.preprocessing import PolynomialFeatures + # from sklearn.linear_model import LinearRegression + # import matplotlib.pyplot as plt + + # points=np.array(self.trajectory) + # x = points[:, 0].reshape(-1, 1) # Reshape for sklearn + # y = points[:, 1] + # # Follow the trajectory until you reach the farthest cylinder that is within the + # poly = PolynomialFeatures(degree=2) # You can change the degree as needed + # x_poly = poly.fit_transform(x) + + # # Fit the polynomial regression model + # model = LinearRegression() + # model.fit(x_poly, y) + # x_pred = np.linspace(min(x), max(x), 100) + + # # Predict using the transformed features + # y_pred = model.predict(poly.transform(x_pred)) + + # # Plot the results + # plt.scatter(x, y, color='blue', label='Original Points') + # plt.plot(x_pred, y_pred, color='red', label='Predicted Trajectory') + # plt.xlabel('X Position') + # plt.ylabel('Y Position') + # plt.title('Robot Trajectory Prediction (Polynomial)') + # plt.legend() + # plt.show() + + self.failures_to_move += 1 + return 0 + + async def available_gates(self) -> list[Gate]: + """ + Returns a list of the gates that still need to be traversed. + """ + gates: list[Gate] = [] + objects: list[PerceptionObject] = ( + await self.mission.database_query(name="all") + ).objects + found_buoys: set[Buoy] = set() + for db_object in objects: + # Could we do this more efficiently with a different database request? + if "buoy" not in db_object.labeled_classification: + continue + # Ignore circular buoys + if "circular" in db_object.labeled_classification: + continue + found_buoys.add(Buoy.from_perception_object(db_object)) + + # Order the buoys by distance (to form gates easier) + pose = self.mission.pose + assert pose is not None + self.ordered_buoys: list[Buoy] = sorted( + found_buoys, + key=lambda b: b.distance(pose), + ) + + # Turn the buoys into gates + handled_buoys: set[Buoy] = set() + print(f"found {len(found_buoys)}...") + for buoy in found_buoys: + # Ignore if already a part of another gate + if buoy in handled_buoys: + continue + # Find all nearby buoys that could potentially form a gates + nearby_buoys = self._nearby_buoys(buoy) + # Which gate should we be making? + intended_gate = MiddleGate + if len(gates) == 0: + intended_gate = StartGate + other_color = intended_gate.other_color(buoy.color) + nearby_colored_buoys = [b for b in nearby_buoys if b.color == other_color] + if len(nearby_colored_buoys) == 0: + raise RuntimeError( + "Can't find any nearby buoys matching the color needed for this gate!", + ) + closest_nearest_buoy = nearby_colored_buoys[0] + gates.append(intended_gate(buoy, closest_nearest_buoy)) + handled_buoys.add(buoy) + handled_buoys.add(closest_nearest_buoy) + return gates + + +class NavigationGatefinder(NaviGatorMission): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + async def run(self, parameters): + await self.change_wrench("autonomous") + await axros.util.wrap_time_notice( + self.set_classifier_enabled.wait_for_service(), + 4, + "classifier enable service", + ) + await self.set_classifier_enabled(SetBoolRequest(True)) + # start_pose = await self.tx_pose() + classifier = GateClassifier(self) + + while True: + rospy.logerr(classifier.ids_invesitigated) + next_gate = await classifier.get_next_gate(self.pose[0]) + if next_gate is Gate: + await self.move.set_position(next_gate.mid_pose[0]).set_orientation( + next_gate.mid_pose[1], + ).go() + elif next_gate == -1: + rospy.loginfo("All Done!") + break diff --git a/NaviGator/simulation/navigator_gazebo/worlds/pathfinding/path1_clear.world b/NaviGator/simulation/navigator_gazebo/worlds/pathfinding/path1_clear.world index e6adad950..f48b90002 100644 --- a/NaviGator/simulation/navigator_gazebo/worlds/pathfinding/path1_clear.world +++ b/NaviGator/simulation/navigator_gazebo/worlds/pathfinding/path1_clear.world @@ -85,7 +85,6 @@ -33.722718 150.674031 0.0 10 10 - 300 wamv_external_pivot_joint @@ -208,16 +207,6 @@ - - buoy_start_r - model://mb_marker_buoy_white - -520.338 168.871 0 0 0 -1.44 - - - buoy_start_l - model://mb_marker_buoy_white - -531.2 176.364 0 0 0 -1.44 - buoy_green1 model://mb_marker_buoy_green @@ -278,15 +267,5 @@ model://mb_marker_buoy_red -500.408539 209.651611 0 0 0 -1.44 - - buoy_end_r - model://mb_marker_buoy_white - -490.633240 200.413437 0 0 0 -1.44 - - - buoy_end_l - model://mb_marker_buoy_white - -492.262939 209.520996 0 0 0 -1.44 - diff --git a/NaviGator/simulation/navigator_gazebo/worlds/pathfinding/path1_obs.world b/NaviGator/simulation/navigator_gazebo/worlds/pathfinding/path1_obs.world index e339607e3..3975b4e93 100644 --- a/NaviGator/simulation/navigator_gazebo/worlds/pathfinding/path1_obs.world +++ b/NaviGator/simulation/navigator_gazebo/worlds/pathfinding/path1_obs.world @@ -208,16 +208,6 @@ - - buoy_start_r - model://mb_marker_buoy_white - -520.338 168.871 0 0 0 -1.44 - - - buoy_start_l - model://mb_marker_buoy_white - -531.2 176.364 0 0 0 -1.44 - buoy_green1 model://mb_marker_buoy_green @@ -278,16 +268,6 @@ model://mb_marker_buoy_red -500.408539 209.651611 0 0 0 -1.44 - - buoy_end_r - model://mb_marker_buoy_white - -490.633240 200.413437 0 0 0 -1.44 - - - buoy_end_l - model://mb_marker_buoy_white - -492.262939 209.520996 0 0 0 -1.44 - buoy_black1 model://mb_round_buoy_black diff --git a/NaviGator/simulation/navigator_gazebo/worlds/pathfinding/path2_clear.world b/NaviGator/simulation/navigator_gazebo/worlds/pathfinding/path2_clear.world index 9aebd1fdb..37cfe0bb8 100644 --- a/NaviGator/simulation/navigator_gazebo/worlds/pathfinding/path2_clear.world +++ b/NaviGator/simulation/navigator_gazebo/worlds/pathfinding/path2_clear.world @@ -208,16 +208,6 @@ - - buoy_start_r - model://mb_marker_buoy_white - -520.338 168.871 0 0 0 -1.44 - - - buoy_start_l - model://mb_marker_buoy_white - -531.2 176.364 0 0 0 -1.44 - green1 model://mb_marker_buoy_green @@ -278,15 +268,5 @@ model://mb_marker_buoy_red -509.021057 211.978912 0 0 0 -1.44 - - buoy_end_r - model://mb_marker_buoy_white - -501.999146 201.727905 0 0 0 -1.44 - - - buoy_end_l - model://mb_marker_buoy_white - -499.310181 210.086487 0 0 0 -1.44 -