From 3e9dd59bd83df96c0e7c63016f19254659ac2746 Mon Sep 17 00:00:00 2001 From: Edwin Shepherd Date: Mon, 13 May 2024 17:37:46 +0100 Subject: [PATCH] clean up marker property generation --- robot/__init__.py | 3 +- robot/apriltags3.py | 4 +- robot/calibrate_camera.py | 71 ----------------- robot/marker_setup/__init__.py | 19 ----- robot/marker_setup/markers.py | 140 --------------------------------- robot/marker_setup/teams.py | 20 ----- robot/vision.py | 46 ++++++----- robot/wrapper.py | 20 ++--- setup.py | 2 +- 9 files changed, 43 insertions(+), 282 deletions(-) delete mode 100755 robot/calibrate_camera.py delete mode 100644 robot/marker_setup/__init__.py delete mode 100644 robot/marker_setup/markers.py delete mode 100644 robot/marker_setup/teams.py diff --git a/robot/__init__.py b/robot/__init__.py index 0782bb7..cf913f6 100644 --- a/robot/__init__.py +++ b/robot/__init__.py @@ -25,7 +25,7 @@ from robot.wrapper import Robot, NoCameraPresent from robot.greengiant import OUTPUT, INPUT, INPUT_ANALOG, INPUT_PULLUP, PWM_SERVO from robot.vision import RoboConUSBCamera -from robot.marker_setup import ( +from robot.game_config import ( MARKER, BASE_MARKER, ARENA_MARKER, @@ -34,6 +34,7 @@ TEAM ) + MINIUM_VERSION = (3, 6) if sys.version_info <= MINIUM_VERSION: raise ImportError( diff --git a/robot/apriltags3.py b/robot/apriltags3.py index dcec1f7..5d20c71 100755 --- a/robot/apriltags3.py +++ b/robot/apriltags3.py @@ -21,7 +21,7 @@ import numpy as np import scipy.spatial.transform as transform -from robot.marker_setup.markers import MARKER +from robot.game_config import MARKER ###################################################################### @@ -435,7 +435,7 @@ def detect(self, img, estimate_tag_pose=False, camera_params=None): if camera_params is None: raise ValueError( "camera_params must be provided to detect if estimate_tag_pose is set to True") - tag_size = MARKER.by_id(tag.id).size + tag_size = MARKER(tag.id).size camera_fx, camera_fy, camera_cx, camera_cy = [ c for c in camera_params] diff --git a/robot/calibrate_camera.py b/robot/calibrate_camera.py deleted file mode 100755 index 2d8c31c..0000000 --- a/robot/calibrate_camera.py +++ /dev/null @@ -1,71 +0,0 @@ -"""A script for getting the focal length luts for a camera -It losely follows the ideas of a PD controller combinded with a NM gradient -descent algo. -Usage: -import robot.calibrate_camera -""" -import robot -import math -import pprint - -TARGET = 3.0 -THRESHOLD = 0.01 -KP = 100 -KD = 5 -K_READING_COUNTS = 0.5 - - -def _get_reading(): - while True: - try: - return R.see()[0].dist - except IndexError: - print("saw nothing") - - -def _get_reading_number(error): - result = int(K_READING_COUNTS / error) - result = abs(result) - if result is 0: - result = 1 - elif result > 6: - result = 6 - return result - - -R = robot.Robot() -result = {} - -for res in R.camera.focal_lengths.copy(): - print("Checking res {}".format(res)) - R.camera.res = res - pprint.pprint(R.camera.focal_lengths) - - error = THRESHOLD + 1.0 - previous_error = error - while abs(error) > THRESHOLD: - value = R.camera.focal_lengths[res][0] - p = error * KP - d = (previous_error - error) * KD - value += p + d - - R.camera.focal_lengths[res] = (value, value) - R.camera._update_camera_params() - - reading_counts = get_reading_number(error) - - dists = [get_reading() for _ in range(reading_counts)] - average_dist = (sum(dists))/reading_counts - - previous_error = error - error = TARGET - average_dist - - print("Tried: {} got dist {} error: {}".format( - value, average_dist, error)) - print(" Max: {} min: {} range: {}".format( - max(dists), min(dists), max(dists) - min(dists))) - print(" P = {} reading_counts {}".format(error * KP, reading_counts)) - - result[res] = (value, value) - -pprint.pprint(result) diff --git a/robot/marker_setup/__init__.py b/robot/marker_setup/__init__.py deleted file mode 100644 index 8fa5340..0000000 --- a/robot/marker_setup/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -from .teams import TEAM -from .markers import ( - MARKER, - MARKER_TYPE, - ARENA_MARKER, - POTATO_MARKER, - BASE_MARKER, - POEM_ON_STARTUP, -) - -__all__ = ( - "TEAM", - "MARKER", - "POTATO_MARKER", - "MARKER_TYPE", - "BASE_MARKER", - "ARENA_MARKER", - "POEM_ON_STARTUP", -) diff --git a/robot/marker_setup/markers.py b/robot/marker_setup/markers.py deleted file mode 100644 index 397a10d..0000000 --- a/robot/marker_setup/markers.py +++ /dev/null @@ -1,140 +0,0 @@ -import enum -import typing - -from .teams import TEAM - -""" -Hiiii! -This file contains the definitions for the markers and the data we assign to them. -I have just set them to 2024's competition values, with MARKER_TYPE deciding whether a marker is part of -the ARENA walls, or a game object, which is this year (2023-2024) called a POTATO. So make the changes you -need! But make sure to change every reference to it in the code, not just the ones in this file. -So go nuts! Good luck with your Robocon, and feel free to add your own messages for the future below! -Byee! - - Holly (2023-2024) - -[Put your future messages here] -""" - -class POEM_ON_STARTUP: - jokes = [ - "Why did the potato cross the road? \ - He saw a fork up ahead.", - "What do you say to a baked potato that's angry? \ - Anything you like, just butter it up.", - "Funny Potato Joke", - "Farm Facts: There are around 5000 different varieties of potato", - "Farm Facts: Potatoes were first domesticated in Peru around 4500 years ago around Lake Titicaca", - "Farm Facts: The word potato originates from the Taino \"batata\", meaning sweet potato.", - "Farm Facts: China is the leading producer of potatoes, with 94.3 million tonnes produced in 2021", - "Farm Facts: The maximum theoretical voltage " - ] - - @staticmethod - def on_startup(logger, random): - """ - This is called on startup. Put something funny and relevant to this - years competition using the logger. Also random is currently passed - as an argument because I don't have the energy to try importing it, - I just spent quite a while struggling with the new brains. - """ - jokeNo = random.randint(0,len(POEM_ON_STARTUP.jokes)) - jokeToPrint = "I don't know what went wrong, but we messed up our joke loading ;-;" - try: - jokeToPrint = POEM_ON_STARTUP.jokes[jokeNo] - except: - jokeToPrint = POEM_ON_STARTUP.jokes[0] - logger.info(jokeToPrint) - -class MARKER_TYPE(enum.Enum): # Keep something like this to determine if a marker is a wall or not. - POTATO = enum.auto() - ARENA = enum.auto() - - -class BASE_MARKER: # Base marker class that POTATO_MARKER and ARENA_MARKER derive from. - team_marker_colors: dict = { # Colour definitions for each team defined in TEAMS - TEAM.RUSSET: (255, 64, 0), # RED - TEAM.SWEET: (255, 255, 32), # YELLOW - TEAM.MARIS_PIPER: (50,255,0), # GREEN - TEAM.PURPLE: (255, 32, 255), # PURPLE - } - - def __init__( - self, - id: int, - type: MARKER_TYPE, - ) -> None: - self.id = id - self.type = type - self.owning_team: typing.Union[TEAM, None] = None - - # Sizes are in meters - self.size = 0.2 if self.type == MARKER_TYPE.ARENA else 0.08 - - def __repr__(self) -> str: - return f"" - - @property - def bounding_box_color(self) -> tuple: - if self.type == MARKER_TYPE.ARENA: # If it is a wall - return tuple(reversed((125, 249, 225))) # Turquoise - elif self.owning_team==TEAM.ARENA: # If it is a Hot Potato (game object owned by ARENA) - return tuple(reversed((255,255,255))) # White - else: # If it is a Jacket Potato (game object owned by a team.) - return tuple(reversed(self.team_marker_colors[self.owning_team])) # Picks the team colour from above - -class ARENA_MARKER(BASE_MARKER): # Not much going on here. This represents a wall. - def __init__(self, id: int) -> None: - super().__init__(id, MARKER_TYPE.ARENA) - - def __repr__(self) -> str: - return f"" - - -class POTATO_MARKER(BASE_MARKER): # This is a game object rather than a wall. Add properties you want to keep track of - def __init__( - self, id: int, owner: TEAM - ) -> None: - super().__init__(id, MARKER_TYPE.POTATO) - self.owning_team = owner - - def __repr__(self) -> str: - return f"" - -class MARKER(BASE_MARKER): # This is literally just how the code gets the different marker types. - @staticmethod - def by_id(id: int, team: typing.Union[TEAM, None] = None) -> BASE_MARKER: # team is currently unused, but it is referenced throughout the code. It is the team of the robot I believe (check this) - """ - Get a marker object from an id - - Marker IDs are greater than 0, and usually won't go higher than 200. They are - read as April tags, so use an online 6x6 April tag generator to test values, - such as: - https://chaitanyantr.github.io/apriltag (remember to set to tag36h11 for this one) - - In 2024 low marker IDs (0-39) are potatoes. The first 4 potato markers are Jacket - Potatoes and belong to the team with the corresponding ID. - The rest of the low markers are unowned - their owning_team property is None - and their owner is ARENA. - - It is probably recommendable that you have duplicate marker IDs, so that any damaged - marker can be replaced by one with equivalent game meaning - in 2024 we made ID 0 and - ID 20 equivalent. - - Higher marker IDs (40+) will be part of the arena. These markers will be - spaced as in 2021-2022's competition (6 markers on each side of the Arena, - the first 50cm away from the wall and each subsequent marker 1m away from - there). In practice these markers start at 100. - """ - - ARENA_WALL_LOWER_ID = 40 - if id >= ARENA_WALL_LOWER_ID: - return ARENA_MARKER(id) - - wrappingId = id % 20 # Make sure that the ID range wraps after 20 values. - if wrappingId<4: # If it is a Jacket Potato (has a team) - owning_team = TEAM[f"T{wrappingId}"] # Set to the corresponding TEAM enum. - else: # If it is a Hot Potato (Has no team) - owning_team = TEAM["ARENA"] - - return POTATO_MARKER(id, owning_team) diff --git a/robot/marker_setup/teams.py b/robot/marker_setup/teams.py deleted file mode 100644 index 23e04bf..0000000 --- a/robot/marker_setup/teams.py +++ /dev/null @@ -1,20 +0,0 @@ -import enum - -""" -This defines each team, and a corresponding Tx value for the team (x is the team index). Make sure that -the value of the Tx matches its team, and is unique - this year I picked the latin names for the potato varieties -that the school will be competing as. -""" -class TEAM(enum.Enum): - RUSSET = "Solanum Tuberosum 'Ranger Russet'" # This matches T0, for example. - SWEET = "Ipomoea batatas" - MARIS_PIPER = "Solanum Tuberosum 'Maris Piper'" - PURPLE = "Solanum Tuberosum 'Vitolette'" - ARENA = "HOTTTTT!" - # There is no T value for ARENA, so there is no way that the assignment of team to a marker can accidentally assign ARENA if the logic goes wrong. - - T0 = "Solanum Tuberosum 'Ranger Russet'" - T1 = "Ipomoea batatas" - T2 = "Solanum Tuberosum 'Maris Piper'" - T3 = "Solanum Tuberosum 'Vitolette'" - diff --git a/robot/vision.py b/robot/vision.py index 8c3ab23..8848466 100755 --- a/robot/vision.py +++ b/robot/vision.py @@ -7,11 +7,13 @@ import threading import queue +from collections.abc import Iterable from datetime import datetime from typing import NamedTuple, Any -from robot.marker_setup.markers import MARKER -from .marker_setup import BASE_MARKER as MarkerInfo + +from robot.game_config import MARKER, WHITE +from .game_config import BASE_MARKER as MarkerInfo import cv2 import numpy as np @@ -78,15 +80,6 @@ class Capture(NamedTuple): _USB_IMAGES_PATH = "/media/RobotUSB/collect_images.txt" _USB_LOGS_PATH = "/media/RobotUSB/log_markers.txt" -# Colours are in the format BGR -PURPLE = (255, 0, 215) # Purple -ORANGE = (0, 128, 255) # Orange -YELLOW = (0, 255, 255) # Yellow -GREEN = (0, 255, 0) # Green -RED = (0, 0, 255) # Red -BLUE = (255, 0, 0) # Blue -WHITE = (255, 255, 255) # White - # MARKER_: Marker Data Types # MARKER_TYPE_: Marker Types # NOTE Data about each marker @@ -188,7 +181,8 @@ def __init__(self, start_res=None, focal_lengths=None): raise "Invalid resolution for camera." elif self.camera_model == 'imx219': # PI cam version 2.1 - # Warning: only full res and 1640x1232 are full image (scaled), everything else seems full-res and cropped, reducing FOV + # Warning: only full res and 1640x1232 are full image (scaled), + # everything else seems full-res and cropped, reducing FOV self.focal_lengths = (PI_2_1_CAMERA_FOCAL_LENGTHS if focal_lengths is None else focal_lengths) @@ -365,12 +359,14 @@ def _draw_bounding_box(self, frame, detections): """ polygon_is_closed = True for detection in detections: - marker_info = MARKER.by_id(detection.id, self.zone) + marker_info = MARKER(detection.id, self.zone) marker_info_colour = marker_info.bounding_box_color marker_code = detection.id - colour = (marker_info_colour - if marker_info_colour is not None - else DEFAULT_BOUNDING_BOX_COLOUR) + + # The reverse is because OpenCV expects BGR but we use RGB + colour = reversed(marker_info_colour + if marker_info_colour is not None + else DEFAULT_BOUNDING_BOX_COLOUR) # need to have this EXACT integer_corners syntax due to opencv bug # https://stackoverflow.com/questions/17241830/ @@ -471,7 +467,7 @@ def _generate_marker_properties(self, tags): detections = Detections() for tag in tags: - info = MARKER.by_id(int(tag.id), self.zone) + info = MARKER(int(tag.id), self.zone) detections.append(Marker(info, tag)) return detections @@ -484,12 +480,24 @@ def _send_to_post_process(self, capture, detections): except queue.Full: logging.warning("Skipping postprocessing as queue is full") - def detect_markers(self): + def _filter_markers(self, markers, look_for_type): + """Ducktype filtering of markers based on type or code or list of both""" + if look_for_type is not None: + if isinstance(look_for_type, Iterable): + markers = filter(lambda m: m.code in look_for_type + or m.type in look_for_type, markers) + else: + markers = filter(lambda m: m.code == look_for_type + or m.type == look_for_type, markers) + return markers + + def detect_markers(self, look_for=None): """Returns the markers the robot can see: - Gets a frame - Finds the markers - Appends RoboCon specific properties, e.g. token or arena - Sends off for post processing + - Filters and sorts the markers """ capture = self.camera.capture() @@ -500,5 +508,7 @@ def detect_markers(self): self._send_to_post_process(capture, detections) markers = self._generate_marker_properties(detections) + markers = self._filter_markers(markers, look_for) + markers = sorted(markers, key=lambda m: m.dist) return markers diff --git a/robot/wrapper.py b/robot/wrapper.py index e49b1ec..a380e34 100644 --- a/robot/wrapper.py +++ b/robot/wrapper.py @@ -20,9 +20,9 @@ class to their respecitve classes from robot import vision from robot.cytron import CytronBoard from robot.greengiant import GreenGiantInternal, GreenGiantGPIOPinList, GreenGiantMotors, _GG_SERVO_PWM_BASE, _GG_GPIO_PWM_BASE, _GG_GPIO_GPIO_BASE, _GG_SERVO_GPIO_BASE -from robot.marker_setup.teams import TEAM -from . import marker_setup -from robot.marker_setup import POEM_ON_STARTUP +from robot.game_config import TEAM +from . import game_config +from robot.game_config import POEM_ON_STARTUP _logger = logging.getLogger("robot") @@ -67,7 +67,7 @@ def __init__(self, start_enable_5v = True, ): - self.zone = marker_setup.TEAM.RUSSET + self.zone = game_config.TEAM.RUSSET self.mode = "competition" self._max_motor_voltage = max_motor_voltage @@ -169,7 +169,7 @@ def report_hardware_status(self): # print report of hardware _logger.info("------HARDWARE REPORT------") - #_logger.info("Time: %s", datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + #_logger.info("Time: %s", datetime.now().strftime('%Y-%m-%d %H:%M:%S')) # no RTC on new boards, perhaps use a "run number" increment instead? _logger.info("Patch Version: ") _logger.info(battery_str) @@ -199,7 +199,7 @@ def enable_motors(self): For the PiLow series the Motors have both a power control and a enable. Generally the Power should not be switched on and off, just the enable bits. The power may - be tripped in extreame circumstances. I guess that here we want to report any + be tripped in extreame circumstances. I guess that here we want to report any reason for the motors not working, which includes power and enable """ @@ -213,7 +213,7 @@ def enable_motors(self, on): """An nice alias for set_12v""" if self._version < 10: return self._green_giant.enable_motors(on) - + @property def enable_12v(self): return self._green_giant.get_12v_acc_power() @@ -225,7 +225,7 @@ def enable_12v(self, on): @property def enable_5v(self): return self._green_giant.get_5v_acc_power() - + @enable_5v.setter def enable_5v(self, on): self._green_giant.set_5v_acc_power(on) @@ -311,10 +311,10 @@ def wait_start(self): def set_user_led(self, val=True): self._green_giant.set_user_led(val) - def see(self) -> vision.Detections: + def see(self, look_for=None) -> vision.Detections: """Take a photo, detect markers in sene, attach RoboCon specific properties""" - return self._vision.detect_markers() + return self._vision.detect_markers(look_for=look_for) def __del__(self): """Frees hardware resources held by the vision object""" diff --git a/setup.py b/setup.py index cdbcfe8..7c93982 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="robot", version="2024.1", - packages=["robot", "robot.marker_setup"], + packages=["robot"], install_requires=[ ],