Search, Flight Tracker, and Config Updates
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
# Single source of truth for all mission parameters.
|
# Single source of truth for all mission parameters.
|
||||||
|
|
||||||
# ── Flight ───────────────────────────────────────────────────
|
# ── Flight ───────────────────────────────────────────────────
|
||||||
altitude: 4.0 # Search altitude (meters)
|
altitude: 4.0 # Search altitude (feet)
|
||||||
|
|
||||||
# ── ArUco Marker ─────────────────────────────────────────────
|
# ── ArUco Marker ─────────────────────────────────────────────
|
||||||
marker:
|
marker:
|
||||||
@@ -13,18 +13,14 @@ marker:
|
|||||||
|
|
||||||
# ── Search Patterns ──────────────────────────────────────────
|
# ── Search Patterns ──────────────────────────────────────────
|
||||||
spiral:
|
spiral:
|
||||||
max_legs: 12
|
max_legs: 60
|
||||||
initial_leg: 4.0
|
|
||||||
leg_increment: 3.2
|
|
||||||
|
|
||||||
lawnmower:
|
lawnmower:
|
||||||
width: 30.0
|
width: 30.0
|
||||||
height: 30.0
|
height: 30.0
|
||||||
lane_spacing: 3.2
|
|
||||||
lanes: 2
|
|
||||||
|
|
||||||
levy:
|
levy:
|
||||||
max_steps: 20
|
max_steps: 40
|
||||||
field_size: 50.0
|
field_size: 50.0
|
||||||
|
|
||||||
# ── Geofence ─────────────────────────────────────────────────
|
# ── Geofence ─────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -5,3 +5,7 @@ connection:
|
|||||||
sim: "tcp:127.0.0.1:5760"
|
sim: "tcp:127.0.0.1:5760"
|
||||||
real: "/dev/ttyAMA0"
|
real: "/dev/ttyAMA0"
|
||||||
baud: 57600
|
baud: 57600
|
||||||
|
|
||||||
|
speed:
|
||||||
|
min_mph: 0.2
|
||||||
|
max_mph: 1.0
|
||||||
|
|||||||
@@ -11,3 +11,7 @@ position:
|
|||||||
topics:
|
topics:
|
||||||
cmd_vel: /ugv/cmd_vel
|
cmd_vel: /ugv/cmd_vel
|
||||||
odometry: /ugv/odometry
|
odometry: /ugv/odometry
|
||||||
|
|
||||||
|
speed:
|
||||||
|
min_mph: 0.5
|
||||||
|
max_mph: 3.5
|
||||||
|
|||||||
@@ -16,13 +16,10 @@ class SearchMode(Enum):
|
|||||||
SPIRAL = "spiral"
|
SPIRAL = "spiral"
|
||||||
LAWNMOWER = "lawnmower"
|
LAWNMOWER = "lawnmower"
|
||||||
LEVY = "levy"
|
LEVY = "levy"
|
||||||
|
|
||||||
|
|
||||||
class Search:
|
class Search:
|
||||||
POSITION_TOLERANCE = 1.0
|
POSITION_TOLERANCE = 0.2
|
||||||
CHECK_INTERVAL = 0.5
|
CHECK_INTERVAL = 0.5
|
||||||
MAX_TRAVEL_TIME = 30.0
|
MAX_TRAVEL_TIME = 300.0
|
||||||
CAM_FOV_METERS = 4.0
|
|
||||||
|
|
||||||
def __init__(self, ctrl: Controller, detector: ObjectDetector,
|
def __init__(self, ctrl: Controller, detector: ObjectDetector,
|
||||||
camera=None, mode: str = "spiral", altitude: float = 5.0,
|
camera=None, mode: str = "spiral", altitude: float = 5.0,
|
||||||
@@ -44,14 +41,17 @@ class Search:
|
|||||||
self.on_target_found = None
|
self.on_target_found = None
|
||||||
self._dispatched_targets = set()
|
self._dispatched_targets = set()
|
||||||
|
|
||||||
self.spiral_max_legs = 12
|
hfov = 1.3962634 # 80 deg horizontal FOV
|
||||||
self.spiral_initial_leg = self.CAM_FOV_METERS
|
self.cam_fov_meters = 2.0 * self.altitude * math.tan(hfov / 2.0)
|
||||||
self.spiral_leg_increment = self.CAM_FOV_METERS * 0.8
|
|
||||||
|
self.spiral_max_legs = 40
|
||||||
|
self.spiral_initial_leg = self.cam_fov_meters
|
||||||
|
self.spiral_leg_increment = self.cam_fov_meters * 0.8
|
||||||
|
|
||||||
self.lawn_width = 30.0
|
self.lawn_width = 30.0
|
||||||
self.lawn_height = 30.0
|
self.lawn_height = 30.0
|
||||||
self.lawn_lane_spacing = self.CAM_FOV_METERS * 0.8
|
self.lawn_lane_spacing = self.cam_fov_meters * 0.8
|
||||||
self.lawn_lanes = 2
|
self.lawn_lanes = int(self.lawn_width / max(self.lawn_lane_spacing, 0.1)) + 1
|
||||||
|
|
||||||
self.levy_max_steps = 20
|
self.levy_max_steps = 20
|
||||||
self.levy_field_size = 50.0
|
self.levy_field_size = 50.0
|
||||||
@@ -159,12 +159,29 @@ class Search:
|
|||||||
return waypoints
|
return waypoints
|
||||||
|
|
||||||
def _clip_to_geofence(self, waypoints, polygon):
|
def _clip_to_geofence(self, waypoints, polygon):
|
||||||
|
warn_dist = getattr(self, 'geofence_warn_dist', 3.0)
|
||||||
|
safe_polygon = []
|
||||||
|
if warn_dist > 0:
|
||||||
|
cx = sum(p[0] for p in polygon) / len(polygon)
|
||||||
|
cy = sum(p[1] for p in polygon) / len(polygon)
|
||||||
|
for x, y in polygon:
|
||||||
|
dx = x - cx
|
||||||
|
dy = y - cy
|
||||||
|
length = math.sqrt(dx*dx + dy*dy)
|
||||||
|
if length > 0:
|
||||||
|
shrink = warn_dist / length
|
||||||
|
safe_polygon.append((x - dx * shrink, y - dy * shrink))
|
||||||
|
else:
|
||||||
|
safe_polygon.append((x, y))
|
||||||
|
else:
|
||||||
|
safe_polygon = polygon
|
||||||
|
|
||||||
clipped = []
|
clipped = []
|
||||||
for wx, wy in waypoints:
|
for wx, wy in waypoints:
|
||||||
if self._point_in_polygon(wx, wy, polygon):
|
if self._point_in_polygon(wx, wy, safe_polygon):
|
||||||
clipped.append((wx, wy))
|
clipped.append((wx, wy))
|
||||||
else:
|
else:
|
||||||
nearest = self._nearest_point_on_polygon(wx, wy, polygon)
|
nearest = self._nearest_point_on_polygon(wx, wy, safe_polygon)
|
||||||
if not clipped or distance_2d(clipped[-1], nearest) > 0.5:
|
if not clipped or distance_2d(clipped[-1], nearest) > 0.5:
|
||||||
clipped.append(nearest)
|
clipped.append(nearest)
|
||||||
return clipped
|
return clipped
|
||||||
@@ -324,10 +341,13 @@ class Search:
|
|||||||
IMG_CX, IMG_CY = IMG_W / 2, IMG_H / 2
|
IMG_CX, IMG_CY = IMG_W / 2, IMG_H / 2
|
||||||
HFOV = 1.3962634 # 80° horizontal FOV
|
HFOV = 1.3962634 # 80° horizontal FOV
|
||||||
|
|
||||||
DESCENT_STEP = 1.0
|
self.ctrl.update_state()
|
||||||
LAND_ALT = 1.5
|
current_alt = self.ctrl.altitude
|
||||||
|
|
||||||
|
DESCENT_STEP = max(0.5, current_alt * 0.25)
|
||||||
|
LAND_ALT = min(1.0, current_alt - 0.2)
|
||||||
CENTER_PX = 50
|
CENTER_PX = 50
|
||||||
MAX_CORRECTIONS = 30
|
MAX_CORRECTIONS = 200
|
||||||
MAX_LOST = 30
|
MAX_LOST = 30
|
||||||
GAIN = 0.35
|
GAIN = 0.35
|
||||||
|
|
||||||
@@ -365,12 +385,16 @@ class Search:
|
|||||||
|
|
||||||
def center_over_target(self, target_id):
|
def center_over_target(self, target_id):
|
||||||
print(f"\n[SEARCH] ===== CENTERING OVER TARGET {target_id} =====")
|
print(f"\n[SEARCH] ===== CENTERING OVER TARGET {target_id} =====")
|
||||||
|
if hasattr(self, 'uav_min_cms'):
|
||||||
|
print(f"[SEARCH] Reducing speed to minimum ({self.uav_min_cms} cm/s) for centering.")
|
||||||
|
self.ctrl.configure_speed_limits(wpnav_speed=self.uav_min_cms, loit_speed=self.uav_min_cms)
|
||||||
|
|
||||||
IMG_W, IMG_H = 640, 480
|
IMG_W, IMG_H = 640, 480
|
||||||
IMG_CX, IMG_CY = IMG_W / 2, IMG_H / 2
|
IMG_CX, IMG_CY = IMG_W / 2, IMG_H / 2
|
||||||
HFOV = 1.3962634 # 80° horizontal FOV
|
HFOV = 1.3962634 # 80° horizontal FOV
|
||||||
|
|
||||||
CENTER_PX = 30
|
CENTER_PX = 30
|
||||||
MAX_CORRECTIONS = 30
|
MAX_CORRECTIONS = 200
|
||||||
MAX_LOST = 30
|
MAX_LOST = 30
|
||||||
GAIN = 0.35
|
GAIN = 0.35
|
||||||
|
|
||||||
@@ -443,14 +467,21 @@ class Search:
|
|||||||
ground_w = 2.0 * alt * math.tan(hfov / 2.0)
|
ground_w = 2.0 * alt * math.tan(hfov / 2.0)
|
||||||
m_per_px = ground_w / img_w
|
m_per_px = ground_w / img_w
|
||||||
|
|
||||||
# Camera pitch-90° inverts image axes relative to NED
|
# Calculate movement in the UAV's body frame
|
||||||
correction_y = -err_x * m_per_px * gain
|
correction_forward = -err_y * m_per_px * gain
|
||||||
correction_x = -err_y * m_per_px * gain
|
correction_right = err_x * m_per_px * gain
|
||||||
|
|
||||||
|
self.ctrl.update_state()
|
||||||
|
yaw = self.ctrl.current_yaw
|
||||||
|
|
||||||
|
# Rotate body correction (Forward, Right) into world NED (North, East)
|
||||||
|
correction_n = correction_forward * math.cos(yaw) - correction_right * math.sin(yaw)
|
||||||
|
correction_e = correction_forward * math.sin(yaw) + correction_right * math.cos(yaw)
|
||||||
|
|
||||||
cur = self.ctrl.get_local_position()
|
cur = self.ctrl.get_local_position()
|
||||||
self.ctrl.move_local_ned(
|
self.ctrl.move_local_ned(
|
||||||
cur['x'] + correction_x,
|
cur['x'] + correction_n,
|
||||||
cur['y'] + correction_y,
|
cur['y'] + correction_e,
|
||||||
-target_alt)
|
-target_alt)
|
||||||
sleep(0.5)
|
sleep(0.5)
|
||||||
|
|
||||||
@@ -460,8 +491,14 @@ class Search:
|
|||||||
return centered
|
return centered
|
||||||
|
|
||||||
def wait_for_position(self, target_x, target_y, timeout=None):
|
def wait_for_position(self, target_x, target_y, timeout=None):
|
||||||
|
self.ctrl.update_state()
|
||||||
|
pos = self.ctrl.get_local_position()
|
||||||
|
dist_total = distance_2d((pos['x'], pos['y']), (target_x, target_y))
|
||||||
|
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
timeout = self.MAX_TRAVEL_TIME
|
# Dynamic timeout: 30s base time + 3 seconds per meter
|
||||||
|
timeout = 30.0 + (dist_total * 3.0)
|
||||||
|
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
while time.time() - t0 < timeout and self.running:
|
while time.time() - t0 < timeout and self.running:
|
||||||
self.ctrl.update_state()
|
self.ctrl.update_state()
|
||||||
|
|||||||
@@ -22,9 +22,12 @@ YAW_TOL = 0.15
|
|||||||
|
|
||||||
|
|
||||||
class UGVController:
|
class UGVController:
|
||||||
def __init__(self, cmd_topic="/ugv/cmd_vel", odom_topic="/ugv/odometry"):
|
def __init__(self, cmd_topic="/ugv/cmd_vel", odom_topic="/ugv/odometry",
|
||||||
|
min_speed=0.5, max_speed=1.0):
|
||||||
self.node = Node()
|
self.node = Node()
|
||||||
self.pub = self.node.advertise(cmd_topic, Twist)
|
self.pub = self.node.advertise(cmd_topic, Twist)
|
||||||
|
self.min_linear_vel = min_speed
|
||||||
|
self.max_linear_vel = max_speed
|
||||||
self.position = {'x': 0.0, 'y': 0.0, 'z': 0.0}
|
self.position = {'x': 0.0, 'y': 0.0, 'z': 0.0}
|
||||||
self.yaw = 0.0
|
self.yaw = 0.0
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
@@ -59,7 +62,7 @@ class UGVController:
|
|||||||
|
|
||||||
def send_cmd_vel(self, linear_x, angular_z):
|
def send_cmd_vel(self, linear_x, angular_z):
|
||||||
msg = Twist()
|
msg = Twist()
|
||||||
msg.linear.x = max(-MAX_LINEAR_VEL, min(MAX_LINEAR_VEL, linear_x))
|
msg.linear.x = max(-self.max_linear_vel, min(self.max_linear_vel, linear_x))
|
||||||
msg.angular.z = max(-MAX_ANGULAR_VEL, min(MAX_ANGULAR_VEL, angular_z))
|
msg.angular.z = max(-MAX_ANGULAR_VEL, min(MAX_ANGULAR_VEL, angular_z))
|
||||||
self.pub.publish(msg)
|
self.pub.publish(msg)
|
||||||
|
|
||||||
@@ -117,9 +120,9 @@ class UGVController:
|
|||||||
if abs(yaw_error) > YAW_TOL:
|
if abs(yaw_error) > YAW_TOL:
|
||||||
angular = max(-MAX_ANGULAR_VEL,
|
angular = max(-MAX_ANGULAR_VEL,
|
||||||
min(MAX_ANGULAR_VEL, yaw_error * 2.0))
|
min(MAX_ANGULAR_VEL, yaw_error * 2.0))
|
||||||
linear = 0.0 if abs(yaw_error) > 0.5 else 0.3
|
linear = 0.0 if abs(yaw_error) > 0.5 else self.min_linear_vel
|
||||||
else:
|
else:
|
||||||
linear = min(MAX_LINEAR_VEL, dist * 0.5)
|
linear = max(self.min_linear_vel, min(self.max_linear_vel, dist * 0.5))
|
||||||
angular = yaw_error * 1.0
|
angular = yaw_error * 1.0
|
||||||
|
|
||||||
self.send_cmd_vel(linear, angular)
|
self.send_cmd_vel(linear, angular)
|
||||||
|
|||||||
33
src/main.py
33
src/main.py
@@ -88,7 +88,10 @@ def main():
|
|||||||
search_cfg = load_config('search.yaml')
|
search_cfg = load_config('search.yaml')
|
||||||
ugv_cfg = load_config('ugv.yaml')
|
ugv_cfg = load_config('ugv.yaml')
|
||||||
|
|
||||||
altitude = args.altitude or search_cfg.get('altitude', 5.0)
|
from utils.convert import feet_to_meters, mph_to_ms
|
||||||
|
|
||||||
|
raw_altitude = args.altitude or search_cfg.get('altitude', 4.0)
|
||||||
|
altitude = feet_to_meters(raw_altitude)
|
||||||
search_mode = args.search
|
search_mode = args.search
|
||||||
|
|
||||||
marker_cfg = search_cfg.get('marker', {})
|
marker_cfg = search_cfg.get('marker', {})
|
||||||
@@ -100,6 +103,14 @@ def main():
|
|||||||
ugv_pos_cfg = ugv_cfg.get('position', {})
|
ugv_pos_cfg = ugv_cfg.get('position', {})
|
||||||
ugv_position = (ugv_pos_cfg.get('x', 0.0), ugv_pos_cfg.get('y', 0.0))
|
ugv_position = (ugv_pos_cfg.get('x', 0.0), ugv_pos_cfg.get('y', 0.0))
|
||||||
|
|
||||||
|
ugv_speed_cfg = ugv_cfg.get('speed', {})
|
||||||
|
ugv_min_mph = ugv_speed_cfg.get('min_mph', 0.5)
|
||||||
|
ugv_max_mph = ugv_speed_cfg.get('max_mph', 3.5)
|
||||||
|
|
||||||
|
uav_speed_cfg = uav_cfg.get('speed', {})
|
||||||
|
uav_min_mph = uav_speed_cfg.get('min_mph', 1.0)
|
||||||
|
uav_max_mph = uav_speed_cfg.get('max_mph', 15.0)
|
||||||
|
|
||||||
ugv_topics = ugv_cfg.get('topics', {})
|
ugv_topics = ugv_cfg.get('topics', {})
|
||||||
ugv_cmd_topic = ugv_topics.get('cmd_vel', '/ugv/cmd_vel')
|
ugv_cmd_topic = ugv_topics.get('cmd_vel', '/ugv/cmd_vel')
|
||||||
ugv_odom_topic = ugv_topics.get('odometry', '/ugv/odometry')
|
ugv_odom_topic = ugv_topics.get('odometry', '/ugv/odometry')
|
||||||
@@ -113,7 +124,11 @@ def main():
|
|||||||
|
|
||||||
ctrl = Controller(conn_str)
|
ctrl = Controller(conn_str)
|
||||||
|
|
||||||
ugv = UGVController(cmd_topic=ugv_cmd_topic, odom_topic=ugv_odom_topic)
|
# Apply configuration speed bounds
|
||||||
|
ugv_min_ms = mph_to_ms(ugv_min_mph)
|
||||||
|
ugv_max_ms = mph_to_ms(ugv_max_mph)
|
||||||
|
ugv = UGVController(cmd_topic=ugv_cmd_topic, odom_topic=ugv_odom_topic,
|
||||||
|
min_speed=ugv_min_ms, max_speed=ugv_max_ms)
|
||||||
|
|
||||||
detector = ObjectDetector(
|
detector = ObjectDetector(
|
||||||
marker_size=marker_size,
|
marker_size=marker_size,
|
||||||
@@ -128,9 +143,8 @@ def main():
|
|||||||
|
|
||||||
def detection_overlay(camera_name, frame):
|
def detection_overlay(camera_name, frame):
|
||||||
detections = detector.detect(frame)
|
detections = detector.detect(frame)
|
||||||
if detections:
|
annotated = detector.annotate_frame(frame, detections)
|
||||||
annotated = detector.annotate_frame(frame, detections)
|
camera.frames[camera_name] = annotated
|
||||||
camera.frames[camera_name] = annotated
|
|
||||||
|
|
||||||
camera.register_callback("downward", detection_overlay)
|
camera.register_callback("downward", detection_overlay)
|
||||||
camera.register_callback("gimbal", detection_overlay)
|
camera.register_callback("gimbal", detection_overlay)
|
||||||
@@ -146,12 +160,15 @@ def main():
|
|||||||
if mode_cfg:
|
if mode_cfg:
|
||||||
search.configure(**{f"{search_mode}_{k}": v for k, v in mode_cfg.items()})
|
search.configure(**{f"{search_mode}_{k}": v for k, v in mode_cfg.items()})
|
||||||
|
|
||||||
|
search.uav_min_cms = int(mph_to_ms(uav_min_mph) * 100)
|
||||||
|
|
||||||
geofence_cfg = search_cfg.get('geofence', {})
|
geofence_cfg = search_cfg.get('geofence', {})
|
||||||
geofence_points = None
|
geofence_points = None
|
||||||
if geofence_cfg.get('enabled', False):
|
if geofence_cfg.get('enabled', False):
|
||||||
raw_pts = geofence_cfg.get('points', [])
|
raw_pts = geofence_cfg.get('points', [])
|
||||||
if raw_pts:
|
if raw_pts:
|
||||||
geofence_points = [tuple(p) for p in raw_pts]
|
geofence_points = [tuple(p) for p in raw_pts]
|
||||||
|
search.geofence_warn_dist = geofence_cfg.get('warning_distance', 3.0)
|
||||||
|
|
||||||
waypoints = search.plan(start_pos=(0.0, 0.0),
|
waypoints = search.plan(start_pos=(0.0, 0.0),
|
||||||
geofence_points=geofence_points)
|
geofence_points=geofence_points)
|
||||||
@@ -172,6 +189,12 @@ def main():
|
|||||||
tracker.start()
|
tracker.start()
|
||||||
|
|
||||||
ctrl.configure_ekf_fast_converge()
|
ctrl.configure_ekf_fast_converge()
|
||||||
|
|
||||||
|
# Configure AP speeds in cm/s from max_mph
|
||||||
|
uav_max_ms = mph_to_ms(uav_max_mph)
|
||||||
|
uav_max_cms = int(uav_max_ms * 100)
|
||||||
|
ctrl.configure_speed_limits(wpnav_speed=uav_max_cms, loit_speed=uav_max_cms)
|
||||||
|
|
||||||
setup_ardupilot(ctrl)
|
setup_ardupilot(ctrl)
|
||||||
if recorder:
|
if recorder:
|
||||||
recorder.set_phase("ekf_wait")
|
recorder.set_phase("ekf_wait")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class FlightTracker:
|
|||||||
UAV_COLOR = (0, 220, 100)
|
UAV_COLOR = (0, 220, 100)
|
||||||
UAV_TRAIL_COLOR = (0, 180, 80)
|
UAV_TRAIL_COLOR = (0, 180, 80)
|
||||||
UGV_COLOR = (60, 60, 255)
|
UGV_COLOR = (60, 60, 255)
|
||||||
|
UGV_TRAIL_COLOR = (40, 40, 200)
|
||||||
TEXT_COLOR = (200, 200, 200)
|
TEXT_COLOR = (200, 200, 200)
|
||||||
TEXT_DIM_COLOR = (120, 120, 120)
|
TEXT_DIM_COLOR = (120, 120, 120)
|
||||||
MARKER_COLOR = (180, 0, 220)
|
MARKER_COLOR = (180, 0, 220)
|
||||||
@@ -42,6 +43,7 @@ class FlightTracker:
|
|||||||
self.uav_alt = 0.0
|
self.uav_alt = 0.0
|
||||||
self.uav_heading = 0.0
|
self.uav_heading = 0.0
|
||||||
self.trail = deque()
|
self.trail = deque()
|
||||||
|
self.ugv_trail = deque()
|
||||||
self.markers_found = {}
|
self.markers_found = {}
|
||||||
|
|
||||||
self.plan_waypoints = []
|
self.plan_waypoints = []
|
||||||
@@ -81,6 +83,7 @@ class FlightTracker:
|
|||||||
def update_ugv(self, x, y):
|
def update_ugv(self, x, y):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.ugv_pos = (x, y)
|
self.ugv_pos = (x, y)
|
||||||
|
self.ugv_trail.append((x, y))
|
||||||
|
|
||||||
def add_marker(self, marker_id, x, y):
|
def add_marker(self, marker_id, x, y):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -136,6 +139,7 @@ class FlightTracker:
|
|||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
trail_copy = list(self.trail)
|
trail_copy = list(self.trail)
|
||||||
|
ugv_trail_copy = list(self.ugv_trail)
|
||||||
uav = self.uav_pos
|
uav = self.uav_pos
|
||||||
uav_alt = self.uav_alt
|
uav_alt = self.uav_alt
|
||||||
uav_hdg = self.uav_heading
|
uav_hdg = self.uav_heading
|
||||||
@@ -147,7 +151,9 @@ class FlightTracker:
|
|||||||
if plan_copy:
|
if plan_copy:
|
||||||
self._draw_plan(frame, plan_copy, active_wp)
|
self._draw_plan(frame, plan_copy, active_wp)
|
||||||
|
|
||||||
self._draw_trail(frame, trail_copy)
|
self._draw_trail(frame, trail_copy, self.UAV_TRAIL_COLOR)
|
||||||
|
if self.show_ugv:
|
||||||
|
self._draw_trail(frame, ugv_trail_copy, self.UGV_TRAIL_COLOR)
|
||||||
|
|
||||||
if self.show_ugv:
|
if self.show_ugv:
|
||||||
self._draw_ugv(frame, ugv)
|
self._draw_ugv(frame, ugv)
|
||||||
@@ -265,12 +271,12 @@ class FlightTracker:
|
|||||||
cv2.putText(frame, str(i), (px + 7, py - 5),
|
cv2.putText(frame, str(i), (px + 7, py - 5),
|
||||||
cv2.FONT_HERSHEY_SIMPLEX, 0.28, self.PLAN_COLOR, 1)
|
cv2.FONT_HERSHEY_SIMPLEX, 0.28, self.PLAN_COLOR, 1)
|
||||||
|
|
||||||
def _draw_trail(self, frame, trail):
|
def _draw_trail(self, frame, trail, color):
|
||||||
if len(trail) < 2:
|
if len(trail) < 2:
|
||||||
return
|
return
|
||||||
pts = [self.world_to_pixel(x, y) for x, y in trail]
|
pts = [self.world_to_pixel(x, y) for x, y in trail]
|
||||||
for i in range(1, len(pts)):
|
for i in range(1, len(pts)):
|
||||||
cv2.line(frame, pts[i - 1], pts[i], self.UAV_TRAIL_COLOR, 2)
|
cv2.line(frame, pts[i - 1], pts[i], color, 2)
|
||||||
|
|
||||||
def _draw_uav(self, frame, pos, heading):
|
def _draw_uav(self, frame, pos, heading):
|
||||||
px, py = self.world_to_pixel(pos[0], pos[1])
|
px, py = self.world_to_pixel(pos[0], pos[1])
|
||||||
@@ -350,8 +356,6 @@ class FlightTracker:
|
|||||||
else:
|
else:
|
||||||
cv2.putText(frame, "None", (x0, y), cv2.FONT_HERSHEY_SIMPLEX, 0.35, self.TEXT_DIM_COLOR, 1)
|
cv2.putText(frame, "None", (x0, y), cv2.FONT_HERSHEY_SIMPLEX, 0.35, self.TEXT_DIM_COLOR, 1)
|
||||||
y += 16
|
y += 16
|
||||||
y += 20
|
|
||||||
cv2.putText(frame, f"Trail: {len(trail)} pts", (x0, y), cv2.FONT_HERSHEY_SIMPLEX, 0.35, self.TEXT_DIM_COLOR, 1)
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|||||||
22
src/utils/convert.py
Normal file
22
src/utils/convert.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Unit conversions: Customary (US) to Metric.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def feet_to_meters(feet: float) -> float:
|
||||||
|
return feet * 0.3048
|
||||||
|
|
||||||
|
def inches_to_meters(inches: float) -> float:
|
||||||
|
return inches * 0.0254
|
||||||
|
|
||||||
|
def miles_to_meters(miles: float) -> float:
|
||||||
|
return miles * 1609.34
|
||||||
|
|
||||||
|
def pounds_to_kg(pounds: float) -> float:
|
||||||
|
return pounds * 0.453592
|
||||||
|
|
||||||
|
def ounces_to_kg(ounces: float) -> float:
|
||||||
|
return ounces * 0.0283495
|
||||||
|
|
||||||
|
def mph_to_ms(mph: float) -> float:
|
||||||
|
return mph * 0.44704
|
||||||
@@ -123,6 +123,13 @@ class ObjectDetector:
|
|||||||
|
|
||||||
def annotate_frame(self, frame, detections):
|
def annotate_frame(self, frame, detections):
|
||||||
annotated = frame.copy()
|
annotated = frame.copy()
|
||||||
|
|
||||||
|
# Draw center crosshair
|
||||||
|
h, w = frame.shape[:2]
|
||||||
|
cx, cy = w // 2, h // 2
|
||||||
|
cv2.line(annotated, (cx - 15, cy), (cx + 15, cy), (0, 255, 0), 2)
|
||||||
|
cv2.line(annotated, (cx, cy - 15), (cx, cy + 15), (0, 255, 0), 2)
|
||||||
|
|
||||||
if not detections:
|
if not detections:
|
||||||
return annotated
|
return annotated
|
||||||
overlay = annotated.copy()
|
overlay = annotated.copy()
|
||||||
|
|||||||
Reference in New Issue
Block a user