From 0a8c6f85e19c964bbe39234d4a5035dae896127c Mon Sep 17 00:00:00 2001 From: Connor Hallemann Date: Tue, 26 Oct 2021 17:49:31 -0700 Subject: [PATCH 01/45] Prelim Interop Testing --- flight/interop_test.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 flight/interop_test.py diff --git a/flight/interop_test.py b/flight/interop_test.py new file mode 100644 index 0000000..197a178 --- /dev/null +++ b/flight/interop_test.py @@ -0,0 +1,20 @@ +from interop.client.auvsi_suas.client import client +from interop.client.auvsi_suas.client.client import interop_api_pb2 + +client = client.AsyncClient(url='http://192.168.74.128:8000', username='testuser', password='testpass') + +odlc = interop_api_pb2.Odlc() +odlc.type = interop_api_pb2.Odlc.STANDARD +odlc.latitude = 38 +odlc.longitude = -76 +odlc.orientation = interop_api_pb2.Odlc.N +odlc.shape = interop_api_pb2.Odlc.SQUARE +odlc.shape_color = interop_api_pb2.Odlc.GREEN +odlc.alphanumeric = 'A' +odlc.alphanumeric_color = interop_api_pb2.Odlc.WHITE + +odlc = client.post_odlc(odlc) + +with open('interop/client/auvsi_suas/client/testdata/A.jpg', 'rb') as f: + image_data = f.read() + client.put_odlc_image(odlc.id, image_data) \ No newline at end of file From ad080ff790c422ab3abd88b218e5452362296a0d Mon Sep 17 00:00:00 2001 From: Connor Hallemann Date: Tue, 1 Mar 2022 17:57:44 -0800 Subject: [PATCH 02/45] JSON Parsing for Waypoints & St. Obstacles --- flight/json_parsing.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 flight/json_parsing.py diff --git a/flight/json_parsing.py b/flight/json_parsing.py new file mode 100644 index 0000000..f204bc7 --- /dev/null +++ b/flight/json_parsing.py @@ -0,0 +1,29 @@ +import json + + +def waypoint_parsing(filename: str): + f = open(filename, ) + data_set = json.load(f) + # print(data_set) + f.close() + + waypoint_Locs = [] + + for i in range(0, len(data_set["waypoints"])): + waypoint_Locs.append(data_set["waypoints"][i]) + + return waypoint_Locs + + +def stationary_obstacle_parsing(filename: str): + f = open(filename, ) + data_set = json.load(f) + # print(data_set) + f.close() + + stationary_Obs = [] + + for i in range(0, len(data_set["stationaryObstacles"])): + stationary_Obs.append(data_set["stationaryObstacles"][i]) + + return stationary_Obs From 48497188e0be0ea99807ff58521f54e6e92514c3 Mon Sep 17 00:00:00 2001 From: Connor Hallemann Date: Thu, 3 Mar 2022 17:26:59 -0800 Subject: [PATCH 03/45] Prelim Base Flight Structure --- flight/.idea/.gitignore | 3 + flight/.idea/flight.iml | 15 +++++ .../inspectionProfiles/profiles_settings.xml | 6 ++ flight/.idea/misc.xml | 4 ++ flight/.idea/modules.xml | 8 +++ flight/.idea/vcs.xml | 6 ++ flight/__init__.py | 11 ++++ flight/state_settings.py | 35 ++++++++++++ flight/states/.idea/.gitignore | 3 + .../inspectionProfiles/profiles_settings.xml | 6 ++ flight/states/.idea/misc.xml | 4 ++ flight/states/.idea/modules.xml | 8 +++ flight/states/.idea/states.iml | 8 +++ flight/states/.idea/vcs.xml | 6 ++ flight/states/main.py | 0 flight/states/pre_processing.py | 12 ++++ flight/states/state.py | 55 +++++++++++++++++++ flight/states/takeoff.py | 11 ++++ flight/states/waypoints.py | 10 ++++ flight/{ => util}/interop_test.py | 0 flight/{ => util}/json_parsing.py | 0 21 files changed, 211 insertions(+) create mode 100644 flight/.idea/.gitignore create mode 100644 flight/.idea/flight.iml create mode 100644 flight/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 flight/.idea/misc.xml create mode 100644 flight/.idea/modules.xml create mode 100644 flight/.idea/vcs.xml create mode 100644 flight/__init__.py create mode 100644 flight/state_settings.py create mode 100644 flight/states/.idea/.gitignore create mode 100644 flight/states/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 flight/states/.idea/misc.xml create mode 100644 flight/states/.idea/modules.xml create mode 100644 flight/states/.idea/states.iml create mode 100644 flight/states/.idea/vcs.xml create mode 100644 flight/states/main.py create mode 100644 flight/states/pre_processing.py create mode 100644 flight/states/state.py create mode 100644 flight/states/takeoff.py create mode 100644 flight/states/waypoints.py rename flight/{ => util}/interop_test.py (100%) rename flight/{ => util}/json_parsing.py (100%) diff --git a/flight/.idea/.gitignore b/flight/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/flight/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/flight/.idea/flight.iml b/flight/.idea/flight.iml new file mode 100644 index 0000000..484e44c --- /dev/null +++ b/flight/.idea/flight.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/flight/.idea/inspectionProfiles/profiles_settings.xml b/flight/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/flight/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/flight/.idea/misc.xml b/flight/.idea/misc.xml new file mode 100644 index 0000000..d1e22ec --- /dev/null +++ b/flight/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/flight/.idea/modules.xml b/flight/.idea/modules.xml new file mode 100644 index 0000000..8d274d3 --- /dev/null +++ b/flight/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/flight/.idea/vcs.xml b/flight/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/flight/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/flight/__init__.py b/flight/__init__.py new file mode 100644 index 0000000..144eb42 --- /dev/null +++ b/flight/__init__.py @@ -0,0 +1,11 @@ +"""Initialization file for state exporting""" +from states.state import State +from states.pre_processing import PreProcess +from states.takeoff import Takeoff +from states.waypoints import Waypoints + +STATES = { + "Pre-Processing": PreProcess, + "Takeoff": Takeoff, + "Waypoints": Waypoints +} diff --git a/flight/state_settings.py b/flight/state_settings.py new file mode 100644 index 0000000..e45ee24 --- /dev/null +++ b/flight/state_settings.py @@ -0,0 +1,35 @@ +"""Class to contain setters and getters for settings in various flight states""" + +DEFAULT_WAYPOINTS: int = 14 +DEFAULT_RUN_TITLE: str = "N/A" +DEFAULT_RUN_DESCRIPTION: str = "N/A" + + +class StateSettings: + def __init__(self): + """Default constructor results in default settings""" + + # ---- Takeoff settings ---- # + + def enable_simple_takeoff(self, simple_takeoff: bool) -> None: + """ + Setter for whether to perform simple takeoff instead of regular takeoff + simple_takeoff(bool): True for drone to go straight up, False to behave normally + """ + self.simple_takeoff = simple_takeoff + + # ---- Waypoint Settings ---- # + + def set_number_waypoints(self, waypoints: int) -> None: + """Set the number of waypoints given by Interop System""" + self.num_waypoints = waypoints + + # ---- Other settings ---- # + + def set_run_title(self, title: str) -> None: + """Set a title for the run/test to be output in logging""" + self.run_title = title + + def set_run_description(self, description: str) -> None: + """Set a description for the run/test to be output in logging""" + self.run_description = description \ No newline at end of file diff --git a/flight/states/.idea/.gitignore b/flight/states/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/flight/states/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/flight/states/.idea/inspectionProfiles/profiles_settings.xml b/flight/states/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/flight/states/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/flight/states/.idea/misc.xml b/flight/states/.idea/misc.xml new file mode 100644 index 0000000..d1e22ec --- /dev/null +++ b/flight/states/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/flight/states/.idea/modules.xml b/flight/states/.idea/modules.xml new file mode 100644 index 0000000..0194d0c --- /dev/null +++ b/flight/states/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/flight/states/.idea/states.iml b/flight/states/.idea/states.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/flight/states/.idea/states.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/flight/states/.idea/vcs.xml b/flight/states/.idea/vcs.xml new file mode 100644 index 0000000..b2bdec2 --- /dev/null +++ b/flight/states/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/flight/states/main.py b/flight/states/main.py new file mode 100644 index 0000000..e69de29 diff --git a/flight/states/pre_processing.py b/flight/states/pre_processing.py new file mode 100644 index 0000000..ba87472 --- /dev/null +++ b/flight/states/pre_processing.py @@ -0,0 +1,12 @@ +"""State to receive data from interop system and perform preliminary calculations""" +import logging +from state import State +from ..state_settings import StateSettings +from takeoff import Takeoff + + +class PreProcess(State): + async def data_retrieval(self, drone): + """Prelim function to accept data""" + return Takeoff(self.state_settings) + diff --git a/flight/states/state.py b/flight/states/state.py new file mode 100644 index 0000000..234b79a --- /dev/null +++ b/flight/states/state.py @@ -0,0 +1,55 @@ +"""Base State class which all other states inherit""" +import logging +from mavsdk import System +from ..state_settings import StateSettings + + +class State: + """ + Base State class + Attributes: + drone (System): The drone object; used for flight. + """ + + def __init__(self, state_settings: StateSettings): + logging.info('State "%s" has begun', self.name) + self.state_settings = state_settings + + async def run(self, drone: System): + """ + Does all of the operations for the state + Parameters + ---------- + drone : System + The drone system; used for flight operations. + Returns + ------- + State or None + Either the next State in the machine or None if the machine stops + Raises + ------ + Exception + If this function hasn't been overloaded. + """ + raise Exception("Run not implemented for state") + + async def _check_arm_or_arm(self, drone: System): + """ + Verifies that the drone is armed, if not armed, arms the drone + Parameters + ---------- + drone : System + The drone system; used for flight operations. + """ + async for is_armed in drone.telemetry.armed(): + if not is_armed: + logging.debug("Not armed. Attempting to arm") + await drone.action.arm() + else: + logging.warning("Drone armed") + break + + @property + def name(self): + """Returns the name of the class""" + return type(self).__name__ \ No newline at end of file diff --git a/flight/states/takeoff.py b/flight/states/takeoff.py new file mode 100644 index 0000000..7ff1956 --- /dev/null +++ b/flight/states/takeoff.py @@ -0,0 +1,11 @@ +"""Run takeoff function to launch drone to minimum altitude""" +import logging +from state import State +from ..state_settings import StateSettings +from waypoints import Waypoints + + +class Takeoff(State): + async def run(self, drone): + """Run offboard functions to have the drone takeoff""" + return Waypoints(self.state_settings) diff --git a/flight/states/waypoints.py b/flight/states/waypoints.py new file mode 100644 index 0000000..b17c1ff --- /dev/null +++ b/flight/states/waypoints.py @@ -0,0 +1,10 @@ +"""Run flight plan assembled in pre-rpocessing to fly to all waypoints sequentially""" +import logging +from state import State +from ..state_settings import StateSettings + + +class Waypoints(State): + async def run(self, drone): + """Run waypoint path plan with obstacle avoidance running throughout""" + return diff --git a/flight/interop_test.py b/flight/util/interop_test.py similarity index 100% rename from flight/interop_test.py rename to flight/util/interop_test.py diff --git a/flight/json_parsing.py b/flight/util/json_parsing.py similarity index 100% rename from flight/json_parsing.py rename to flight/util/json_parsing.py From e2ad2f78772bad638cd145a0333c00f2efe98c91 Mon Sep 17 00:00:00 2001 From: Connor Hallemann Date: Wed, 9 Mar 2022 08:54:29 -0800 Subject: [PATCH 04/45] Flight Structure w/o Decorators --- flight/__init__.py | 11 +++++++++-- flight/state_settings.py | 19 ++++++++++++++++++- flight/states/final_state.py | 22 ++++++++++++++++++++++ flight/states/land.py | 24 ++++++++++++++++++++++++ flight/states/pre_processing.py | 18 ++++++++++++++++-- flight/states/start_state.py | 23 +++++++++++++++++++++++ flight/states/takeoff.py | 17 +++++++++++++++-- flight/states/waypoints.py | 23 +++++++++++++++++++---- 8 files changed, 146 insertions(+), 11 deletions(-) create mode 100644 flight/states/final_state.py create mode 100644 flight/states/land.py create mode 100644 flight/states/start_state.py diff --git a/flight/__init__.py b/flight/__init__.py index 144eb42..d5619ba 100644 --- a/flight/__init__.py +++ b/flight/__init__.py @@ -1,11 +1,18 @@ """Initialization file for state exporting""" +from typing import Dict from states.state import State +from states.start_state import Start from states.pre_processing import PreProcess from states.takeoff import Takeoff from states.waypoints import Waypoints - +from states.land import Land +from states.final_state import Final +STATES: Dict[str, State] STATES = { + "Start State": Start, "Pre-Processing": PreProcess, "Takeoff": Takeoff, - "Waypoints": Waypoints + "Waypoints": Waypoints, + "Land": Land, + "Final State": Final } diff --git a/flight/state_settings.py b/flight/state_settings.py index e45ee24..4df8dc0 100644 --- a/flight/state_settings.py +++ b/flight/state_settings.py @@ -6,8 +6,25 @@ class StateSettings: + """ + Initialize settings for state machine + Functions: + __init__: Sets preliminary values for SUAS overheads + enable_simple_takeoff() -> None: Sets if takeoff procedure should be simple for testing + set_number_waypoints() -> None: Sets the number of waypoints in flight plan + set_run_title() -> None: Sets name of competition + set_run_description() -> None: Sets description for competition + Member Variables: + num_waypoints: Number of waypoints on flight plan + run_title: Title of Competition + run_description: Description for Competition + """ def __init__(self): """Default constructor results in default settings""" + self.simple_takeoff = False + self.num_waypoints = DEFAULT_WAYPOINTS + self.run_title = DEFAULT_RUN_TITLE + self.run_description = DEFAULT_RUN_DESCRIPTION # ---- Takeoff settings ---- # @@ -32,4 +49,4 @@ def set_run_title(self, title: str) -> None: def set_run_description(self, description: str) -> None: """Set a description for the run/test to be output in logging""" - self.run_description = description \ No newline at end of file + self.run_description = description diff --git a/flight/states/final_state.py b/flight/states/final_state.py new file mode 100644 index 0000000..20b09a7 --- /dev/null +++ b/flight/states/final_state.py @@ -0,0 +1,22 @@ +"""Final state to end all drone processes""" +import logging +from mavsdk import System +from state import State +from ..state_settings import StateSettings + + +class Final(State): + """Empty state to end flight + Functions: + run() -> None: Ends the state machine + Member Variables: + None + """ + async def run(self, drone: System): + """Do nothing and end + Args: + drone: System - drone object that must be passed to any function with control of the drone + Returns: + None + """ + return diff --git a/flight/states/land.py b/flight/states/land.py new file mode 100644 index 0000000..0469db1 --- /dev/null +++ b/flight/states/land.py @@ -0,0 +1,24 @@ +"""Landing the drone at home position after each other task is finished""" +import logging +from mavsdk import System +from state import State +from ..state_settings import StateSettings +from states.final_state import Final + + +class Land(State): + """ + State to land the drone safely after finishing other flight & vision tasks + Functions: + run() -> Final: Running the landing procedure after returning to home + Member Variables: + None + """ + async def run(self, drone: System) -> Final: + """Run landing function to have drone slowly return to home position + Args: + drone: System - MAVSDK drone object for direct drone control + Returns: + Final: final state for ending state machine + """ + return Final(self.state_settings) diff --git a/flight/states/pre_processing.py b/flight/states/pre_processing.py index ba87472..25e6b9e 100644 --- a/flight/states/pre_processing.py +++ b/flight/states/pre_processing.py @@ -1,12 +1,26 @@ """State to receive data from interop system and perform preliminary calculations""" import logging +from mavsdk import System from state import State from ..state_settings import StateSettings from takeoff import Takeoff class PreProcess(State): - async def data_retrieval(self, drone): - """Prelim function to accept data""" + """ + State to accept data and plan out optimal mission structure based on relative distances + Functions: + data_retrieval() -> Takeoff: Connects to Interop and downloads JSON data from system + ... # Will add more functions as needed during dev + Member Variables: + None + """ + async def data_retrieval(self, drone: System) -> Takeoff: + """Prelim function to accept data + Args: + drone: System - MAVSDK drone object for direct drone control + Returns: + Takeoff: the next state, the Takeoff state to advance state machine + """ return Takeoff(self.state_settings) diff --git a/flight/states/start_state.py b/flight/states/start_state.py new file mode 100644 index 0000000..28794c1 --- /dev/null +++ b/flight/states/start_state.py @@ -0,0 +1,23 @@ +"""Beginning state of flight operation, proceed to pre-processing""" +import logging +from mavsdk import System +from state import State +from ..state_settings import StateSettings +from states.pre_processing import PreProcess + + +class Start(State): + """Preliminary state, proceed state machine into pre-processing + Functions: + begin() -> PreProcess: beginning function to start state machine, proceeding to pre-processing step + Member Variables: + None + """ + async def begin(self, drone: System) -> PreProcess: + """Initialization function to start flight state machine + Args: + drone: System - drone object for directv drone control + Returns: + PreProcess: data pre-processing state, to advance state machine + """ + return PreProcess(self.state_settings) diff --git a/flight/states/takeoff.py b/flight/states/takeoff.py index 7ff1956..78a2884 100644 --- a/flight/states/takeoff.py +++ b/flight/states/takeoff.py @@ -1,11 +1,24 @@ """Run takeoff function to launch drone to minimum altitude""" import logging +from mavsdk import System from state import State from ..state_settings import StateSettings from waypoints import Waypoints class Takeoff(State): - async def run(self, drone): - """Run offboard functions to have the drone takeoff""" + """ + Runs manual takeoff process, to lift drone to 100ft altitude + Functions: + run() -> Waypoints: runs drone movement functions to rise to 100ft + Member Variables: + None + """ + async def run(self, drone: System) -> Waypoints: + """Run offboard functions to have the drone takeoff + Args: + drone: System - MAVSDK drone object for direct drone control + Returns: + Waypoints: next state, Waypoints state to perform waypoint flight plan + """ return Waypoints(self.state_settings) diff --git a/flight/states/waypoints.py b/flight/states/waypoints.py index b17c1ff..2c94705 100644 --- a/flight/states/waypoints.py +++ b/flight/states/waypoints.py @@ -1,10 +1,25 @@ -"""Run flight plan assembled in pre-rpocessing to fly to all waypoints sequentially""" +"""Run flight plan assembled in pre-processing to fly to all waypoints sequentially""" import logging +from mavsdk import System from state import State from ..state_settings import StateSettings +from states.land import Land class Waypoints(State): - async def run(self, drone): - """Run waypoint path plan with obstacle avoidance running throughout""" - return + """ + State to fly to each waypoint in mission plan, using Obstacle Avoidance during flight + Functions: + run() -> Land: For each waypoint, flies to Latlon & avoids any obstacles + Member Variables: + None + """ + async def run(self, drone: System) -> Land: + """Run waypoint path plan with obstacle avoidance running throughout + Args: + drone: System - MAVSDK drone object for direct drone control + Returns: + Land: landing state to land drone after finishing tasks # This will change once other tasks become + testable within state machine + """ + return Land(self.state_settings) From c90eaa203f2a8dfe0eed467410aa39fe94ca75f2 Mon Sep 17 00:00:00 2001 From: Connor Hallemann Date: Wed, 9 Mar 2022 09:29:52 -0800 Subject: [PATCH 05/45] Settings w/ Decorators --- flight/__init__.py | 4 +- flight/state_settings.py | 129 ++++++++++++++++++++++++++++++--------- 2 files changed, 101 insertions(+), 32 deletions(-) diff --git a/flight/__init__.py b/flight/__init__.py index d5619ba..332d8e9 100644 --- a/flight/__init__.py +++ b/flight/__init__.py @@ -7,8 +7,8 @@ from states.waypoints import Waypoints from states.land import Land from states.final_state import Final -STATES: Dict[str, State] -STATES = { + +STATES: Dict[str, State] = { "Start State": Start, "Pre-Processing": PreProcess, "Takeoff": Takeoff, diff --git a/flight/state_settings.py b/flight/state_settings.py index 4df8dc0..2df11b1 100644 --- a/flight/state_settings.py +++ b/flight/state_settings.py @@ -1,8 +1,8 @@ """Class to contain setters and getters for settings in various flight states""" -DEFAULT_WAYPOINTS: int = 14 -DEFAULT_RUN_TITLE: str = "N/A" -DEFAULT_RUN_DESCRIPTION: str = "N/A" +DEFAULT_WAYPOINTS: int = 14 # Total number of waypoints flown to in Auto. flight of SUAS2022 +DEFAULT_RUN_TITLE: str = "N/A" # Title of flight mission for logging +DEFAULT_RUN_DESCRIPTION: str = "N/A" # Description of current flight mission for logging class StateSettings: @@ -10,43 +10,112 @@ class StateSettings: Initialize settings for state machine Functions: __init__: Sets preliminary values for SUAS overheads - enable_simple_takeoff() -> None: Sets if takeoff procedure should be simple for testing - set_number_waypoints() -> None: Sets the number of waypoints in flight plan - set_run_title() -> None: Sets name of competition - set_run_description() -> None: Sets description for competition + simple_takeoff() -> None: Enables access to status of simple_takeoff + -> bool: Setter to determine if testing takeoff procedure desired + num_waypoints() -> None: Enables access to number of waypoints in the competition + -> int: Sets the number of waypoints in the competition + run_title() -> None: Enables access to set name of the current flight + -> str: Sets the name of the current flight for logging + run_description() -> None: Enables access to description of current flight mission + -> str: Sets the description of current flight mission Member Variables: - num_waypoints: Number of waypoints on flight plan - run_title: Title of Competition - run_description: Description for Competition + __num_waypoints: Number of waypoints on flight plan + __run_title: Title of Competition + __run_description: Description for Competition """ - def __init__(self): - """Default constructor results in default settings""" - self.simple_takeoff = False - self.num_waypoints = DEFAULT_WAYPOINTS - self.run_title = DEFAULT_RUN_TITLE - self.run_description = DEFAULT_RUN_DESCRIPTION + def __init__(self, takeoff_bool: bool = False, num_waypoints: int = DEFAULT_WAYPOINTS, + title: str = DEFAULT_RUN_TITLE, description: str = DEFAULT_RUN_DESCRIPTION): + """Default constructor results in default settings + Args: + takeoff_bool: bool - determines if a simple takeoff procedure should be executed for testing + num_waypoints: int - Number of waypoints, extracted from SUAS mission plan + title: str - Title of current flight mission, for logging purposes + description: str - Description of current flight mission, for logging purposes + """ + self.__simple_takeoff = takeoff_bool + self.__num_waypoints = num_waypoints + self.__run_title = title + self.__run_description = description # ---- Takeoff settings ---- # - - def enable_simple_takeoff(self, simple_takeoff: bool) -> None: + @property + def simple_takeoff(self) -> bool: + """Establishes simple_takeoff as a private member variable + Args: + N/A + Returns: + bool: Status of simple_takeoff variable """ - Setter for whether to perform simple takeoff instead of regular takeoff - simple_takeoff(bool): True for drone to go straight up, False to behave normally + return self.__simple_takeoff + + @simple_takeoff.setter + def simple_takeoff(self, simple_takeoff: bool) -> None: + """Setter for whether to perform simple takeoff instead of regular takeoff + Args: + simple_takeoff: bool - True for drone to go straight up, False to behave normally + Returns: + None """ - self.simple_takeoff = simple_takeoff + self.__simple_takeoff = simple_takeoff # ---- Waypoint Settings ---- # + @property + def num_waypoints(self) -> int: + """Establishes num_waypoints as private member variable + Args: + N/A + Returns: + int: Number of waypoints in the competition + """ + return self.__num_waypoints - def set_number_waypoints(self, waypoints: int) -> None: - """Set the number of waypoints given by Interop System""" - self.num_waypoints = waypoints + @num_waypoints.setter + def num_waypoints(self, waypoints: int) -> None: + """Set the number of waypoints given by Interop System + Args: + waypoints: int - Number of waypoints present in mission plan + Returns: + None + """ + self.__num_waypoints = waypoints # ---- Other settings ---- # + @property + def run_title(self) -> str: + """Set a title for the run/test to be output in logging + Args: + N/A + Returns: + str: Created title for flight mission + """ + return self.__run_title - def set_run_title(self, title: str) -> None: - """Set a title for the run/test to be output in logging""" - self.run_title = title + @run_title.setter + def run_title(self, title: str) -> None: + """Sets the title of the flight mission for logging + Args: + title: str - Desired title for the flight + Returns: + None + """ + self.__run_title = title - def set_run_description(self, description: str) -> None: - """Set a description for the run/test to be output in logging""" - self.run_description = description + @property + def run_description(self) -> str: + """Set a description for the run/test to be output in logging + Args: + N/A + Returns: + str: Desired description for flight mission + """ + return self.__run_description + + @run_description.setter + def run_description(self, description: str) -> None: + """Sets a description of the flight mission + Args: + description: str - Written description for flight mission for logs + Returns: + None + """ + self.__run_description = description From 51e3348aa62f3bd32aa9543f66ccd42bd83339c7 Mon Sep 17 00:00:00 2001 From: Connor Hallemann Date: Tue, 15 Mar 2022 17:06:34 -0700 Subject: [PATCH 06/45] Test --- flight/state_settings.py | 12 ++++--- flight/states/final_state.py | 2 +- flight/states/pre_processing.py | 1 - flight/states/state.py | 61 ++++++++++++++++++--------------- flight/util/json_parsing.py | 18 ++++++---- 5 files changed, 54 insertions(+), 40 deletions(-) diff --git a/flight/state_settings.py b/flight/state_settings.py index 2df11b1..322dfdc 100644 --- a/flight/state_settings.py +++ b/flight/state_settings.py @@ -24,18 +24,20 @@ class StateSettings: __run_description: Description for Competition """ def __init__(self, takeoff_bool: bool = False, num_waypoints: int = DEFAULT_WAYPOINTS, - title: str = DEFAULT_RUN_TITLE, description: str = DEFAULT_RUN_DESCRIPTION): + title: str = DEFAULT_RUN_TITLE, description: str = DEFAULT_RUN_DESCRIPTION) -> None: """Default constructor results in default settings Args: takeoff_bool: bool - determines if a simple takeoff procedure should be executed for testing num_waypoints: int - Number of waypoints, extracted from SUAS mission plan title: str - Title of current flight mission, for logging purposes description: str - Description of current flight mission, for logging purposes + Returns: + None """ - self.__simple_takeoff = takeoff_bool - self.__num_waypoints = num_waypoints - self.__run_title = title - self.__run_description = description + self.__simple_takeoff: bool = takeoff_bool + self.__num_waypoints: int = num_waypoints + self.__run_title: str = title + self.__run_description: str = description # ---- Takeoff settings ---- # @property diff --git a/flight/states/final_state.py b/flight/states/final_state.py index 20b09a7..98781df 100644 --- a/flight/states/final_state.py +++ b/flight/states/final_state.py @@ -12,7 +12,7 @@ class Final(State): Member Variables: None """ - async def run(self, drone: System): + async def run(self, drone: System) -> None: """Do nothing and end Args: drone: System - drone object that must be passed to any function with control of the drone diff --git a/flight/states/pre_processing.py b/flight/states/pre_processing.py index 25e6b9e..980d915 100644 --- a/flight/states/pre_processing.py +++ b/flight/states/pre_processing.py @@ -23,4 +23,3 @@ async def data_retrieval(self, drone: System) -> Takeoff: Takeoff: the next state, the Takeoff state to advance state machine """ return Takeoff(self.state_settings) - diff --git a/flight/states/state.py b/flight/states/state.py index 234b79a..1a3fbe0 100644 --- a/flight/states/state.py +++ b/flight/states/state.py @@ -7,39 +7,41 @@ class State: """ Base State class - Attributes: + Functions: + run() -> (State, None): Runs the code present in each state and returns the next state, or None if at the + end of the machine + _check_arm_or_arm() -> None: Determines if drone is armed, and if not, arms it. + Member Variables: drone (System): The drone object; used for flight. """ - def __init__(self, state_settings: StateSettings): + def __init__(self, state_settings: StateSettings) -> None: + """ Initializes base State class using a StateSettings class + Args: + state_settings: StateSettings - class which contains basic competition data + Returns: + None + """ logging.info('State "%s" has begun', self.name) - self.state_settings = state_settings + self.state_settings: StateSettings = state_settings - async def run(self, drone: System): - """ - Does all of the operations for the state - Parameters - ---------- - drone : System - The drone system; used for flight operations. - Returns - ------- - State or None - Either the next State in the machine or None if the machine stops - Raises - ------ - Exception - If this function hasn't been overloaded. + async def run(self, drone: System) -> None: + """ Runs the functions and code present in state for competition goals + Args: + drone: System - drone object for MAVSDK control + Returns: + None + Exception: + General: raises if no code is present in run function """ raise Exception("Run not implemented for state") - async def _check_arm_or_arm(self, drone: System): - """ - Verifies that the drone is armed, if not armed, arms the drone - Parameters - ---------- - drone : System - The drone system; used for flight operations. + async def _check_arm_or_arm(self, drone: System) -> None: + """ Verifies that the drone is armed, if not armed, arms the drone + Args: + drone: System - The drone system; used for flight operations. + Returns: + None """ async for is_armed in drone.telemetry.armed(): if not is_armed: @@ -51,5 +53,10 @@ async def _check_arm_or_arm(self, drone: System): @property def name(self): - """Returns the name of the class""" - return type(self).__name__ \ No newline at end of file + """ Getter function to return the name of the class + Args: + N/A + Returns: + Name property of current state class + """ + return type(self).__name__ diff --git a/flight/util/json_parsing.py b/flight/util/json_parsing.py index f204bc7..ea838b6 100644 --- a/flight/util/json_parsing.py +++ b/flight/util/json_parsing.py @@ -1,18 +1,24 @@ +from typing import Dict, List import json -def waypoint_parsing(filename: str): +def waypoint_parsing(filename: str) -> List[Dict[str, float]]: + """Accepts name of JSON file and extracts waypoint data from JSON + Args: + filename: str - String of data file to open and access waypoint data + Returns: + Dict - dictionary containing latitude, longitude and altitude of each waypoint in mission + """ f = open(filename, ) - data_set = json.load(f) - # print(data_set) + data_set: Dict[str, List] = json.load(f) f.close() - waypoint_Locs = [] + waypoint_locs: List[Dict[str, float]] = [] for i in range(0, len(data_set["waypoints"])): - waypoint_Locs.append(data_set["waypoints"][i]) + waypoint_locs.append(data_set["waypoints"][i]) - return waypoint_Locs + return waypoint_locs def stationary_obstacle_parsing(filename: str): From c007851480f5c9af9acf38a57fc0729db4b5220e Mon Sep 17 00:00:00 2001 From: Connor Hallemann Date: Tue, 22 Mar 2022 17:20:09 -0700 Subject: [PATCH 07/45] JSON Parsing Documentation --- flight/util/json_parsing.py | 39 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/flight/util/json_parsing.py b/flight/util/json_parsing.py index ea838b6..9450e11 100644 --- a/flight/util/json_parsing.py +++ b/flight/util/json_parsing.py @@ -3,33 +3,38 @@ def waypoint_parsing(filename: str) -> List[Dict[str, float]]: - """Accepts name of JSON file and extracts waypoint data from JSON + """Accepts name of JSON file and extracts waypoint data for SUAS mission Args: filename: str - String of data file to open and access waypoint data Returns: - Dict - dictionary containing latitude, longitude and altitude of each waypoint in mission + List - List of dictionaries containing latitude, longitude and altitude of each waypoint in mission """ - f = open(filename, ) - data_set: Dict[str, List] = json.load(f) + with open(filename) as f: + try: + data_set: Dict[str, List] = json.load(f) + except: + f.close() f.close() - waypoint_locs: List[Dict[str, float]] = [] - - for i in range(0, len(data_set["waypoints"])): - waypoint_locs.append(data_set["waypoints"][i]) + waypoint_locs: List[Dict[str, float]] = [point for point in data_set["waypoints"]] return waypoint_locs -def stationary_obstacle_parsing(filename: str): - f = open(filename, ) - data_set = json.load(f) - # print(data_set) +def stationary_obstacle_parsing(filename: str) -> List[Dict[str, float]]: + """Opens passed JSON file and extracts the Stationary obstacle attributes + Args: + filename: str - String of JSON file name and file type + Returns: + List - list of dictionaries containing latitude, longitude, radius, and height of obstacles + """ + with open(filename) as f: + try: + data_set: Dict[str, List] = json.load(f) + except: + f.close() f.close() - stationary_Obs = [] - - for i in range(0, len(data_set["stationaryObstacles"])): - stationary_Obs.append(data_set["stationaryObstacles"][i]) + stationary_obs: List[Dict[str, float]] = [obs for obs in data_set["stationaryObstacles"]] - return stationary_Obs + return stationary_obs From 59ef20be444417d8fa5830d451c0ca6c7e980b46 Mon Sep 17 00:00:00 2001 From: Connor Hallemann Date: Tue, 22 Mar 2022 17:32:22 -0700 Subject: [PATCH 08/45] Single Line Docstring and Test File Removal --- flight/util/interop_test.py | 20 -------------------- flight/util/json_parsing.py | 1 + 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 flight/util/interop_test.py diff --git a/flight/util/interop_test.py b/flight/util/interop_test.py deleted file mode 100644 index 197a178..0000000 --- a/flight/util/interop_test.py +++ /dev/null @@ -1,20 +0,0 @@ -from interop.client.auvsi_suas.client import client -from interop.client.auvsi_suas.client.client import interop_api_pb2 - -client = client.AsyncClient(url='http://192.168.74.128:8000', username='testuser', password='testpass') - -odlc = interop_api_pb2.Odlc() -odlc.type = interop_api_pb2.Odlc.STANDARD -odlc.latitude = 38 -odlc.longitude = -76 -odlc.orientation = interop_api_pb2.Odlc.N -odlc.shape = interop_api_pb2.Odlc.SQUARE -odlc.shape_color = interop_api_pb2.Odlc.GREEN -odlc.alphanumeric = 'A' -odlc.alphanumeric_color = interop_api_pb2.Odlc.WHITE - -odlc = client.post_odlc(odlc) - -with open('interop/client/auvsi_suas/client/testdata/A.jpg', 'rb') as f: - image_data = f.read() - client.put_odlc_image(odlc.id, image_data) \ No newline at end of file diff --git a/flight/util/json_parsing.py b/flight/util/json_parsing.py index 9450e11..d73ccdd 100644 --- a/flight/util/json_parsing.py +++ b/flight/util/json_parsing.py @@ -1,3 +1,4 @@ +"""Utility function to parse mission data and retrieve specific component data""" from typing import Dict, List import json From 17d9c3a559cc1d0b517a4fdb63d92f13d37e8292 Mon Sep 17 00:00:00 2001 From: Connor Hallemann Date: Thu, 7 Apr 2022 17:54:06 -0700 Subject: [PATCH 09/45] Documentation updates --- flight/state_settings.py | 171 +++++++++++++++++++++++--------- flight/states/final_state.py | 30 ++++-- flight/states/land.py | 30 ++++-- flight/states/pre_processing.py | 32 ++++-- flight/states/start_state.py | 34 +++++-- flight/states/state.py | 79 ++++++++++----- flight/states/takeoff.py | 30 ++++-- flight/states/waypoints.py | 28 ++++-- flight/util/json_parsing.py | 50 ++++++++-- 9 files changed, 348 insertions(+), 136 deletions(-) diff --git a/flight/state_settings.py b/flight/state_settings.py index 322dfdc..d1c852c 100644 --- a/flight/state_settings.py +++ b/flight/state_settings.py @@ -8,30 +8,58 @@ class StateSettings: """ Initialize settings for state machine - Functions: - __init__: Sets preliminary values for SUAS overheads - simple_takeoff() -> None: Enables access to status of simple_takeoff - -> bool: Setter to determine if testing takeoff procedure desired - num_waypoints() -> None: Enables access to number of waypoints in the competition - -> int: Sets the number of waypoints in the competition - run_title() -> None: Enables access to set name of the current flight - -> str: Sets the name of the current flight for logging - run_description() -> None: Enables access to description of current flight mission - -> str: Sets the description of current flight mission - Member Variables: + + Attributes + ---------- __num_waypoints: Number of waypoints on flight plan __run_title: Title of Competition __run_description: Description for Competition + + Methods + ------- + __init__ + Sets preliminary values for SUAS overheads + + @Setter + ------- + simple_takeoff() -> None + Enables access to status of simple_takeoff + num_waypoints() -> None + Enables access to number of waypoints in the competition + run_title() -> None + Enables access to set name of the current flight + run_description() -> None + Enables access to description of current flight mission + + @Property + -------- + simple_takeoff() -> bool + Setter to determine if testing takeoff procedure desired + num_waypoints() -> int + Sets the number of waypoints in the competition + run_title() -> str + Sets the name of the current flight for logging + run_description() -> str + Sets the description of current flight mission + """ def __init__(self, takeoff_bool: bool = False, num_waypoints: int = DEFAULT_WAYPOINTS, title: str = DEFAULT_RUN_TITLE, description: str = DEFAULT_RUN_DESCRIPTION) -> None: """Default constructor results in default settings - Args: - takeoff_bool: bool - determines if a simple takeoff procedure should be executed for testing - num_waypoints: int - Number of waypoints, extracted from SUAS mission plan - title: str - Title of current flight mission, for logging purposes - description: str - Description of current flight mission, for logging purposes - Returns: + + Parameters + ---------- + takeoff_bool: bool + determines if a simple takeoff procedure should be executed for testing + num_waypoints: int + Number of waypoints, extracted from SUAS mission plan + title: str + Title of current flight mission, for logging purposes + description: str + Description of current flight mission, for logging purposes + + Returns + ------- None """ self.__simple_takeoff: bool = takeoff_bool @@ -43,19 +71,30 @@ def __init__(self, takeoff_bool: bool = False, num_waypoints: int = DEFAULT_WAYP @property def simple_takeoff(self) -> bool: """Establishes simple_takeoff as a private member variable - Args: + + Parameters + ---------- N/A - Returns: - bool: Status of simple_takeoff variable + + Returns + ------- + __simple_takeoff: bool + Status of simple_takeoff variable """ return self.__simple_takeoff @simple_takeoff.setter def simple_takeoff(self, simple_takeoff: bool) -> None: - """Setter for whether to perform simple takeoff instead of regular takeoff - Args: - simple_takeoff: bool - True for drone to go straight up, False to behave normally - Returns: + """ + Setter for whether to perform simple takeoff instead of regular takeoff + + Parameters + ---------- + simple_takeoff: bool + True for drone to go straight up, False to behave normally + + Returns + ------- None """ self.__simple_takeoff = simple_takeoff @@ -63,20 +102,32 @@ def simple_takeoff(self, simple_takeoff: bool) -> None: # ---- Waypoint Settings ---- # @property def num_waypoints(self) -> int: - """Establishes num_waypoints as private member variable - Args: + """ + Establishes num_waypoints as private member variable + + Parameters + ---------- N/A - Returns: - int: Number of waypoints in the competition + + Returns + ------- + __num_waypoints: int + Number of waypoints in the competition """ return self.__num_waypoints @num_waypoints.setter def num_waypoints(self, waypoints: int) -> None: - """Set the number of waypoints given by Interop System - Args: - waypoints: int - Number of waypoints present in mission plan - Returns: + """ + Set the number of waypoints given by Interop System + + Parameters + ---------- + waypoints: int + Number of waypoints present in mission plan + + Returns + ------- None """ self.__num_waypoints = waypoints @@ -84,40 +135,64 @@ def num_waypoints(self, waypoints: int) -> None: # ---- Other settings ---- # @property def run_title(self) -> str: - """Set a title for the run/test to be output in logging - Args: + """ + Set a title for the run/test to be output in logging + + Parameters + ---------- N/A - Returns: - str: Created title for flight mission + + Returns + ------- + __run_title: str + Created title for flight mission """ return self.__run_title @run_title.setter def run_title(self, title: str) -> None: - """Sets the title of the flight mission for logging - Args: - title: str - Desired title for the flight - Returns: + """ + Sets the title of the flight mission for logging + + Parameters + ---------- + title: str + Desired title for the flight + + Returns + ------- None """ self.__run_title = title @property def run_description(self) -> str: - """Set a description for the run/test to be output in logging - Args: + """ + Set a description for the run/test to be output in logging + + Parameters + ---------- N/A - Returns: - str: Desired description for flight mission + + Returns + ------- + __run_description: str + Desired description for flight mission """ return self.__run_description @run_description.setter def run_description(self, description: str) -> None: - """Sets a description of the flight mission - Args: - description: str - Written description for flight mission for logs - Returns: + """ + Sets a description of the flight mission + + Parameters + ---------- + description: str + Written description for flight mission for logs + + Returns + ------- None """ self.__run_description = description diff --git a/flight/states/final_state.py b/flight/states/final_state.py index 98781df..6a91db0 100644 --- a/flight/states/final_state.py +++ b/flight/states/final_state.py @@ -6,17 +6,29 @@ class Final(State): - """Empty state to end flight - Functions: - run() -> None: Ends the state machine - Member Variables: - None + """ + Empty state to end flight + + Attributes + ---------- + N/A + + Methods + ------- + run() -> None + Ends the state machine """ async def run(self, drone: System) -> None: - """Do nothing and end - Args: - drone: System - drone object that must be passed to any function with control of the drone - Returns: + """ + Do nothing and end + + Parameters + ---------- + drone: System + drone object that must be passed to any function with control of the drone + + Returns + ------- None """ return diff --git a/flight/states/land.py b/flight/states/land.py index 0469db1..e1f53a2 100644 --- a/flight/states/land.py +++ b/flight/states/land.py @@ -9,16 +9,28 @@ class Land(State): """ State to land the drone safely after finishing other flight & vision tasks - Functions: - run() -> Final: Running the landing procedure after returning to home - Member Variables: - None + + Attributes + ---------- + N/A + + Methods + ------- + run() -> Final + Running the landing procedure after returning to home """ async def run(self, drone: System) -> Final: - """Run landing function to have drone slowly return to home position - Args: - drone: System - MAVSDK drone object for direct drone control - Returns: - Final: final state for ending state machine + """ + Run landing function to have drone slowly return to home position + + Parameters + ---------- + drone: System + MAVSDK drone object for direct drone control + + Returns + ------- + Final : State + final state for ending state machine """ return Final(self.state_settings) diff --git a/flight/states/pre_processing.py b/flight/states/pre_processing.py index 980d915..7723b15 100644 --- a/flight/states/pre_processing.py +++ b/flight/states/pre_processing.py @@ -9,17 +9,29 @@ class PreProcess(State): """ State to accept data and plan out optimal mission structure based on relative distances - Functions: - data_retrieval() -> Takeoff: Connects to Interop and downloads JSON data from system - ... # Will add more functions as needed during dev - Member Variables: - None + + Attributes + ---------- + N/A + + Methods + ------- + data_retrieval() -> Takeoff + Connects to Interop and downloads JSON data from system + """ async def data_retrieval(self, drone: System) -> Takeoff: - """Prelim function to accept data - Args: - drone: System - MAVSDK drone object for direct drone control - Returns: - Takeoff: the next state, the Takeoff state to advance state machine + """ + Prelim function to accept data + + Parameters + ---------- + drone: System + MAVSDK drone object for direct drone control + + Returns + ------- + Takeoff : State + the next state, the Takeoff state to advance state machine """ return Takeoff(self.state_settings) diff --git a/flight/states/start_state.py b/flight/states/start_state.py index 28794c1..d3b0c2e 100644 --- a/flight/states/start_state.py +++ b/flight/states/start_state.py @@ -7,17 +7,31 @@ class Start(State): - """Preliminary state, proceed state machine into pre-processing - Functions: - begin() -> PreProcess: beginning function to start state machine, proceeding to pre-processing step - Member Variables: - None + """ + Preliminary state, proceed state machine into pre-processing + + Attributes + ---------- + N/A + + Methods + ------- + begin() -> PreProcess + Beginning function to start state machine, proceeding to pre-processing step + """ async def begin(self, drone: System) -> PreProcess: - """Initialization function to start flight state machine - Args: - drone: System - drone object for directv drone control - Returns: - PreProcess: data pre-processing state, to advance state machine + """ + Initialization function to start flight state machine + + Parameters + ---------- + drone: System + drone object for directv drone control + + Returns + ------- + PreProcess : State + data pre-processing state, to advance state machine """ return PreProcess(self.state_settings) diff --git a/flight/states/state.py b/flight/states/state.py index 1a3fbe0..3e295c0 100644 --- a/flight/states/state.py +++ b/flight/states/state.py @@ -7,40 +7,68 @@ class State: """ Base State class - Functions: - run() -> (State, None): Runs the code present in each state and returns the next state, or None if at the - end of the machine - _check_arm_or_arm() -> None: Determines if drone is armed, and if not, arms it. - Member Variables: - drone (System): The drone object; used for flight. + + Attributes + ---------- + drone (System) + The drone object; used for flight. + + Methods + ------- + run() -> None + Performs all movement operations contained within the state + _check_arm_or_arm() -> None + Determines if drone is armed, and if not, arms it. + """ def __init__(self, state_settings: StateSettings) -> None: - """ Initializes base State class using a StateSettings class - Args: - state_settings: StateSettings - class which contains basic competition data - Returns: + """ + Initializes base State class using a StateSettings class + + Parameters + ---------- + state_settings: StateSettings + class which contains basic competition data + + Returns + ------- None """ logging.info('State "%s" has begun', self.name) self.state_settings: StateSettings = state_settings async def run(self, drone: System) -> None: - """ Runs the functions and code present in state for competition goals - Args: - drone: System - drone object for MAVSDK control - Returns: + """ + Runs the functions and code present in state for competition goals + + Parameters + ---------- + drone: System + drone object for MAVSDK control + + Returns + ------- None - Exception: - General: raises if no code is present in run function + + Raises + ------ + General + raises if no code is present in run function """ raise Exception("Run not implemented for state") async def _check_arm_or_arm(self, drone: System) -> None: - """ Verifies that the drone is armed, if not armed, arms the drone - Args: - drone: System - The drone system; used for flight operations. - Returns: + """ + Verifies that the drone is armed, if not armed, arms the drone + + Parameters + ---------- + drone: System + The drone system; used for flight operations. + + Returns + ------- None """ async for is_armed in drone.telemetry.armed(): @@ -53,10 +81,15 @@ async def _check_arm_or_arm(self, drone: System) -> None: @property def name(self): - """ Getter function to return the name of the class - Args: + """ + Getter function to return the name of the class + + Parameters + ---------- N/A - Returns: + + Returns + ------- Name property of current state class """ return type(self).__name__ diff --git a/flight/states/takeoff.py b/flight/states/takeoff.py index 78a2884..42f6977 100644 --- a/flight/states/takeoff.py +++ b/flight/states/takeoff.py @@ -9,16 +9,28 @@ class Takeoff(State): """ Runs manual takeoff process, to lift drone to 100ft altitude - Functions: - run() -> Waypoints: runs drone movement functions to rise to 100ft - Member Variables: - None + + Attributes + ---------- + N/A + + Methods + ------- + run() -> Waypoints + runs drone movement functions to rise to 100ft """ async def run(self, drone: System) -> Waypoints: - """Run offboard functions to have the drone takeoff - Args: - drone: System - MAVSDK drone object for direct drone control - Returns: - Waypoints: next state, Waypoints state to perform waypoint flight plan + """ + Run offboard functions to have the drone takeoff + + Parameters + ---------- + drone: System + MAVSDK drone object for direct drone control + + Returns + ------- + Waypoints : State + Next state, Waypoints state to perform waypoint flight plan """ return Waypoints(self.state_settings) diff --git a/flight/states/waypoints.py b/flight/states/waypoints.py index 2c94705..bad6a54 100644 --- a/flight/states/waypoints.py +++ b/flight/states/waypoints.py @@ -9,17 +9,29 @@ class Waypoints(State): """ State to fly to each waypoint in mission plan, using Obstacle Avoidance during flight - Functions: - run() -> Land: For each waypoint, flies to Latlon & avoids any obstacles - Member Variables: - None + + Attributes + ---------- + N/A + + Methods + ------- + run() -> Land + For each waypoint, flies to Latlon & avoids any obstacles + """ async def run(self, drone: System) -> Land: - """Run waypoint path plan with obstacle avoidance running throughout - Args: + """ + Run waypoint path plan with obstacle avoidance running throughout + + Parameters + ---------- drone: System - MAVSDK drone object for direct drone control - Returns: - Land: landing state to land drone after finishing tasks # This will change once other tasks become + + Returns + ------- + Land : State + landing state to land drone after finishing tasks # This will change once other tasks become testable within state machine """ return Land(self.state_settings) diff --git a/flight/util/json_parsing.py b/flight/util/json_parsing.py index d73ccdd..28b7d38 100644 --- a/flight/util/json_parsing.py +++ b/flight/util/json_parsing.py @@ -4,11 +4,26 @@ def waypoint_parsing(filename: str) -> List[Dict[str, float]]: - """Accepts name of JSON file and extracts waypoint data for SUAS mission - Args: - filename: str - String of data file to open and access waypoint data - Returns: - List - List of dictionaries containing latitude, longitude and altitude of each waypoint in mission + """ + Accepts name of JSON file and extracts waypoint data for SUAS mission + + Parameters + --------- + filename: str + String of data file to open and access waypoint data + + Returns + ------- + List[Dict[str, float]] + List of dictionaries containing latitude, longitude and altitude of each waypoint in mission + Dict[str, float] + dictionary containing specific coordinate or attribute with string label + + Raises + ------ + General Exception + Designed to catch any error that could arise and prevent corruption + """ with open(filename) as f: try: @@ -23,11 +38,26 @@ def waypoint_parsing(filename: str) -> List[Dict[str, float]]: def stationary_obstacle_parsing(filename: str) -> List[Dict[str, float]]: - """Opens passed JSON file and extracts the Stationary obstacle attributes - Args: - filename: str - String of JSON file name and file type - Returns: - List - list of dictionaries containing latitude, longitude, radius, and height of obstacles + """ + Opens passed JSON file and extracts the Stationary obstacle attributes + + Parameters + ---------- + filename: str + String of JSON file name and file type + + Returns + ------- + stationary_obs: List[Dict[str, float]] + list of dictionaries containing latitude, longitude, radius, and height of obstacles + Dict[str, float] + dictionary containing specific coordinate or attribute with string label + + Raises + ------ + General Exception + Broad exception to catch any error and prevent corruption + """ with open(filename) as f: try: From 86e31b9d21374305580da75e9ef548b45f154c77 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 4 Nov 2021 17:57:15 -0500 Subject: [PATCH 10/45] Redundant Task --- vision/Object Detection/object_image.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 vision/Object Detection/object_image.py diff --git a/vision/Object Detection/object_image.py b/vision/Object Detection/object_image.py deleted file mode 100644 index e69de29..0000000 From 4be6bfc3ba79e98a90b294b0f1bf1f66c8653114 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 4 Nov 2021 17:58:21 -0500 Subject: [PATCH 11/45] pre-commit config from IARC --- .pre-commit-config.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..345f8cb --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,13 @@ +repos: +- repo: https://github.com/ambv/black + rev: stable + hooks: + - id: black + language_version: python3.8 +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.5.0 # Use the ref you want to point at + hooks: + - id: trailing-whitespace + - id: check-added-large-files + - id: check-merge-conflict + - id: end-of-file-fixer From 5c2816eabad98fba563e9c44a15b8cfa1365f643 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 4 Nov 2021 17:59:27 -0500 Subject: [PATCH 12/45] gitignore from IARC --- .gitignore | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5608405 --- /dev/null +++ b/.gitignore @@ -0,0 +1,124 @@ +# Vision +vision_images/ +vision_videos/ +*.jpg +*.png +*.bag + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# PyCharm +.idea/ +# Vim +.vim/ +# Visual Studio Code +.vscode/ + +# Spreadsheets +*.csv + +# Vision Benchmark Ouputs +results/*.txt From 332a2c0bc46398e09d1481823fd9506ba7dfbe98 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 4 Nov 2021 18:31:27 -0500 Subject: [PATCH 13/45] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 345f8cb..f2abfa7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/ambv/black - rev: stable + rev: 21.10b0 hooks: - id: black language_version: python3.8 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.5.0 # Use the ref you want to point at + rev: v4.0.1 # Use the ref you want to point at hooks: - id: trailing-whitespace - id: check-added-large-files From 1b80126995c71278ce55e4f9a366572f95d6fa93 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 4 Nov 2021 18:39:42 -0500 Subject: [PATCH 14/45] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 7f34587..1296b23 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,8 @@ # SUAS-2022 Missouri S&T Multirotor Robot Design Team's code for the Association for Unmanned Vehicle Systems International's 2022 Student Unmanned Aerial Systems Competition (AUVSI SUAS 2022) + +# Setup +``` +pip install pre-commit +pre-commit install +``` From 5fed42d21e078f1957928146f52ae3cf485af98b Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 4 Nov 2021 18:54:09 -0500 Subject: [PATCH 15/45] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1296b23..c8185f1 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,4 @@ Missouri S&T Multirotor Robot Design Team's code for the Association for Unm pip install pre-commit pre-commit install ``` + > `branch_name` should follow the convention `feature/{feature_name}`, or `hotfix/{fix_name}` From e1fa9f78fe9a4a3b0a884f9e2a8fb1a33b4cd1b8 Mon Sep 17 00:00:00 2001 From: mrouie Date: Fri, 5 Nov 2021 00:15:38 -0500 Subject: [PATCH 16/45] Add json, xml, yaml, and mixed line ending hooks --- .pre-commit-config.yaml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2abfa7..1af943d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,19 @@ -repos: -- repo: https://github.com/ambv/black - rev: 21.10b0 +repo: +- repo: https://github.com/ambv/black + rev: stable hooks: - id: black language_version: python3.8 -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 # Use the ref you want to point at +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.15.0 # Use the ref you want to point at hooks: - - id: trailing-whitespace - - id: check-added-large-files - - id: check-merge-conflict - - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-added-large-files + - id: check-merge-conflict + - id: end-of-file-fixer + - id: check-json + - id: check-xml + - id: check-yaml + - id: mixed-line-ending + args: ['--fix=lf'] + description: Forces line endings to the UNIX LF character From c283b6a5db9b1c6d249da7ed6a548b5236caf638 Mon Sep 17 00:00:00 2001 From: mrouie Date: Fri, 5 Nov 2021 00:45:48 -0500 Subject: [PATCH 17/45] Resolved errors --- .pre-commit-config.yaml | 32 ++++++++++++++++---------------- run.py | 1 + 2 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 run.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1af943d..109dece 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,19 +1,19 @@ -repo: -- repo: https://github.com/ambv/black +repos: +- repo: https://github.com/ambv/black rev: stable hooks: - - id: black - language_version: python3.8 -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.15.0 # Use the ref you want to point at + - id: black + language_version: python3.8 +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 # Use the ref you want to point at hooks: - - id: trailing-whitespace - - id: check-added-large-files - - id: check-merge-conflict - - id: end-of-file-fixer - - id: check-json - - id: check-xml - - id: check-yaml - - id: mixed-line-ending - args: ['--fix=lf'] - description: Forces line endings to the UNIX LF character + - id: trailing-whitespace + - id: check-added-large-files + - id: check-merge-conflict + - id: end-of-file-fixer + - id: check-json + - id: check-xml + - id: check-yaml + - id: mixed-line-ending + args: ['--fix=lf'] + description: Forces line endings to the UNIX LF character diff --git a/run.py b/run.py new file mode 100644 index 0000000..ba38a01 --- /dev/null +++ b/run.py @@ -0,0 +1 @@ +## From e3f7cfe659dbc12f9adf19963451449e9f2eed43 Mon Sep 17 00:00:00 2001 From: ClayJay3 Date: Thu, 4 Nov 2021 19:09:58 -0500 Subject: [PATCH 18/45] Create object bounding box script. --- .pre-commit-config.yaml | 2 +- .../Object Detection/object_bounding_box.py | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 109dece..e56b65d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/ambv/black - rev: stable + rev: 21.10b0 hooks: - id: black language_version: python3.8 diff --git a/vision/Object Detection/object_bounding_box.py b/vision/Object Detection/object_bounding_box.py index e69de29..8835f86 100644 --- a/vision/Object Detection/object_bounding_box.py +++ b/vision/Object Detection/object_bounding_box.py @@ -0,0 +1,74 @@ +""" +Find the bounding box of the contour and crop down the image to include only the object of interest. +""" +import cv2 +import numpy as np +import argparse +import random + + +def get_bounding_image(img, contour): + """ + Calculate the bounding box with the given contour and then crop the given image. + + Parameters + ---------- + img: numpy.array + The input image to be cropped. + contour: numpy.array + The contour located in the given image. Used to find bounding box for cropping. + + Returns + ------- + numpy.array - Cropped image. + """ + # Find the bounding box of the contour. + x, y, w, h = cv2.boundingRect(contour) + # Return the cropped image using numpy splicing. + return img[y : y + h, x : x + w] + + +if __name__ == "__main__": + """ + Driver for testing object_bounding_box. + """ + # Create object for parsing command-line arguments. + parser = argparse.ArgumentParser( + description="Read the given input image and crop based on a randomly generated contour." + ) + # Add parser arguments. + parser.add_argument("-i", "--input", type=str, help="Path to image file.") + # Parse the command line arguments to an object. + args = parser.parse_args() + + # Check if input was given. + if args.input: + # Open the img at the given path. + try: + img = cv2.imread(args.input) + except FileNotFoundError as e: + print(f"Unable to get image file: {e}") + + # Get image size info. + h, w, c = img.shape + # Generate a random contour, then find bounding box and crop the given image. + contour = np.array( + [ + [random.randint(0, w), random.randint(0, h)], + [random.randint(0, w), random.randint(0, h)], + [random.randint(0, w), random.randint(0, h)], + ], + dtype=np.int32, + ) + + # Call bounding box function. + cropped = get_bounding_image(img, contour) + # Display new image. + cv2.imshow("bounding box", cropped) + cv2.imshow("original", img) + cv2.waitKey(0) + else: + # Throw error if no input file was given. + raise FileNotFoundError( + "No input image file has been given. For help type --help" + ) From fdee5e96bd739fbe49df8ff068a47f9486b50e5f Mon Sep 17 00:00:00 2001 From: ClayJay3 Date: Tue, 9 Nov 2021 22:16:20 -0600 Subject: [PATCH 19/45] Update object_bounding_box.py --- vision/Object Detection/object_bounding_box.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vision/Object Detection/object_bounding_box.py b/vision/Object Detection/object_bounding_box.py index 8835f86..6480eba 100644 --- a/vision/Object Detection/object_bounding_box.py +++ b/vision/Object Detection/object_bounding_box.py @@ -7,7 +7,7 @@ import random -def get_bounding_image(img, contour): +def get_bounding_image(img: np.array, contour: np.array) -> np.array: """ Calculate the bounding box with the given contour and then crop the given image. From a68ed20cdeb272ade69e390a006728d1d51f663a Mon Sep 17 00:00:00 2001 From: mrouie Date: Tue, 1 Mar 2022 19:24:48 -0600 Subject: [PATCH 20/45] Test file. remove pls --- run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.py b/run.py index ba38a01..da9b56c 100644 --- a/run.py +++ b/run.py @@ -1 +1 @@ -## +this_is_a_really_useful_variable_name = tuple(list(tuple(list(tuple(list([1,2,3]))))))# bad code was written on this day on this time on this year From c85e2769729335f97d14e54d5e2a536fba8f882f Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Tue, 8 Feb 2022 18:59:54 -0600 Subject: [PATCH 21/45] Auto-Formatting --- vision/Object Detection/standard_obj_text.py | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 vision/Object Detection/standard_obj_text.py diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py new file mode 100644 index 0000000..79b0210 --- /dev/null +++ b/vision/Object Detection/standard_obj_text.py @@ -0,0 +1,26 @@ +""" +Functions related to detecting the text of standard objects. +""" + + +def detect_text(): + """ + Detect text within an image. + + Returns location and character. ## TODO: Documentation to be updated later + """ + return + + +def get_text_color(): + """ + Detect the color of the text. + """ + return + + +if __name__ == "__main__": + """ + Driver for testing text detection and classification functions. + """ + pass From 317d562984d1402f7456b0e5565fad8f00c91f6d Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Tue, 8 Feb 2022 20:04:22 -0600 Subject: [PATCH 22/45] Initial text detection --- vision/Object Detection/standard_obj_text.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index 79b0210..91b56da 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -1,20 +1,32 @@ """ Functions related to detecting the text of standard objects. """ +import cv2 +import numpy as np +import pytesseract - -def detect_text(): +def detect_text(img): """ Detect text within an image. + Will return string for parameter odlc.alphanumeric + + Parameters + ---------- + img : np.ndarray + image to detect text within Returns location and character. ## TODO: Documentation to be updated later """ + + txt_data = pytesseract.image_to_data(img_rgb, output_type=pytesseract.Output.DICT, lang="eng") # get data from image + return def get_text_color(): """ Detect the color of the text. + Will return interop_api_pb2.Odlc.%COLOR% for odlc.alphanumeric_color """ return @@ -23,4 +35,6 @@ def get_text_color(): """ Driver for testing text detection and classification functions. """ - pass + img = cv2.imread("test_image.jpg") + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # Convert to RGB ## TODO: remove, not needed for camera feed, only for opencv imreads + From 3664a478d7b5d7e151e91ec20d554b437ce90c7c Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Tue, 8 Feb 2022 20:38:11 -0600 Subject: [PATCH 23/45] notes on random ideas --- vision/Object Detection/standard_obj_text.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index 91b56da..1bcd4b0 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -30,6 +30,19 @@ def get_text_color(): """ return +''' +Random Ideas: +- seperate detection functions + - detect general - detection for when drone is just flying around + - detect on object - detection for when object has been identified, will crop/rotate image to encompass the object and then detect text +- multiprocess image on each of 4 axis for text + - would be quicker and check multiple angles for the text + - would need function to map detected text back to unrotated images +- text color + - find average color of area within detected text and map to nearest color in interop + OR + - check for each color from interop within detected text - slower +''' if __name__ == "__main__": """ From db2ed9b84796bf859d5ce1dd2c8d7dac87523770 Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Thu, 10 Feb 2022 18:40:44 -0600 Subject: [PATCH 24/45] Updated format, typing --- vision/Object Detection/standard_obj_text.py | 64 ++++++++++++++++---- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index 1bcd4b0..f65370e 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -2,35 +2,72 @@ Functions related to detecting the text of standard objects. """ import cv2 +from matplotlib.pyplot import get import numpy as np import pytesseract -def detect_text(img): + +def splice_rotate_img(img: np.ndarray, bounds: np.ndarray) -> np.ndarray: """ - Detect text within an image. - Will return string for parameter odlc.alphanumeric + Splice a portion of an image and rotate to be rectangular. Parameters ---------- img : np.ndarray - image to detect text within + the image to take a splice of + bounds : np.ndarray + array of tuple bounds (4 x-y coordinates) - Returns location and character. ## TODO: Documentation to be updated later + Returns + ------- + np.ndarray + the spliced/rotated images """ - - txt_data = pytesseract.image_to_data(img_rgb, output_type=pytesseract.Output.DICT, lang="eng") # get data from image - return -def get_text_color(): +def get_text_color(img: np.ndarray, bounds: np.ndarray) -> np.ndarray: """ Detect the color of the text. Will return interop_api_pb2.Odlc.%COLOR% for odlc.alphanumeric_color + ## TODO: change to return type to interop enum class """ return -''' + +def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> tuple: + """ + Detect text within an image. + Will return string for parameter odlc.alphanumeric + + Parameters + ---------- + img : np.ndarray + image to detect text within + bounds : np.ndarray + array of tuple bounds (4 x-y coordinates) + + Returns + ------- + tuple + tuple containing bounds array and the character detected ([bounds], 'character', color) + + ## TODO: Documentation to be updated later + """ + corrected_img = img + if bounds != None: + corrected_img = splice_rotate_img(img, bounds) + + txt_data = pytesseract.image_to_data( + img_rgb, output_type=pytesseract.Output.DICT, lang="eng" + ) # get data from image + + color = get_text_color() + + return + + +""" Random Ideas: - seperate detection functions - detect general - detection for when drone is just flying around @@ -42,12 +79,13 @@ def get_text_color(): - find average color of area within detected text and map to nearest color in interop OR - check for each color from interop within detected text - slower -''' +""" if __name__ == "__main__": """ Driver for testing text detection and classification functions. """ img = cv2.imread("test_image.jpg") - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # Convert to RGB ## TODO: remove, not needed for camera feed, only for opencv imreads - + img_rgb = cv2.cvtColor( + img, cv2.COLOR_BGR2RGB + ) # Convert to RGB ## TODO: remove, not needed for camera feed, only for opencv imreads From 91a8e3f8e878a27a04a75a53b315a89e4cc09d91 Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Thu, 10 Feb 2022 19:33:27 -0600 Subject: [PATCH 25/45] Filtering detected text --- vision/Object Detection/standard_obj_text.py | 52 +++++++++++++++----- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index f65370e..179e7e9 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -1,6 +1,7 @@ """ Functions related to detecting the text of standard objects. """ +from curses.ascii import isupper import cv2 from matplotlib.pyplot import get import numpy as np @@ -31,11 +32,22 @@ def get_text_color(img: np.ndarray, bounds: np.ndarray) -> np.ndarray: Detect the color of the text. Will return interop_api_pb2.Odlc.%COLOR% for odlc.alphanumeric_color ## TODO: change to return type to interop enum class + + Ideas + ----- + - find average color of area within detected text and map to nearest color in interop + OR + - check for each color from interop within detected text - slower """ - return + color = np.array( + [np.mean(img[:, :, 0]), np.mean(img[:, :, 1]), np.mean(img[:, :, 2])] + ) -def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> tuple: + return color + + +def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: """ Detect text within an image. Will return string for parameter odlc.alphanumeric @@ -49,22 +61,42 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> tuple: Returns ------- - tuple - tuple containing bounds array and the character detected ([bounds], 'character', color) + np.ndarray + np.ndarray containing detected characters, format: ([bounds], 'character', color) ## TODO: Documentation to be updated later """ + # correct image if necessary corrected_img = img if bounds != None: corrected_img = splice_rotate_img(img, bounds) + # detect text txt_data = pytesseract.image_to_data( - img_rgb, output_type=pytesseract.Output.DICT, lang="eng" - ) # get data from image + corrected_img, output_type=pytesseract.Output.DICT, lang="eng" + ) - color = get_text_color() + found_characters = np.array([]) - return + # filter detected text to find valid characters + for i, txt in enumerate(txt_data): + if txt != None and len(txt) == 1: # length of 1 + # must be uppercase letter or number + if isupper(txt) or txt.isnumeric(): + # get data for each text object detected + x = txt_data["left"][i] + y = txt_data["top"][i] + w = txt_data["width"][i] + h = txt_data["height"][i] + + bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)] + + color = get_text_color(img, bounds) + + # add to found characters array + found_characters = np.append(found_characters, (txt, bounds, color)) + + return found_characters """ @@ -76,9 +108,7 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> tuple: - would be quicker and check multiple angles for the text - would need function to map detected text back to unrotated images - text color - - find average color of area within detected text and map to nearest color in interop - OR - - check for each color from interop within detected text - slower + """ if __name__ == "__main__": From d1cf74ba40decb0cd3d4b2e565d65db241ec9fdf Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Thu, 10 Feb 2022 19:52:56 -0600 Subject: [PATCH 26/45] Plan for getting text color --- vision/Object Detection/standard_obj_text.py | 32 ++++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index 179e7e9..63a37a3 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -32,18 +32,30 @@ def get_text_color(img: np.ndarray, bounds: np.ndarray) -> np.ndarray: Detect the color of the text. Will return interop_api_pb2.Odlc.%COLOR% for odlc.alphanumeric_color ## TODO: change to return type to interop enum class + Need way to filter out object color from text color. Ideas ----- - - find average color of area within detected text and map to nearest color in interop - OR - - check for each color from interop within detected text - slower + kmeans to filter down to most common color in bounds + - likely to be the color of the text + get average color after kmeans """ + # kmeans to get single color + cropped_img = img[bounds[0, 0] : bounds[2, 0], bounds[0, 1] : bounds[2, 1]] + # kmeans_img = cv2.kmeans(cropped_img, K=1,) + # get average color of detected text color = np.array( - [np.mean(img[:, :, 0]), np.mean(img[:, :, 1]), np.mean(img[:, :, 2])] + [ + np.mean(img[:, :, 0]), + np.mean(img[:, :, 1]), + np.mean(img[:, :, 2]), + ] ## TODO: swtich to kmeans image ) + # map detected color to available colors in competition + ## TODO: need to get way to correlate to available competition colors + return color @@ -99,18 +111,6 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: return found_characters -""" -Random Ideas: -- seperate detection functions - - detect general - detection for when drone is just flying around - - detect on object - detection for when object has been identified, will crop/rotate image to encompass the object and then detect text -- multiprocess image on each of 4 axis for text - - would be quicker and check multiple angles for the text - - would need function to map detected text back to unrotated images -- text color - -""" - if __name__ == "__main__": """ Driver for testing text detection and classification functions. From b05b5f9bdfa2fbfc091c73bd5e6b23a36f932bbf Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Thu, 10 Feb 2022 20:00:07 -0600 Subject: [PATCH 27/45] Update doc, fix char check --- vision/Object Detection/standard_obj_text.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index 63a37a3..2387f08 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -39,6 +39,18 @@ def get_text_color(img: np.ndarray, bounds: np.ndarray) -> np.ndarray: kmeans to filter down to most common color in bounds - likely to be the color of the text get average color after kmeans + + Parameters + ---------- + img : np.ndarray + the image the text is in + bounds : np.ndarray + bounds of the text + + Returns + ------- + np.ndarray + the color of the text """ # kmeans to get single color cropped_img = img[bounds[0, 0] : bounds[2, 0], bounds[0, 1] : bounds[2, 1]] @@ -94,7 +106,7 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: for i, txt in enumerate(txt_data): if txt != None and len(txt) == 1: # length of 1 # must be uppercase letter or number - if isupper(txt) or txt.isnumeric(): + if (txt.isalpha() and isupper(txt)) or txt.isnumeric(): # get data for each text object detected x = txt_data["left"][i] y = txt_data["top"][i] From 5dee1569995efd21919fdd3b7057a3640f6816fe Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Tue, 15 Feb 2022 19:30:57 -0600 Subject: [PATCH 28/45] Image processing, fixed detection --- vision/Object Detection/standard_obj_text.py | 51 ++++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index 2387f08..882c51d 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -95,15 +95,42 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: if bounds != None: corrected_img = splice_rotate_img(img, bounds) + # Image processing to make text more clear + gray_img = cv2.cvtColor(corrected_img, cv2.COLOR_RGB2GRAY) + + blurred_img = cv2.GaussianBlur(gray_img, ksize=(5, 5), sigmaX=0) + + # cv2.imshow("Blurred", blurred_img) + # cv2.waitKey(0) + # cv2.destroyAllWindows() + + # laplace_img = cv2.Laplacian(blurred_img, ddepth=cv2.CV_8U, ksize=5) + + # cv2.imshow("Laplacian", laplace_img) + # cv2.waitKey(0) + # cv2.destroyAllWindows() + + output_image = np.dstack((blurred_img, blurred_img, blurred_img)) + + print("Image processing complete.") # detect text txt_data = pytesseract.image_to_data( - corrected_img, output_type=pytesseract.Output.DICT, lang="eng" + blurred_img, output_type=pytesseract.Output.DICT, lang="eng", config='--psm 10' ) - + print(txt_data) + # filter detected text to find valid characters found_characters = np.array([]) + for i, txt in enumerate(txt_data["text"]): + print("Text:", txt) + x = txt_data["left"][i] + y = txt_data["top"][i] + w = txt_data["width"][i] + h = txt_data["height"][i] + bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)] + + for j in range(4): + cv2.line(output_image, bounds[j], bounds[j+1], (0, 255, 0), thickness=2) - # filter detected text to find valid characters - for i, txt in enumerate(txt_data): if txt != None and len(txt) == 1: # length of 1 # must be uppercase letter or number if (txt.isalpha() and isupper(txt)) or txt.isnumeric(): @@ -115,11 +142,16 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)] - color = get_text_color(img, bounds) + color = 'Green' # TODO: remove + # color = get_text_color(img, bounds) # TODO: uncomment when implemented # add to found characters array found_characters = np.append(found_characters, (txt, bounds, color)) + # cv2.imshow("Output", output_image) + # cv2.waitKey(0) + # cv2.destroyAllWindows() + return found_characters @@ -127,7 +159,8 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: """ Driver for testing text detection and classification functions. """ - img = cv2.imread("test_image.jpg") - img_rgb = cv2.cvtColor( - img, cv2.COLOR_BGR2RGB - ) # Convert to RGB ## TODO: remove, not needed for camera feed, only for opencv imreads + img = cv2.imread("/home/cameron/Documents/GitHub/SUAS-2022/vision/Object Detection/letter_a.jpg") + + detected_chars = detect_text(img) + + print("The following characters were found in the image:", detected_chars) \ No newline at end of file From ebb5cf662e963a50c06a8710bbbcafcfb02a4499 Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Tue, 15 Feb 2022 19:53:18 -0600 Subject: [PATCH 29/45] Draw line around detected characters --- vision/Object Detection/standard_obj_text.py | 40 +++++++++++--------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index 882c51d..242bc68 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -119,19 +119,19 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: ) print(txt_data) # filter detected text to find valid characters - found_characters = np.array([]) + found_characters = [] for i, txt in enumerate(txt_data["text"]): - print("Text:", txt) - x = txt_data["left"][i] - y = txt_data["top"][i] - w = txt_data["width"][i] - h = txt_data["height"][i] - bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)] - - for j in range(4): - cv2.line(output_image, bounds[j], bounds[j+1], (0, 255, 0), thickness=2) - - if txt != None and len(txt) == 1: # length of 1 + # Shows all detected text ## TODO: remove + # print("Text:", txt) + # x = txt_data["left"][i] + # y = txt_data["top"][i] + # w = txt_data["width"][i] + # h = txt_data["height"][i] + # bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)] + # for j in range(4): + # cv2.line(output_image, bounds[j], bounds[j+1], (0, 255, 0), thickness=2) + + if (txt != None) and (len(txt) == 1): # length of 1 # must be uppercase letter or number if (txt.isalpha() and isupper(txt)) or txt.isnumeric(): # get data for each text object detected @@ -146,11 +146,17 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: # color = get_text_color(img, bounds) # TODO: uncomment when implemented # add to found characters array - found_characters = np.append(found_characters, (txt, bounds, color)) + found_characters += [(txt, bounds, color)] + + for c in found_characters: + cv2.line(output_image, c[1][0], c[1][1], (0, 0, 255), thickness=2) + cv2.line(output_image, c[1][1], c[1][2], (0, 0, 255), thickness=2) + cv2.line(output_image, c[1][2], c[1][3], (0, 0, 255), thickness=2) + cv2.line(output_image, c[1][3], c[1][0], (0, 0, 255), thickness=2) - # cv2.imshow("Output", output_image) - # cv2.waitKey(0) - # cv2.destroyAllWindows() + cv2.imshow("Output", output_image) + cv2.waitKey(0) + cv2.destroyAllWindows() return found_characters @@ -159,7 +165,7 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: """ Driver for testing text detection and classification functions. """ - img = cv2.imread("/home/cameron/Documents/GitHub/SUAS-2022/vision/Object Detection/letter_a.jpg") + img = cv2.imread("/home/cameron/Documents/GitHub/SUAS-2022/vision/Object Detection/text_y.jpg") detected_chars = detect_text(img) From 79f6418249fb2bde08f1a971a247b49f1c8f3d27 Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Tue, 15 Feb 2022 19:56:14 -0600 Subject: [PATCH 30/45] Autoformatting --- vision/Object Detection/standard_obj_text.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index 242bc68..67a4236 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -99,11 +99,11 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: gray_img = cv2.cvtColor(corrected_img, cv2.COLOR_RGB2GRAY) blurred_img = cv2.GaussianBlur(gray_img, ksize=(5, 5), sigmaX=0) - + # cv2.imshow("Blurred", blurred_img) # cv2.waitKey(0) # cv2.destroyAllWindows() - + # laplace_img = cv2.Laplacian(blurred_img, ddepth=cv2.CV_8U, ksize=5) # cv2.imshow("Laplacian", laplace_img) @@ -115,7 +115,7 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: print("Image processing complete.") # detect text txt_data = pytesseract.image_to_data( - blurred_img, output_type=pytesseract.Output.DICT, lang="eng", config='--psm 10' + blurred_img, output_type=pytesseract.Output.DICT, lang="eng", config="--psm 10" ) print(txt_data) # filter detected text to find valid characters @@ -129,7 +129,7 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: # h = txt_data["height"][i] # bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)] # for j in range(4): - # cv2.line(output_image, bounds[j], bounds[j+1], (0, 255, 0), thickness=2) + # cv2.line(output_image, bounds[j], bounds[j+1], (0, 255, 0), thickness=2) if (txt != None) and (len(txt) == 1): # length of 1 # must be uppercase letter or number @@ -142,12 +142,12 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)] - color = 'Green' # TODO: remove + color = "Green" # TODO: remove # color = get_text_color(img, bounds) # TODO: uncomment when implemented # add to found characters array found_characters += [(txt, bounds, color)] - + for c in found_characters: cv2.line(output_image, c[1][0], c[1][1], (0, 0, 255), thickness=2) cv2.line(output_image, c[1][1], c[1][2], (0, 0, 255), thickness=2) @@ -165,8 +165,10 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: """ Driver for testing text detection and classification functions. """ - img = cv2.imread("/home/cameron/Documents/GitHub/SUAS-2022/vision/Object Detection/text_y.jpg") + img = cv2.imread( + "/home/cameron/Documents/GitHub/SUAS-2022/vision/Object Detection/text_y.jpg" + ) detected_chars = detect_text(img) - - print("The following characters were found in the image:", detected_chars) \ No newline at end of file + + print("The following characters were found in the image:", detected_chars) From 26b1d7bfeba054904a83846fb5aea143b6ce993e Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Tue, 22 Feb 2022 19:21:58 -0600 Subject: [PATCH 31/45] New preprocessing, rotate images --- vision/Object Detection/standard_obj_text.py | 118 ++++++++++++++----- 1 file changed, 87 insertions(+), 31 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index 67a4236..d30fef9 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -8,23 +8,46 @@ import pytesseract -def splice_rotate_img(img: np.ndarray, bounds: np.ndarray) -> np.ndarray: +def slice_rotate_img(img: np.ndarray, bounds: np.ndarray) -> np.ndarray: """ - Splice a portion of an image and rotate to be rectangular. + Slice a portion of an image and rotate to be rectangular. Parameters ---------- img : np.ndarray the image to take a splice of bounds : np.ndarray - array of tuple bounds (4 x-y coordinates) + array of tuple bounds (4 x-y coordinates; tl-tr-br-bl) Returns ------- np.ndarray the spliced/rotated images """ - return + # Find center point + min_x = np.amin(bounds[:][0]) + max_x = np.amax(bounds[:][0]) + min_y = np.amin(bounds[:][1]) + max_y = np.amax(bounds[:][1]) + center_pt = (int((max_x + min_x) / 2), int((max_y + min_y) / 2)) + print(center_pt) + # Get angle of rotation + tl_x = bounds[0][0] + tr_x = bounds[1][0] + tl_y = bounds[0][1] + tr_y = bounds[1][1] + angle = np.rad2deg(np.arctan((tr_y - tl_y) / (tr_x - tl_x))) + print(angle) + + # Rotate image + rot_mat = cv2.getRotationMatrix2D(center_pt, angle, 1.0) + rotated_img = cv2.warpAffine(img, rot_mat, img.shape[1::-1], flags=cv2.INTER_LINEAR) + + cv2.imshow("Rotated image", rotated_img) + cv2.waitKey(0) + cv2.destroyAllWindows() + + return rotated_img def get_text_color(img: np.ndarray, bounds: np.ndarray) -> np.ndarray: @@ -71,6 +94,47 @@ def get_text_color(img: np.ndarray, bounds: np.ndarray) -> np.ndarray: return color +def preprocess_img(img: np.ndarray) -> np.ndarray: + """ + Preprocess image for text detection. + + Parameters + ---------- + img : np.ndarray + image to preprocess + + Returns + ------- + np.ndarray + the image after preprocessing + """ + # grayscale + gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) + + # blur to remove noise + blur = cv2.medianBlur(gray, ksize=9) + + # thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1] + + # erode and dilate to increase text clarity and reduce noise + kernel = np.ones((5, 5), np.uint8) + eroded = cv2.erode(blur, kernel=kernel, iterations=1) + dilated = cv2.dilate(eroded, kernel=kernel, iterations=1) + + laplace_img = cv2.Laplacian(dilated, ddepth=cv2.CV_8U, ksize=5) + + # binarize image + binarized = np.where(laplace_img > 50, np.uint8(255), np.uint8(0)) + print(type(binarized[0][0])) + # print(np.shape(binarized)) + + # edge detection + # edges = cv2.Canny(laplace_img, 100, 200) + + return binarized + # return binarized + + def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: """ Detect text within an image. @@ -93,47 +157,31 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: # correct image if necessary corrected_img = img if bounds != None: - corrected_img = splice_rotate_img(img, bounds) + corrected_img = slice_rotate_img(img, bounds) # Image processing to make text more clear - gray_img = cv2.cvtColor(corrected_img, cv2.COLOR_RGB2GRAY) - - blurred_img = cv2.GaussianBlur(gray_img, ksize=(5, 5), sigmaX=0) + processed_img = preprocess_img(corrected_img) - # cv2.imshow("Blurred", blurred_img) - # cv2.waitKey(0) - # cv2.destroyAllWindows() - - # laplace_img = cv2.Laplacian(blurred_img, ddepth=cv2.CV_8U, ksize=5) - - # cv2.imshow("Laplacian", laplace_img) - # cv2.waitKey(0) - # cv2.destroyAllWindows() + cv2.imshow("Processed Image", processed_img) + cv2.waitKey(0) + cv2.destroyAllWindows() - output_image = np.dstack((blurred_img, blurred_img, blurred_img)) + # output_image = np.dstack((blurred_img, blurred_img, blurred_img)) + output_image = np.dstack((processed_img, processed_img, processed_img)) print("Image processing complete.") # detect text txt_data = pytesseract.image_to_data( - blurred_img, output_type=pytesseract.Output.DICT, lang="eng", config="--psm 10" + output_image, output_type=pytesseract.Output.DICT, lang="eng", config="--psm 10" ) print(txt_data) # filter detected text to find valid characters found_characters = [] for i, txt in enumerate(txt_data["text"]): - # Shows all detected text ## TODO: remove - # print("Text:", txt) - # x = txt_data["left"][i] - # y = txt_data["top"][i] - # w = txt_data["width"][i] - # h = txt_data["height"][i] - # bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)] - # for j in range(4): - # cv2.line(output_image, bounds[j], bounds[j+1], (0, 255, 0), thickness=2) - if (txt != None) and (len(txt) == 1): # length of 1 # must be uppercase letter or number - if (txt.isalpha() and isupper(txt)) or txt.isnumeric(): + if txt.isalpha() or txt.isnumeric(): + # if (txt.isalpha() and isupper(txt)) or txt.isnumeric(): # get data for each text object detected x = txt_data["left"][i] y = txt_data["top"][i] @@ -148,6 +196,9 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: # add to found characters array found_characters += [(txt, bounds, color)] + ## TODO: convert bound coordinates back to regular image if rotated + + # Draw bounds of detected character for c in found_characters: cv2.line(output_image, c[1][0], c[1][1], (0, 0, 255), thickness=2) cv2.line(output_image, c[1][1], c[1][2], (0, 0, 255), thickness=2) @@ -168,7 +219,12 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: img = cv2.imread( "/home/cameron/Documents/GitHub/SUAS-2022/vision/Object Detection/text_y.jpg" ) + # img = cv2.imread( + # "/home/cameron/Documents/GitHub/SUAS-2022/vision/Object Detection/letter_a.jpg" + # ) + + bounds = [[14, 63], [112, 5], [192, 231], [94, 173]] - detected_chars = detect_text(img) + detected_chars = detect_text(img, bounds) print("The following characters were found in the image:", detected_chars) From 058fbe4dd28f319315a889c25c1339a84af22bc5 Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Tue, 22 Feb 2022 19:31:04 -0600 Subject: [PATCH 32/45] Put functions in class --- vision/Object Detection/standard_obj_text.py | 410 ++++++++++--------- 1 file changed, 210 insertions(+), 200 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index d30fef9..eb35184 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -8,208 +8,216 @@ import pytesseract -def slice_rotate_img(img: np.ndarray, bounds: np.ndarray) -> np.ndarray: +class TextDetection: """ - Slice a portion of an image and rotate to be rectangular. - - Parameters - ---------- - img : np.ndarray - the image to take a splice of - bounds : np.ndarray - array of tuple bounds (4 x-y coordinates; tl-tr-br-bl) - - Returns - ------- - np.ndarray - the spliced/rotated images + Class for handling detection of text on standard objects. """ - # Find center point - min_x = np.amin(bounds[:][0]) - max_x = np.amax(bounds[:][0]) - min_y = np.amin(bounds[:][1]) - max_y = np.amax(bounds[:][1]) - center_pt = (int((max_x + min_x) / 2), int((max_y + min_y) / 2)) - print(center_pt) - # Get angle of rotation - tl_x = bounds[0][0] - tr_x = bounds[1][0] - tl_y = bounds[0][1] - tr_y = bounds[1][1] - angle = np.rad2deg(np.arctan((tr_y - tl_y) / (tr_x - tl_x))) - print(angle) - - # Rotate image - rot_mat = cv2.getRotationMatrix2D(center_pt, angle, 1.0) - rotated_img = cv2.warpAffine(img, rot_mat, img.shape[1::-1], flags=cv2.INTER_LINEAR) - - cv2.imshow("Rotated image", rotated_img) - cv2.waitKey(0) - cv2.destroyAllWindows() - - return rotated_img - - -def get_text_color(img: np.ndarray, bounds: np.ndarray) -> np.ndarray: - """ - Detect the color of the text. - Will return interop_api_pb2.Odlc.%COLOR% for odlc.alphanumeric_color - ## TODO: change to return type to interop enum class - Need way to filter out object color from text color. - - Ideas - ----- - kmeans to filter down to most common color in bounds - - likely to be the color of the text - get average color after kmeans - - Parameters - ---------- - img : np.ndarray - the image the text is in - bounds : np.ndarray - bounds of the text - - Returns - ------- - np.ndarray - the color of the text - """ - # kmeans to get single color - cropped_img = img[bounds[0, 0] : bounds[2, 0], bounds[0, 1] : bounds[2, 1]] - # kmeans_img = cv2.kmeans(cropped_img, K=1,) - - # get average color of detected text - color = np.array( - [ - np.mean(img[:, :, 0]), - np.mean(img[:, :, 1]), - np.mean(img[:, :, 2]), - ] ## TODO: swtich to kmeans image - ) - - # map detected color to available colors in competition - ## TODO: need to get way to correlate to available competition colors - - return color - - -def preprocess_img(img: np.ndarray) -> np.ndarray: - """ - Preprocess image for text detection. - - Parameters - ---------- - img : np.ndarray - image to preprocess - - Returns - ------- - np.ndarray - the image after preprocessing - """ - # grayscale - gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) - - # blur to remove noise - blur = cv2.medianBlur(gray, ksize=9) - - # thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1] - - # erode and dilate to increase text clarity and reduce noise - kernel = np.ones((5, 5), np.uint8) - eroded = cv2.erode(blur, kernel=kernel, iterations=1) - dilated = cv2.dilate(eroded, kernel=kernel, iterations=1) - - laplace_img = cv2.Laplacian(dilated, ddepth=cv2.CV_8U, ksize=5) - - # binarize image - binarized = np.where(laplace_img > 50, np.uint8(255), np.uint8(0)) - print(type(binarized[0][0])) - # print(np.shape(binarized)) - - # edge detection - # edges = cv2.Canny(laplace_img, 100, 200) - return binarized - # return binarized - - -def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: - """ - Detect text within an image. - Will return string for parameter odlc.alphanumeric - - Parameters - ---------- - img : np.ndarray - image to detect text within - bounds : np.ndarray - array of tuple bounds (4 x-y coordinates) - - Returns - ------- - np.ndarray - np.ndarray containing detected characters, format: ([bounds], 'character', color) - - ## TODO: Documentation to be updated later - """ - # correct image if necessary - corrected_img = img - if bounds != None: - corrected_img = slice_rotate_img(img, bounds) - - # Image processing to make text more clear - processed_img = preprocess_img(corrected_img) - - cv2.imshow("Processed Image", processed_img) - cv2.waitKey(0) - cv2.destroyAllWindows() - - # output_image = np.dstack((blurred_img, blurred_img, blurred_img)) - output_image = np.dstack((processed_img, processed_img, processed_img)) - - print("Image processing complete.") - # detect text - txt_data = pytesseract.image_to_data( - output_image, output_type=pytesseract.Output.DICT, lang="eng", config="--psm 10" - ) - print(txt_data) - # filter detected text to find valid characters - found_characters = [] - for i, txt in enumerate(txt_data["text"]): - if (txt != None) and (len(txt) == 1): # length of 1 - # must be uppercase letter or number - if txt.isalpha() or txt.isnumeric(): - # if (txt.isalpha() and isupper(txt)) or txt.isnumeric(): - # get data for each text object detected - x = txt_data["left"][i] - y = txt_data["top"][i] - w = txt_data["width"][i] - h = txt_data["height"][i] - - bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)] - - color = "Green" # TODO: remove - # color = get_text_color(img, bounds) # TODO: uncomment when implemented - - # add to found characters array - found_characters += [(txt, bounds, color)] - - ## TODO: convert bound coordinates back to regular image if rotated - - # Draw bounds of detected character - for c in found_characters: - cv2.line(output_image, c[1][0], c[1][1], (0, 0, 255), thickness=2) - cv2.line(output_image, c[1][1], c[1][2], (0, 0, 255), thickness=2) - cv2.line(output_image, c[1][2], c[1][3], (0, 0, 255), thickness=2) - cv2.line(output_image, c[1][3], c[1][0], (0, 0, 255), thickness=2) - - cv2.imshow("Output", output_image) - cv2.waitKey(0) - cv2.destroyAllWindows() - - return found_characters + def __init__(self): + self.angle = 0 + + def detect_text(self, img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: + """ + Detect text within an image. + Will return string for parameter odlc.alphanumeric + + Parameters + ---------- + img : np.ndarray + image to detect text within + bounds : np.ndarray + array of tuple bounds (4 x-y coordinates) + + Returns + ------- + np.ndarray + np.ndarray containing detected characters, format: ([bounds], 'character', color) + + ## TODO: Documentation to be updated later + """ + # correct image if necessary + corrected_img = img + if bounds != None: + corrected_img = self._slice_rotate_img(img, bounds) + + # Image processing to make text more clear + processed_img = self._preprocess_img(corrected_img) + + cv2.imshow("Processed Image", processed_img) + cv2.waitKey(0) + cv2.destroyAllWindows() + + # output_image = np.dstack((blurred_img, blurred_img, blurred_img)) + output_image = np.dstack((processed_img, processed_img, processed_img)) + + print("Image processing complete.") + # detect text + txt_data = pytesseract.image_to_data( + output_image, + output_type=pytesseract.Output.DICT, + lang="eng", + config="--psm 10", + ) + print(txt_data) + # filter detected text to find valid characters + found_characters = [] + for i, txt in enumerate(txt_data["text"]): + if (txt != None) and (len(txt) == 1): # length of 1 + # must be uppercase letter or number + if txt.isalpha() or txt.isnumeric(): + # if (txt.isalpha() and isupper(txt)) or txt.isnumeric(): + # get data for each text object detected + x = txt_data["left"][i] + y = txt_data["top"][i] + w = txt_data["width"][i] + h = txt_data["height"][i] + + bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)] + + color = "Green" # TODO: remove + # color = get_text_color(img, bounds) # TODO: uncomment when implemented + + # add to found characters array + found_characters += [(txt, bounds, color)] + + ## TODO: convert bound coordinates back to regular image if rotated + + # Draw bounds of detected character + for c in found_characters: + cv2.line(output_image, c[1][0], c[1][1], (0, 0, 255), thickness=2) + cv2.line(output_image, c[1][1], c[1][2], (0, 0, 255), thickness=2) + cv2.line(output_image, c[1][2], c[1][3], (0, 0, 255), thickness=2) + cv2.line(output_image, c[1][3], c[1][0], (0, 0, 255), thickness=2) + + cv2.imshow("Output", output_image) + cv2.waitKey(0) + cv2.destroyAllWindows() + + return found_characters + + def get_text_color(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: + """ + Detect the color of the text. + Will return interop_api_pb2.Odlc.%COLOR% for odlc.alphanumeric_color + ## TODO: change to return type to interop enum class + Need way to filter out object color from text color. + + Ideas + ----- + kmeans to filter down to most common color in bounds + - likely to be the color of the text + get average color after kmeans + + Parameters + ---------- + img : np.ndarray + the image the text is in + bounds : np.ndarray + bounds of the text + + Returns + ------- + np.ndarray + the color of the text + """ + # kmeans to get single color + cropped_img = img[bounds[0, 0] : bounds[2, 0], bounds[0, 1] : bounds[2, 1]] + # kmeans_img = cv2.kmeans(cropped_img, K=1,) + + # get average color of detected text + color = np.array( + [ + np.mean(img[:, :, 0]), + np.mean(img[:, :, 1]), + np.mean(img[:, :, 2]), + ] ## TODO: swtich to kmeans image + ) + + # map detected color to available colors in competition + ## TODO: need to get way to correlate to available competition colors + + return color + + def _slice_rotate_img(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: + """ + Slice a portion of an image and rotate to be rectangular. + + Parameters + ---------- + img : np.ndarray + the image to take a splice of + bounds : np.ndarray + array of tuple bounds (4 x-y coordinates; tl-tr-br-bl) + + Returns + ------- + np.ndarray + the spliced/rotated images + """ + # Find center point + min_x = np.amin(bounds[:][0]) + max_x = np.amax(bounds[:][0]) + min_y = np.amin(bounds[:][1]) + max_y = np.amax(bounds[:][1]) + center_pt = (int((max_x + min_x) / 2), int((max_y + min_y) / 2)) + + # Get angle of rotation + tl_x = bounds[0][0] + tr_x = bounds[1][0] + tl_y = bounds[0][1] + tr_y = bounds[1][1] + angle = np.rad2deg(np.arctan((tr_y - tl_y) / (tr_x - tl_x))) + self.angle = angle + + # Rotate image + rot_mat = cv2.getRotationMatrix2D(center_pt, angle, 1.0) + rotated_img = cv2.warpAffine( + img, rot_mat, img.shape[1::-1], flags=cv2.INTER_LINEAR + ) + + cv2.imshow("Rotated image", rotated_img) + cv2.waitKey(0) + cv2.destroyAllWindows() + + return rotated_img + + def _preprocess_img(self, img: np.ndarray) -> np.ndarray: + """ + Preprocess image for text detection. + + Parameters + ---------- + img : np.ndarray + image to preprocess + + Returns + ------- + np.ndarray + the image after preprocessing + """ + # grayscale + gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) + + # blur to remove noise + blur = cv2.medianBlur(gray, ksize=9) + + # thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1] + + # erode and dilate to increase text clarity and reduce noise + kernel = np.ones((5, 5), np.uint8) + eroded = cv2.erode(blur, kernel=kernel, iterations=1) + dilated = cv2.dilate(eroded, kernel=kernel, iterations=1) + + laplace_img = cv2.Laplacian(dilated, ddepth=cv2.CV_8U, ksize=5) + + # binarize image + binarized = np.where(laplace_img > 50, np.uint8(255), np.uint8(0)) + + # edge detection + # edges = cv2.Canny(laplace_img, 100, 200) + + return binarized + # return binarized if __name__ == "__main__": @@ -225,6 +233,8 @@ def detect_text(img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: bounds = [[14, 63], [112, 5], [192, 231], [94, 173]] - detected_chars = detect_text(img, bounds) + detector = TextDetection() + + detected_chars = detector.detect_text(img, bounds) print("The following characters were found in the image:", detected_chars) From 089acfdf2b3870169e6e3c16b2ffc4def1923c88 Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Tue, 22 Feb 2022 19:52:20 -0600 Subject: [PATCH 33/45] Add check for full-image text --- vision/Object Detection/standard_obj_text.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index eb35184..c2e8cc9 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -72,13 +72,18 @@ def detect_text(self, img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: w = txt_data["width"][i] h = txt_data["height"][i] - bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)] + # Don't continue processing if text is size of full image + img_h, img_w = np.shape(processed_img) + if not (x == 0 and y == 0 and w == img_w and h == img_h): + print("Full-image text") - color = "Green" # TODO: remove - # color = get_text_color(img, bounds) # TODO: uncomment when implemented + bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)] - # add to found characters array - found_characters += [(txt, bounds, color)] + color = "Green" # TODO: remove + # color = get_text_color(img, bounds) # TODO: uncomment when implemented + + # add to found characters array + found_characters += [(txt, bounds, color)] ## TODO: convert bound coordinates back to regular image if rotated @@ -213,10 +218,12 @@ def _preprocess_img(self, img: np.ndarray) -> np.ndarray: # binarize image binarized = np.where(laplace_img > 50, np.uint8(255), np.uint8(0)) + blur_2 = cv2.medianBlur(binarized, ksize=3) + # edge detection # edges = cv2.Canny(laplace_img, 100, 200) - return binarized + return blur_2 # return binarized From 970e16f9710bde72f3b536217d5fecf003fe6802 Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Tue, 1 Mar 2022 19:20:51 -0600 Subject: [PATCH 34/45] Fix crop/rotation, cleanup --- vision/Object Detection/standard_obj_text.py | 93 ++++++++------------ 1 file changed, 36 insertions(+), 57 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index c2e8cc9..2436c3b 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -1,9 +1,7 @@ """ Functions related to detecting the text of standard objects. """ -from curses.ascii import isupper import cv2 -from matplotlib.pyplot import get import numpy as np import pytesseract @@ -14,7 +12,7 @@ class TextDetection: """ def __init__(self): - self.angle = 0 + self.image = np.array([]) def detect_text(self, img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: """ @@ -35,6 +33,10 @@ def detect_text(self, img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: ## TODO: Documentation to be updated later """ + cv2.imshow("Original Image", img) ## TODO: remove all imshows + cv2.waitKey(0) + cv2.destroyAllWindows() + # correct image if necessary corrected_img = img if bounds != None: @@ -50,8 +52,8 @@ def detect_text(self, img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: # output_image = np.dstack((blurred_img, blurred_img, blurred_img)) output_image = np.dstack((processed_img, processed_img, processed_img)) - print("Image processing complete.") # detect text + print("Image processing complete.") txt_data = pytesseract.image_to_data( output_image, output_type=pytesseract.Output.DICT, @@ -64,8 +66,7 @@ def detect_text(self, img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: for i, txt in enumerate(txt_data["text"]): if (txt != None) and (len(txt) == 1): # length of 1 # must be uppercase letter or number - if txt.isalpha() or txt.isnumeric(): - # if (txt.isalpha() and isupper(txt)) or txt.isnumeric(): + if (txt.isalpha() and txt.isupper()) or txt.isnumeric(): # get data for each text object detected x = txt_data["left"][i] y = txt_data["top"][i] @@ -75,18 +76,13 @@ def detect_text(self, img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: # Don't continue processing if text is size of full image img_h, img_w = np.shape(processed_img) if not (x == 0 and y == 0 and w == img_w and h == img_h): - print("Full-image text") - bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)] - color = "Green" # TODO: remove - # color = get_text_color(img, bounds) # TODO: uncomment when implemented + color = self._get_text_color(img, bounds) # add to found characters array found_characters += [(txt, bounds, color)] - ## TODO: convert bound coordinates back to regular image if rotated - # Draw bounds of detected character for c in found_characters: cv2.line(output_image, c[1][0], c[1][1], (0, 0, 255), thickness=2) @@ -100,18 +96,9 @@ def detect_text(self, img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: return found_characters - def get_text_color(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: + def _get_text_color(self, img: np.ndarray, bounds: np.ndarray) -> str: """ Detect the color of the text. - Will return interop_api_pb2.Odlc.%COLOR% for odlc.alphanumeric_color - ## TODO: change to return type to interop enum class - Need way to filter out object color from text color. - - Ideas - ----- - kmeans to filter down to most common color in bounds - - likely to be the color of the text - get average color after kmeans Parameters ---------- @@ -122,24 +109,11 @@ def get_text_color(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: Returns ------- - np.ndarray + str the color of the text """ - # kmeans to get single color - cropped_img = img[bounds[0, 0] : bounds[2, 0], bounds[0, 1] : bounds[2, 1]] - # kmeans_img = cv2.kmeans(cropped_img, K=1,) - - # get average color of detected text - color = np.array( - [ - np.mean(img[:, :, 0]), - np.mean(img[:, :, 1]), - np.mean(img[:, :, 2]), - ] ## TODO: swtich to kmeans image - ) - - # map detected color to available colors in competition - ## TODO: need to get way to correlate to available competition colors + ## TEMP: Initial Code to provide broad outline, out of scope of current issue + color = "Green" return color @@ -159,25 +133,38 @@ def _slice_rotate_img(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: np.ndarray the spliced/rotated images """ - # Find center point - min_x = np.amin(bounds[:][0]) - max_x = np.amax(bounds[:][0]) - min_y = np.amin(bounds[:][1]) - max_y = np.amax(bounds[:][1]) - center_pt = (int((max_x + min_x) / 2), int((max_y + min_y) / 2)) + # Slice image around bounds and find center point + x_vals = [coord[0] for coord in bounds] + y_vals = [coord[1] for coord in bounds] + min_x = np.amin(x_vals) + max_x = np.amax(x_vals) + min_y = np.amin(y_vals) + max_y = np.amax(y_vals) + + cropped_img = img[min_x:max_x][min_y:max_y][:] + + dimensions = np.shape(cropped_img) + center_pt = ( + int(dimensions[0] / 2), + int( + dimensions[1] / 2, + ), + ) # Get angle of rotation tl_x = bounds[0][0] - tr_x = bounds[1][0] + tr_x = bounds[3][ + 0 + ] ## TODO: 1st index depends on how bounds stored for standard object tl_y = bounds[0][1] - tr_y = bounds[1][1] + tr_y = bounds[3][1] angle = np.rad2deg(np.arctan((tr_y - tl_y) / (tr_x - tl_x))) self.angle = angle # Rotate image rot_mat = cv2.getRotationMatrix2D(center_pt, angle, 1.0) rotated_img = cv2.warpAffine( - img, rot_mat, img.shape[1::-1], flags=cv2.INTER_LINEAR + cropped_img, rot_mat, cropped_img.shape[1::-1], flags=cv2.INTER_LINEAR ) cv2.imshow("Rotated image", rotated_img) @@ -206,8 +193,6 @@ def _preprocess_img(self, img: np.ndarray) -> np.ndarray: # blur to remove noise blur = cv2.medianBlur(gray, ksize=9) - # thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1] - # erode and dilate to increase text clarity and reduce noise kernel = np.ones((5, 5), np.uint8) eroded = cv2.erode(blur, kernel=kernel, iterations=1) @@ -218,13 +203,10 @@ def _preprocess_img(self, img: np.ndarray) -> np.ndarray: # binarize image binarized = np.where(laplace_img > 50, np.uint8(255), np.uint8(0)) + # Additional blur to remove noise blur_2 = cv2.medianBlur(binarized, ksize=3) - # edge detection - # edges = cv2.Canny(laplace_img, 100, 200) - return blur_2 - # return binarized if __name__ == "__main__": @@ -234,11 +216,8 @@ def _preprocess_img(self, img: np.ndarray) -> np.ndarray: img = cv2.imread( "/home/cameron/Documents/GitHub/SUAS-2022/vision/Object Detection/text_y.jpg" ) - # img = cv2.imread( - # "/home/cameron/Documents/GitHub/SUAS-2022/vision/Object Detection/letter_a.jpg" - # ) - bounds = [[14, 63], [112, 5], [192, 231], [94, 173]] + bounds = [[77, 184], [3, 91], [120, 0], [194, 82]] detector = TextDetection() From ee9b833fe12c6c76eb8176beab102bd85345e8c4 Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Tue, 1 Mar 2022 19:41:19 -0600 Subject: [PATCH 35/45] Add get_text_characteristics, cleanup --- vision/Object Detection/standard_obj_text.py | 106 ++++++++++--------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index 2436c3b..e686a2a 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -6,15 +6,41 @@ import pytesseract -class TextDetection: +class TextCharacteristics: """ - Class for handling detection of text on standard objects. + Class for detecting characteristics of text on standard objects. """ def __init__(self): self.image = np.array([]) - def detect_text(self, img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: + def get_text_characteristics(self, img: np.ndarray, bounds: np.ndarray) -> tuple: + """ + Gets the characteristics of the text on the standard object. + + Parameters + ---------- + img : np.ndarray + image to find characteristics of text within + bounds : np.ndarray + bounds of standard object containing text on in image + + Returns + ------- + tuple + characteristics of the text in the form (character, orientation, color) + """ + characters = self._detect_text(img, bounds) + if len(characters) != 1: + return (None, None, None) + character, char_bounds = characters[0] + + orientation = self._get_orientation(img, char_bounds) + color = self._get_text_color(img, char_bounds) + + return (character, orientation, color) + + def _detect_text(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: """ Detect text within an image. Will return string for parameter odlc.alphanumeric @@ -30,29 +56,15 @@ def detect_text(self, img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: ------- np.ndarray np.ndarray containing detected characters, format: ([bounds], 'character', color) - - ## TODO: Documentation to be updated later """ - cv2.imshow("Original Image", img) ## TODO: remove all imshows - cv2.waitKey(0) - cv2.destroyAllWindows() - # correct image if necessary - corrected_img = img - if bounds != None: - corrected_img = self._slice_rotate_img(img, bounds) - - # Image processing to make text more clear - processed_img = self._preprocess_img(corrected_img) - - cv2.imshow("Processed Image", processed_img) - cv2.waitKey(0) - cv2.destroyAllWindows() + rotated_img = self._slice_rotate_img(img, bounds) - # output_image = np.dstack((blurred_img, blurred_img, blurred_img)) + ## Image preprocessing to make text more clear ## + processed_img = self._preprocess_img(rotated_img) output_image = np.dstack((processed_img, processed_img, processed_img)) - # detect text + ## Detect Text ## print("Image processing complete.") txt_data = pytesseract.image_to_data( output_image, @@ -60,8 +72,8 @@ def detect_text(self, img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: lang="eng", config="--psm 10", ) - print(txt_data) - # filter detected text to find valid characters + + ## Filter detected text to find valid characters ## found_characters = [] for i, txt in enumerate(txt_data["text"]): if (txt != None) and (len(txt) == 1): # length of 1 @@ -78,21 +90,8 @@ def detect_text(self, img: np.ndarray, bounds: np.ndarray = None) -> np.ndarray: if not (x == 0 and y == 0 and w == img_w and h == img_h): bounds = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)] - color = self._get_text_color(img, bounds) - # add to found characters array - found_characters += [(txt, bounds, color)] - - # Draw bounds of detected character - for c in found_characters: - cv2.line(output_image, c[1][0], c[1][1], (0, 0, 255), thickness=2) - cv2.line(output_image, c[1][1], c[1][2], (0, 0, 255), thickness=2) - cv2.line(output_image, c[1][2], c[1][3], (0, 0, 255), thickness=2) - cv2.line(output_image, c[1][3], c[1][0], (0, 0, 255), thickness=2) - - cv2.imshow("Output", output_image) - cv2.waitKey(0) - cv2.destroyAllWindows() + found_characters += [(txt, bounds)] return found_characters @@ -112,11 +111,27 @@ def _get_text_color(self, img: np.ndarray, bounds: np.ndarray) -> str: str the color of the text """ - ## TEMP: Initial Code to provide broad outline, out of scope of current issue + ## TEMP: Implemmentation out of scope of current issue color = "Green" return color + def _get_orientation(self, img: np.ndarray, bounds: np.ndarray) -> str: + """ + Get the orientation of the text. + + Parameters + ---------- + img : np.ndarray + the image the text is in + bounds : np.ndarray + bounds of the text + """ + ## TEMP: Implemmentation out of scope of current issue + orientation = "N" + + return orientation + def _slice_rotate_img(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: """ Slice a portion of an image and rotate to be rectangular. @@ -152,10 +167,9 @@ def _slice_rotate_img(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: ) # Get angle of rotation + ## TODO: 1st index depends on how bounds stored for standard object tl_x = bounds[0][0] - tr_x = bounds[3][ - 0 - ] ## TODO: 1st index depends on how bounds stored for standard object + tr_x = bounds[3][0] tl_y = bounds[0][1] tr_y = bounds[3][1] angle = np.rad2deg(np.arctan((tr_y - tl_y) / (tr_x - tl_x))) @@ -167,10 +181,6 @@ def _slice_rotate_img(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: cropped_img, rot_mat, cropped_img.shape[1::-1], flags=cv2.INTER_LINEAR ) - cv2.imshow("Rotated image", rotated_img) - cv2.waitKey(0) - cv2.destroyAllWindows() - return rotated_img def _preprocess_img(self, img: np.ndarray) -> np.ndarray: @@ -219,8 +229,8 @@ def _preprocess_img(self, img: np.ndarray) -> np.ndarray: bounds = [[77, 184], [3, 91], [120, 0], [194, 82]] - detector = TextDetection() + detector = TextCharacteristics() - detected_chars = detector.detect_text(img, bounds) + detected_chars = detector.get_text_characteristics(img, bounds) - print("The following characters were found in the image:", detected_chars) + print("The following character was found in the image:", detected_chars) From 322595d219d12166d79b289437fd8a34c989e27a Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Tue, 1 Mar 2022 19:49:50 -0600 Subject: [PATCH 36/45] Cleanup and documentation updates --- vision/Object Detection/standard_obj_text.py | 28 +++++++++++--------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index e686a2a..3a2c2dd 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -1,6 +1,7 @@ """ -Functions related to detecting the text of standard objects. +Algorithms related to detecting the text of standard objects. """ + import cv2 import numpy as np import pytesseract @@ -9,6 +10,7 @@ class TextCharacteristics: """ Class for detecting characteristics of text on standard objects. + Characteristics consist of the character, orientation, and color. """ def __init__(self): @@ -30,17 +32,21 @@ def get_text_characteristics(self, img: np.ndarray, bounds: np.ndarray) -> tuple tuple characteristics of the text in the form (character, orientation, color) """ + ## Get the character ## characters = self._detect_text(img, bounds) if len(characters) != 1: return (None, None, None) character, char_bounds = characters[0] + ## Get the orientation ## orientation = self._get_orientation(img, char_bounds) + + ## Get the color of the text ## color = self._get_text_color(img, char_bounds) return (character, orientation, color) - def _detect_text(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: + def _detect_text(self, img: np.ndarray, bounds: np.ndarray) -> list: """ Detect text within an image. Will return string for parameter odlc.alphanumeric @@ -54,10 +60,10 @@ def _detect_text(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: Returns ------- - np.ndarray - np.ndarray containing detected characters, format: ([bounds], 'character', color) + list + list containing detected characters and their bounds, Format of characters is ([bounds], 'character'). """ - # correct image if necessary + ## Crop and rotate the image ## rotated_img = self._slice_rotate_img(img, bounds) ## Image preprocessing to make text more clear ## @@ -148,7 +154,7 @@ def _slice_rotate_img(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: np.ndarray the spliced/rotated images """ - # Slice image around bounds and find center point + ## Slice image around bounds and find center point ## x_vals = [coord[0] for coord in bounds] y_vals = [coord[1] for coord in bounds] min_x = np.amin(x_vals) @@ -166,16 +172,15 @@ def _slice_rotate_img(self, img: np.ndarray, bounds: np.ndarray) -> np.ndarray: ), ) - # Get angle of rotation + ## Get angle of rotation ## ## TODO: 1st index depends on how bounds stored for standard object tl_x = bounds[0][0] tr_x = bounds[3][0] tl_y = bounds[0][1] tr_y = bounds[3][1] angle = np.rad2deg(np.arctan((tr_y - tl_y) / (tr_x - tl_x))) - self.angle = angle - # Rotate image + ## Rotate image ## rot_mat = cv2.getRotationMatrix2D(center_pt, angle, 1.0) rotated_img = cv2.warpAffine( cropped_img, rot_mat, cropped_img.shape[1::-1], flags=cv2.INTER_LINEAR @@ -208,12 +213,13 @@ def _preprocess_img(self, img: np.ndarray) -> np.ndarray: eroded = cv2.erode(blur, kernel=kernel, iterations=1) dilated = cv2.dilate(eroded, kernel=kernel, iterations=1) + # laplace edge detection laplace_img = cv2.Laplacian(dilated, ddepth=cv2.CV_8U, ksize=5) # binarize image binarized = np.where(laplace_img > 50, np.uint8(255), np.uint8(0)) - # Additional blur to remove noise + # additional blur to remove noise blur_2 = cv2.medianBlur(binarized, ksize=3) return blur_2 @@ -226,11 +232,9 @@ def _preprocess_img(self, img: np.ndarray) -> np.ndarray: img = cv2.imread( "/home/cameron/Documents/GitHub/SUAS-2022/vision/Object Detection/text_y.jpg" ) - bounds = [[77, 184], [3, 91], [120, 0], [194, 82]] detector = TextCharacteristics() - detected_chars = detector.get_text_characteristics(img, bounds) print("The following character was found in the image:", detected_chars) From 25f192775aca29a8485e8c7d754e29104d88f338 Mon Sep 17 00:00:00 2001 From: Cameron Falls Date: Thu, 3 Mar 2022 18:19:22 -0600 Subject: [PATCH 37/45] Add command line args --- vision/Object Detection/standard_obj_text.py | 25 ++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/vision/Object Detection/standard_obj_text.py b/vision/Object Detection/standard_obj_text.py index 3a2c2dd..1f20ff7 100644 --- a/vision/Object Detection/standard_obj_text.py +++ b/vision/Object Detection/standard_obj_text.py @@ -229,9 +229,30 @@ def _preprocess_img(self, img: np.ndarray) -> np.ndarray: """ Driver for testing text detection and classification functions. """ - img = cv2.imread( - "/home/cameron/Documents/GitHub/SUAS-2022/vision/Object Detection/text_y.jpg" + import argparse + + parser = argparse.ArgumentParser( + description="Runs text characteristics algorithms. Must specify a file." + ) + + parser.add_argument( + "-f", + "--file_name", + type=str, + help="Filename of the image. Required argument.", ) + + args = parser.parse_args() + + # no benchmark name specified, cannot continue + if not args.file_name: + raise RuntimeError("No file specified.") + file_name = args.file_name + + img = cv2.imread(file_name) + + # bounds for stock image, given by standard object detection + ## TODO: Change to function once implemented bounds = [[77, 184], [3, 91], [120, 0], [194, 82]] detector = TextCharacteristics() From a1961b3a58ae132833f58c870591db441573b80f Mon Sep 17 00:00:00 2001 From: Christopher Spencer Date: Thu, 3 Mar 2022 21:18:46 -0600 Subject: [PATCH 38/45] Initial obstacle avoidance code --- flight/Stationary Obstacle Avoidance/main.py | 68 +++++ .../math_functions.py | 113 +++++++++ .../obstacle_avoidance.py | 238 ++++++++++++++++++ .../Stationary Obstacle Avoidance/plotter.py | 133 ++++++++++ .../sample_data.json | 96 +++++++ 5 files changed, 648 insertions(+) create mode 100644 flight/Stationary Obstacle Avoidance/main.py create mode 100644 flight/Stationary Obstacle Avoidance/math_functions.py create mode 100644 flight/Stationary Obstacle Avoidance/obstacle_avoidance.py create mode 100644 flight/Stationary Obstacle Avoidance/plotter.py create mode 100644 flight/Stationary Obstacle Avoidance/sample_data.json diff --git a/flight/Stationary Obstacle Avoidance/main.py b/flight/Stationary Obstacle Avoidance/main.py new file mode 100644 index 0000000..f09ff7f --- /dev/null +++ b/flight/Stationary Obstacle Avoidance/main.py @@ -0,0 +1,68 @@ +import obstacle_avoidance +import plotter + +SAFETY_MARGIN = 10 # meters +MAX_DISTANCE = 0 # meters + +waypoints = [ + {"latitude": 37.94883876837826, "longitude": -91.78443677093598, "altitude": 100.0}, + { + "latitude": 37.949016435151485, + "longitude": -91.78364283711778, + "altitude": 150.0, + }, + {"latitude": 37.94957481364174, "longitude": -91.78369648129468, "altitude": 300.0}, + {"latitude": 37.95004012580848, "longitude": -91.78468353414975, "altitude": 250.0}, + { + "latitude": 37.949735558177984, + "longitude": -91.78542382379104, + "altitude": 150.0, + }, + { + "latitude": 37.948790418686826, + "longitude": -91.78605823636349, + "altitude": 200.0, + }, + { + "latitude": 37.948576497594445, + "longitude": -91.78460843230208, + "altitude": 150.0, + }, +] + +obstacles = [ + { + "latitude": 37.94986244280119, + "longitude": -91.78386816205061, + "radius": 20, + "height": 400, + }, + { + "latitude": 37.949388706571064, + "longitude": -91.78569200437985, + "radius": 35, + "height": 200, + }, + { + "latitude": 37.94828884469052, + "longitude": -91.78546673913256, + "radius": 50, + "height": 150, + }, +] + +if __name__ == "__main__": + # Convert latlon data to UTM projection + waypoints = obstacle_avoidance.all_latlon_to_utm(waypoints) + obstacles = obstacle_avoidance.all_latlon_to_utm(obstacles) + + # Plot data before processing + # plotter.plot_data(waypoints, obstacles, SAFETY_MARGIN, flight_path_color='ko-') + + # Find new safe path between all waypoints + waypoints = obstacle_avoidance.get_safe_route( + waypoints, obstacles, SAFETY_MARGIN, MAX_DISTANCE, debugging=False + ) + + # Plot data after processing + plotter.plot_data(waypoints, obstacles, SAFETY_MARGIN) diff --git a/flight/Stationary Obstacle Avoidance/math_functions.py b/flight/Stationary Obstacle Avoidance/math_functions.py new file mode 100644 index 0000000..2fef860 --- /dev/null +++ b/flight/Stationary Obstacle Avoidance/math_functions.py @@ -0,0 +1,113 @@ +import math +from typing import List, Tuple + + +def distance(a: Tuple[int], b: Tuple[int]) -> float: + """Calculates the euclidean distance between two points + + Args: + a (Tuple[int]): x,y coordinates for point a + b (Tuple[int]): x,y coordinates for point b + + Returns: + float: distance between points a and b + """ + return abs(math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2)) + + +# Takes a point and returns the two points that are tangent to a circle of radius r +# src: https://math.stackexchange.com/a/3190374 +def tangent_points(point, circle, r): + Px = point[0] + Py = point[1] + Cx = circle[0] + Cy = circle[1] + dx, dy = Px - Cx, Py - Cy + dxr, dyr = -dy, dx + d = math.sqrt(dx**2 + dy**2) + rho = r / d + ad = rho**2 + bd = rho * math.sqrt(1 - rho**2) + T1x = Cx + ad * dx + bd * dxr + T1y = Cy + ad * dy + bd * dyr + T2x = Cx + ad * dx - bd * dxr + T2y = Cy + ad * dy - bd * dyr + return ((T1x, T1y), (T2x, T2y)) + + +# Returns the equation of a line given two points +# src: https://stackoverflow.com/a/20679579 +def line(p1, p2): + A = p1[1] - p2[1] + B = p2[0] - p1[0] + C = p1[0] * p2[1] - p2[0] * p1[1] + return A, B, -C + + +# Finds the intersection of two lines +# src: https://stackoverflow.com/a/20679579 +def intersection(L1, L2): + D = L1[0] * L2[1] - L1[1] * L2[0] + Dx = L1[2] * L2[1] - L1[1] * L2[2] + Dy = L1[0] * L2[2] - L1[2] * L2[0] + if D != 0: + x = Dx / D + y = Dy / D + return x, y + else: + return False + + +# Returns a circle with a center and a radius given 3 points on the edge +def find_circle(b, c, d): + temp = c[0] ** 2 + c[1] ** 2 + bc = (b[0] ** 2 + b[1] ** 2 - temp) / 2 + cd = (temp - d[0] ** 2 - d[1] ** 2) / 2 + det = (b[0] - c[0]) * (c[1] - d[1]) - (c[0] - d[0]) * (b[1] - c[1]) + + if abs(det) < 1.0e-10: + return None + + # Center of circle + cx = (bc * (c[1] - d[1]) - cd * (b[1] - c[1])) / det + cy = ((b[0] - c[0]) * cd - (c[0] - d[0]) * bc) / det + + radius = ((cx - b[0]) ** 2 + (cy - b[1]) ** 2) ** 0.5 + + return ((cx, cy), radius) + + +# Calculates which pair of tangent points are on the same side of the circle +# returns line segments between the start and end points and the appropriate tangents +def resolve_closest_tangents(start, end, A1, A2, B1, B2): + distances = [distance(A1, B1), distance(A1, B2), distance(A2, B1), distance(A2, B2)] + i = distances.index(min(distances)) + + if i == 0: + # then A1, B1 are on the same side, A2, B2 on the other + route1_segment1 = line(start, A1) + route1_segment2 = line(B1, end) + route2_segment1 = line(start, A2) + route2_segment2 = line(B2, end) + elif i == 1: + # then A1, B2 are on the same side, A2, B1 on the other + route1_segment1 = line(start, A1) + route1_segment2 = line(B2, end) + route2_segment1 = line(start, A2) + route2_segment2 = line(B1, end) + elif i == 2: + # then A2, B1 are on the same side, A1, B2 on the other + route1_segment1 = line(start, A2) + route1_segment2 = line(B1, end) + route2_segment1 = line(start, A1) + route2_segment2 = line(B2, end) + elif i == 3: + # then A2, B2 are on the same side, A1, B1 on the other + route1_segment1 = line(start, A2) + route1_segment2 = line(B2, end) + route2_segment1 = line(start, A1) + route2_segment2 = line(B1, end) + else: + print("Error: index was " + str(i) + ", expected index range 0-3.") + + return route1_segment1, route1_segment2, route2_segment1, route2_segment2 diff --git a/flight/Stationary Obstacle Avoidance/obstacle_avoidance.py b/flight/Stationary Obstacle Avoidance/obstacle_avoidance.py new file mode 100644 index 0000000..ff531af --- /dev/null +++ b/flight/Stationary Obstacle Avoidance/obstacle_avoidance.py @@ -0,0 +1,238 @@ +from typing import List, Tuple +from collections import namedtuple +import utm +from fractions import Fraction +from shapely.geometry import LineString +from shapely.geometry import Point +import math_functions +import plotter + + +def latlon_to_utm(coords): + utm_coords = utm.from_latlon(coords["latitude"], coords["longitude"]) + coords["utm_x"] = utm_coords[0] + coords["utm_y"] = utm_coords[1] + coords["utm_zone_number"] = utm_coords[2] + coords["utm_zone_letter"] = utm_coords[3] + return coords + + +def all_latlon_to_utm(list_of_coords): + for i in range(len(list_of_coords)): + list_of_coords[i] = latlon_to_utm(list_of_coords[i]) + return list_of_coords + + +def check_for_collision(p1, p2, obstacles, padding): + for i, obstacle in enumerate(obstacles): + x = obstacle["utm_x"] + y = obstacle["utm_y"] + radius = obstacle["radius"] + line = LineString([(p1[0], p1[1]), (p2[0], p2[1])]) + circle = ( + Point(x, y).buffer(radius + padding).boundary + ) # https://stackoverflow.com/a/30998492 + if circle.intersection(line): + return i + else: + return None + + +def find_new_point( + pointA, pointB, obstacle, radius, padding, max_distance=0, debugging=False +) -> tuple: + # rough calculation of new height + new_altitude = (pointA[2] + pointB[2]) / 2 + + # find tangents to the circle from points A and B + (tangentA1, tangentA2) = math_functions.tangent_points( + pointA, obstacle, radius + padding + ) + (tangentB1, tangentB2) = math_functions.tangent_points( + pointB, obstacle, radius + padding + ) + + # get lines between points A, B, and their respective tangents to the circle + ( + route1_segment1, + route1_segment2, + route2_segment1, + route2_segment2, + ) = math_functions.resolve_closest_tangents( + pointA, pointB, tangentA1, tangentA2, tangentB1, tangentB2 + ) + + # find intersection points between lines + route1X, route1Y = math_functions.intersection(route1_segment1, route1_segment2) + route2X, route2Y = math_functions.intersection(route2_segment1, route2_segment2) + + # calculate distance of each route on either side of circle + route1_distance = math_functions.distance( + pointA, (route1X, route1Y) + ) + math_functions.distance(pointB, (route1X, route1Y)) + route2_distance = math_functions.distance( + pointA, (route2X, route2Y) + ) + math_functions.distance(pointB, (route2X, route2Y)) + + # choose shortest path + if route1_distance < route2_distance: + new_point = (route1X, route1Y) + alt_point = (route2X, route2Y) + else: + new_point = (route2X, route2Y) + alt_point = (route1X, route1Y) + + # Handle scenario where new waypoint is too far from the circle + special_case = False + # temp_x, extra1X, extra2X, temp_y, extra1Y, extra2Y = 0,0,0,0,0,0 # init vars + waypoint_distance_from_circle = math_functions.distance( + (new_point[0], new_point[1]), obstacle + ) + if waypoint_distance_from_circle > radius + padding + max_distance: + special_case = True + + print("New waypoint is too far away, splitting and adding more waypoints") + # https://math.stackexchange.com/a/1630886 + t = (radius + padding) / waypoint_distance_from_circle + temp_x = ((1 - t) * obstacle[0]) + (t * new_point[0]) + temp_y = ((1 - t) * obstacle[1]) + (t * new_point[1]) + temp_point = (temp_x, temp_y) + + first_segment = math_functions.line( + (pointA[0], pointA[1]), (new_point[0], new_point[1]) + ) + second_segment = math_functions.line( + (pointB[0], pointB[1]), (new_point[0], new_point[1]) + ) + + m = -1 / ( + (temp_y - obstacle[1]) / (temp_x - obstacle[0]) + ) # slope of new middle line segment + m_frac = Fraction(str(m)) + slopeX = m_frac.denominator + slopeY = m_frac.numerator + middle_segment = math_functions.line( + (temp_x, temp_y), (temp_x + slopeX, temp_y + slopeY) + ) + + extra1X, extra1Y = math_functions.intersection(first_segment, middle_segment) + extra2X, extra2Y = math_functions.intersection(second_segment, middle_segment) + extra_1 = (extra1X, extra1Y) + extra_2 = (extra2X, extra2Y) + + # plot extra data about this specific path and obstacle + if debugging: + plotter.plot_debug( + pointA, + pointB, + obstacle, + tangentA1, + tangentA2, + tangentB1, + tangentB2, + new_point, + alt_point, + temp_point, + extra_1, + extra_2, + special_case, + padding, + ) + + zone_number = pointA[3] + zone_letter = pointA[4] + + if special_case: + return [ + { + "utm_x": extra_1[0], + "utm_y": extra_1[1], + "utm_zone_number": zone_number, + "utm_zone_letter": zone_letter, + "latitude": utm.to_latlon(*extra_1, zone_number, zone_letter)[0], + "longitude": utm.to_latlon(*extra_1, zone_number, zone_letter)[1], + "altitude": new_altitude, + }, + { + "utm_x": extra_2[0], + "utm_y": extra_2[1], + "utm_zone_number": zone_number, + "utm_zone_letter": zone_letter, + "latitude": utm.to_latlon(*extra_2, zone_number, zone_letter)[0], + "longitude": utm.to_latlon(*extra_2, zone_number, zone_letter)[1], + "altitude": new_altitude, + }, + ] + else: + return [ + { + "utm_x": new_point[0], + "utm_y": new_point[1], + "utm_zone_number": zone_number, + "utm_zone_letter": zone_letter, + "latitude": utm.to_latlon(*new_point, zone_number, zone_letter)[0], + "longitude": utm.to_latlon(*new_point, zone_number, zone_letter)[1], + "altitude": new_altitude, + } + ] + + +def get_safe_route(waypoints, obstacles, padding, max_distance, debugging=False): + i = 0 + num_waypoints = len(waypoints) - 1 + while i < num_waypoints: + point_a = ( + waypoints[i]["utm_x"], + waypoints[i]["utm_y"], + waypoints[i]["altitude"], + waypoints[i]["utm_zone_number"], + waypoints[i]["utm_zone_letter"], + ) + point_b = ( + waypoints[i + 1]["utm_x"], + waypoints[i + 1]["utm_y"], + waypoints[i + 1]["altitude"], + waypoints[i + 1]["utm_zone_number"], + waypoints[i + 1]["utm_zone_letter"], + ) + + j = check_for_collision(point_a, point_b, obstacles, padding) + + if j != None: + print( + "Path between waypoints " + + str(i) + + " and " + + str(i + 1) + + " intersect obstacle " + + str(j) + ) + obstacle = ( + obstacles[j]["utm_x"], + obstacles[j]["utm_y"], + obstacles[j]["radius"], + obstacles[j]["height"], + ) + data = find_new_point( + point_a, + point_b, + obstacle, + obstacle[2], + padding, + max_distance, + debugging, + ) + # insert between waypoints + if len(data) == 2: + waypoints.insert(i + 1, data[0]) + waypoints.insert(i + 2, data[1]) + num_waypoints += 2 # increase number of waypoints + i -= 2 # back up two waypoint in case path to new waypoint causes new conflicts + else: + waypoints.insert(i + 1, data[0]) + num_waypoints += 1 # increase number of waypoints + i -= 1 # back up one waypoint in case path to new waypoint causes new conflicts + + i += 1 + + return waypoints diff --git a/flight/Stationary Obstacle Avoidance/plotter.py b/flight/Stationary Obstacle Avoidance/plotter.py new file mode 100644 index 0000000..749b563 --- /dev/null +++ b/flight/Stationary Obstacle Avoidance/plotter.py @@ -0,0 +1,133 @@ +import matplotlib.pyplot as plt + + +def plot_data(waypoints, obstacles, padding, flight_path_color="bo-"): + # plot obstacles + for obstacle in obstacles: + x = obstacle["utm_x"] + y = obstacle["utm_y"] + radius = obstacle["radius"] + + plt.gca().add_patch( + plt.Circle((x, y), radius + padding, color="gray") + ) # padded obstacle + plt.gca().add_patch(plt.Circle((x, y), radius, color="red")) # true obstacle + + # plot waypoints + x, y = [], [] + for waypoint in waypoints: + x.append(waypoint["utm_x"]) + y.append(waypoint["utm_y"]) + plt.plot(x, y, flight_path_color) + + plt.gca().set_aspect(1) + plt.show() + + +def plot_debug( + pointA, + pointB, + obstacle, + tangentA1, + tangentA2, + tangentB1, + tangentB2, + new_point, + alt_point, + temp_point, + extra_1, + extra_2, + special_case, + padding, +): + radius = obstacle[2] + + # plot obstacle + plt.gca().add_patch( + plt.Circle((obstacle[0], obstacle[1]), radius + padding, color="gray") + ) # padded obstacle + plt.gca().add_patch( + plt.Circle((obstacle[0], obstacle[1]), radius, color="red") + ) # true obstacle + + # prepare point data + x = [ + pointA[0], + pointB[0], + obstacle[0], + tangentA1[0], + tangentA2[0], + tangentB1[0], + tangentB2[0], + new_point[0], + alt_point[0], + temp_point[0], + extra_1[0], + extra_2[0], + ] + y = [ + pointA[1], + pointB[1], + obstacle[1], + tangentA1[1], + tangentA2[1], + tangentB1[1], + tangentB2[1], + new_point[1], + alt_point[1], + temp_point[1], + extra_1[1], + extra_2[1], + ] + col = [ + "green", + "green", + "black", + "black", + "black", + "black", + "black", + "blue", + "blue", + "blue", + "blue", + "blue", + ] + txt = [ + "Start", + "End", + "Obstacle", + "A1", + "A2", + "B1", + "B2", + "New Waypoint", + "Alt Waypoint", + "Temp", + "Extra 1", + "Extra 2", + ] + + # plot points + for i in range(len(x)): + plt.scatter(x[i], y[i], c=col[i]) + + # plot text labels + for i, v in enumerate(txt): + plt.gca().annotate(v, (x[i], y[i])) + + if special_case: + plt.plot( + [pointA[0], extra_1[0], extra_2[0], pointB[0]], + [pointA[1], extra_1[1], extra_2[1], pointB[1]], + "go-", + ) + else: + plt.plot( + [pointA[0], new_point[0], pointB[0]], + [pointA[1], new_point[1], pointB[1]], + "go-", + ) + + plt.gca().set_aspect(1) + plt.show() diff --git a/flight/Stationary Obstacle Avoidance/sample_data.json b/flight/Stationary Obstacle Avoidance/sample_data.json new file mode 100644 index 0000000..5c47857 --- /dev/null +++ b/flight/Stationary Obstacle Avoidance/sample_data.json @@ -0,0 +1,96 @@ +{ + "id": 1, + "lostCommsPos": { + "latitude": 37.9490953, + "longitude": -91.7848293 + }, + "flyZones": [ + { + "altitudeMin": 100.0, + "altitudeMax": 400.0, + "boundaryPoints": [ + { + "latitude": 37.95120673695754, + "longitude": -91.78693406657685 + }, + { + "latitude": 37.951248325955994, + "longitude": -91.78207299965484 + }, + { + "latitude": 37.94797658615526, + "longitude": -91.78697801835733 + }, + { + "latitude": 37.948226725559984, + "longitude": -91.78191292975686 + }, + { + "latitude": 37.94711526778687, + "longitude": -91.78116415681103 + }, + { + "latitude": 37.94623824627026, + "longitude": -91.78490802154013 + } + ] + } + ], + "waypoints": [ + { + "latitude": 37.94883876837826, + "longitude": -91.78443677093598, + "altitude": 100.0 + }, + { + "latitude": 37.949016435151485, + "longitude": -91.78364283711778, + "altitude": 150.0 + }, + { + "latitude": 37.94957481364174, + "longitude": -91.78369648129468, + "altitude": 300.0 + }, + { + "latitude": 37.95004012580848, + "longitude": -91.78468353414975, + "altitude": 250.0 + }, + { + "latitude": 37.949735558177984, + "longitude": -91.78542382379104, + "altitude": 150.0 + }, + { + "latitude": 37.9487541650019, + "longitude": -91.785788604194, + "altitude": 200.0 + }, + { + "latitude": 37.948576497594445, + "longitude": -91.78460843230208, + "altitude": 150.0 + } + ], + "stationaryObstacles": [ + { + "latitude": 37.94918564120274, + "longitude": -91.78431875374679, + "radius": 30, + "height": 400 + }, + { + "latitude": 37.94966787631084, + "longitude": -91.78554184098023, + "radius": 40, + "height": 200 + }, + { + "latitude": 37.94828884469052, + "longitude": -91.78546673913256, + "radius": 20, + "height": 150 + } + ] +} From 7240e917e31feed918ab1e963d1fcd4688c848bf Mon Sep 17 00:00:00 2001 From: Christopher Spencer Date: Sun, 6 Mar 2022 11:50:33 -0600 Subject: [PATCH 39/45] Document and type all functions --- flight/Stationary Obstacle Avoidance/main.py | 6 +- .../math_functions.py | 82 ++++++++++++----- .../obstacle_avoidance.py | 91 ++++++++++++++++--- .../Stationary Obstacle Avoidance/plotter.py | 76 ++++++++++++---- 4 files changed, 197 insertions(+), 58 deletions(-) diff --git a/flight/Stationary Obstacle Avoidance/main.py b/flight/Stationary Obstacle Avoidance/main.py index f09ff7f..4f43072 100644 --- a/flight/Stationary Obstacle Avoidance/main.py +++ b/flight/Stationary Obstacle Avoidance/main.py @@ -40,7 +40,7 @@ { "latitude": 37.949388706571064, "longitude": -91.78569200437985, - "radius": 35, + "radius": 30, "height": 200, }, { @@ -57,7 +57,7 @@ obstacles = obstacle_avoidance.all_latlon_to_utm(obstacles) # Plot data before processing - # plotter.plot_data(waypoints, obstacles, SAFETY_MARGIN, flight_path_color='ko-') + # plotter.plot_data(waypoints, obstacles, SAFETY_MARGIN) # Find new safe path between all waypoints waypoints = obstacle_avoidance.get_safe_route( @@ -65,4 +65,4 @@ ) # Plot data after processing - plotter.plot_data(waypoints, obstacles, SAFETY_MARGIN) + plotter.plot_data(waypoints, obstacles, SAFETY_MARGIN, flight_path_color="bo-") diff --git a/flight/Stationary Obstacle Avoidance/math_functions.py b/flight/Stationary Obstacle Avoidance/math_functions.py index 2fef860..cda9b2f 100644 --- a/flight/Stationary Obstacle Avoidance/math_functions.py +++ b/flight/Stationary Obstacle Avoidance/math_functions.py @@ -1,23 +1,28 @@ import math -from typing import List, Tuple +from typing import Tuple -def distance(a: Tuple[int], b: Tuple[int]) -> float: - """Calculates the euclidean distance between two points +Point = Tuple[float, float] +Line = Tuple[float, float, float] - Args: - a (Tuple[int]): x,y coordinates for point a - b (Tuple[int]): x,y coordinates for point b - Returns: - float: distance between points a and b - """ +def distance(a: Point, b: Point) -> float: + """Returns the euclidean distance between two points""" return abs(math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2)) -# Takes a point and returns the two points that are tangent to a circle of radius r -# src: https://math.stackexchange.com/a/3190374 -def tangent_points(point, circle, r): +def tangent_points(point: Point, circle: Point, r: float) -> Tuple[Point, Point]: + """ + From a point outside ther circle, calculates the two points that are tangent to the circle + + Args: + point (Point): a point that lies outside the radius of the circle + circle (Point): center of the circle + r (float): radius of the circle + + Returns: + Tuple[Point, Point]: the coordinates of each tangent to the circle + """ Px = point[0] Py = point[1] Cx = circle[0] @@ -35,18 +40,25 @@ def tangent_points(point, circle, r): return ((T1x, T1y), (T2x, T2y)) -# Returns the equation of a line given two points -# src: https://stackoverflow.com/a/20679579 -def line(p1, p2): +def line(p1: Point, p2: Point) -> Line: + """Returns the standard form of the equation of a line given two points""" A = p1[1] - p2[1] B = p2[0] - p1[0] C = p1[0] * p2[1] - p2[0] * p1[1] return A, B, -C -# Finds the intersection of two lines -# src: https://stackoverflow.com/a/20679579 -def intersection(L1, L2): +def intersection(L1: Line, L2: Line) -> Point: + """ + Finds the intersection of two lines + + Args: + L1 (Line): equation of a line in standard form + L2 (Line): equation of a line in standard form + + Returns: + Point: coordinates of the intersection + """ D = L1[0] * L2[1] - L1[1] * L2[0] Dx = L1[2] * L2[1] - L1[1] * L2[2] Dy = L1[0] * L2[2] - L1[2] * L2[0] @@ -58,8 +70,18 @@ def intersection(L1, L2): return False -# Returns a circle with a center and a radius given 3 points on the edge -def find_circle(b, c, d): +def find_circle(b: Point, c: Point, d: Point) -> Tuple[Point, float]: + """ + Finds the center and radius of a circle given three points on the circumfrence of the circle + + Args: + b (Point): a point on the xy plane + c (Point): a point on the xy plane + d (Point): a point on the xy plane + + Returns: + Tuple[Point, float]: the center coordinates and radius of the circle + """ temp = c[0] ** 2 + c[1] ** 2 bc = (b[0] ** 2 + b[1] ** 2 - temp) / 2 cd = (temp - d[0] ** 2 - d[1] ** 2) / 2 @@ -77,9 +99,23 @@ def find_circle(b, c, d): return ((cx, cy), radius) -# Calculates which pair of tangent points are on the same side of the circle -# returns line segments between the start and end points and the appropriate tangents -def resolve_closest_tangents(start, end, A1, A2, B1, B2): +def resolve_closest_tangents( + start: Point, end: Point, A1: Point, A2: Point, B1: Point, B2: Point +) -> Tuple[Line, Line, Line, Line]: + """ + Calculates which pair of tangent points are on the same side of the circle + + Args: + start (Point): starting point + end (Point): ending point + A1 (Point): tangent one of the starting point + A2 (Point): tangent two of the starting point + B1 (Point): tangent one of the ending point + B2 (Point): tangent two of the ending point + + Returns: + Tuple[Line, Line, Line, Line]: Line segments from the start to the tangent to the end + """ distances = [distance(A1, B1), distance(A1, B2), distance(A2, B1), distance(A2, B2)] i = distances.index(min(distances)) diff --git a/flight/Stationary Obstacle Avoidance/obstacle_avoidance.py b/flight/Stationary Obstacle Avoidance/obstacle_avoidance.py index ff531af..7219540 100644 --- a/flight/Stationary Obstacle Avoidance/obstacle_avoidance.py +++ b/flight/Stationary Obstacle Avoidance/obstacle_avoidance.py @@ -1,14 +1,17 @@ -from typing import List, Tuple +from typing import Tuple from collections import namedtuple import utm from fractions import Fraction -from shapely.geometry import LineString -from shapely.geometry import Point +from shapely import geometry import math_functions import plotter -def latlon_to_utm(coords): +Point = Tuple[float, float] + + +def latlon_to_utm(coords: dict) -> dict: + """Converts latlon coordinates to utm coordinates and adds the data to the dictionary""" utm_coords = utm.from_latlon(coords["latitude"], coords["longitude"]) coords["utm_x"] = utm_coords[0] coords["utm_y"] = utm_coords[1] @@ -17,21 +20,35 @@ def latlon_to_utm(coords): return coords -def all_latlon_to_utm(list_of_coords): +def all_latlon_to_utm(list_of_coords: list[dict]) -> list[dict]: + """Converts a list of dictionarys with latlon data to add utm data""" for i in range(len(list_of_coords)): list_of_coords[i] = latlon_to_utm(list_of_coords[i]) return list_of_coords -def check_for_collision(p1, p2, obstacles, padding): +def check_for_collision( + p1: Point, p2: Point, obstacles: list[dict], padding: float +) -> int or None: + """ + Checks if the path between two points collides with an obstacle in the given list + + Args: + p1 (Point): Starting point + p2 (Point): Ending point + obstacles (list[dict]): List of dictionaries containing obstacle data + padding (float): Saftey margin around obstacle + + Returns: + int: Index of the obstacle if a collision is found + """ for i, obstacle in enumerate(obstacles): x = obstacle["utm_x"] y = obstacle["utm_y"] radius = obstacle["radius"] - line = LineString([(p1[0], p1[1]), (p2[0], p2[1])]) - circle = ( - Point(x, y).buffer(radius + padding).boundary - ) # https://stackoverflow.com/a/30998492 + line = geometry.LineString([(p1[0], p1[1]), (p2[0], p2[1])]) + # Creates a perfect circle shape by buffering a single point by a desired radius + circle = geometry.Point(x, y).buffer(radius + padding).boundary if circle.intersection(line): return i else: @@ -39,8 +56,32 @@ def check_for_collision(p1, p2, obstacles, padding): def find_new_point( - pointA, pointB, obstacle, radius, padding, max_distance=0, debugging=False -) -> tuple: + pointA: Point, + pointB: Point, + obstacle: Point, + radius: float, + padding: float, + max_distance: float = 0, + debugging: bool = False, +) -> list[dict]: + """ + Given a starting and ending point whose path intersects a given obstacle, + finds new waypoint(s) that lie outside the circle allowing causing the path to no longer intersect the obstacle + + Args: + pointA (Point): Starting point + pointB (Point): Ending point + obstacle (Point): Obstacle that lies between the starting and ending point + radius (float): Radius of the obstacle + padding (float): Saftey margin around the obstacle + max_distance (float, optional): Maximum distance the new single waypoint can be away from the circle + before splitting into two new waypoints Defaults to 0. + debugging (bool, optional): Debugging on or off. Defaults to False. + + Returns: + list[dict]: List of one or two waypoints + """ + # rough calculation of new height new_altitude = (pointA[2] + pointB[2]) / 2 @@ -92,7 +133,8 @@ def find_new_point( special_case = True print("New waypoint is too far away, splitting and adding more waypoints") - # https://math.stackexchange.com/a/1630886 + + # Equation for finding a point along a line a certain distance from another point t = (radius + padding) / waypoint_distance_from_circle temp_x = ((1 - t) * obstacle[0]) + (t * new_point[0]) temp_y = ((1 - t) * obstacle[1]) + (t * new_point[1]) @@ -177,7 +219,28 @@ def find_new_point( ] -def get_safe_route(waypoints, obstacles, padding, max_distance, debugging=False): +def get_safe_route( + waypoints: list[dict], + obstacles: list[dict], + padding: float, + max_distance: float, + debugging: bool = False, +) -> list[dict]: + """ + Given a list of obstacles and a list of waypoints in which the path between some waypoints may intersect the obstacles, + calculates a new safe route with additional waypoints that avoids the obstacles + + Args: + waypoints (list[dict]): List of waypoints with utm data + obstacles (list[dict]): List of obstacles with utm data + padding (float): Saftey margin around obstacles + max_distance (float): Maximum distance away from circle before switching from single point to two points + debugging (bool, optional): Debugging on or off. Defaults to False. + + Returns: + list[dict]: Updated list of waypoints that include additional safe waypoints if needed + """ + i = 0 num_waypoints = len(waypoints) - 1 while i < num_waypoints: diff --git a/flight/Stationary Obstacle Avoidance/plotter.py b/flight/Stationary Obstacle Avoidance/plotter.py index 749b563..80c7641 100644 --- a/flight/Stationary Obstacle Avoidance/plotter.py +++ b/flight/Stationary Obstacle Avoidance/plotter.py @@ -1,7 +1,26 @@ import matplotlib.pyplot as plt +from typing import List, Tuple -def plot_data(waypoints, obstacles, padding, flight_path_color="bo-"): +Point = Tuple[float, float] + + +def plot_data( + waypoints: List[dict], + obstacles: List[dict], + padding: float, + flight_path_color: str = "ko-", +) -> None: + """ + Plots the waypoints, obstacles, and flight path between waypoints + + Args: + waypoints (List[dict]): List of dictionaries containing waypoint data + obstacles (List[dict]): List of dictionaries containing obstacle data + padding (float): Saftey margin around obstacles + flight_path_color (str, optional): Color of lines indicating flight path. Defaults to black. + """ + # plot obstacles for obstacle in obstacles: x = obstacle["utm_x"] @@ -25,21 +44,42 @@ def plot_data(waypoints, obstacles, padding, flight_path_color="bo-"): def plot_debug( - pointA, - pointB, - obstacle, - tangentA1, - tangentA2, - tangentB1, - tangentB2, - new_point, - alt_point, - temp_point, - extra_1, - extra_2, - special_case, - padding, -): + pointA: Point, + pointB: Point, + obstacle: Point, + tangentA1: Point, + tangentA2: Point, + tangentB1: Point, + tangentB2: Point, + new_point: Point, + alt_point: Point, + temp_point: Point, + extra_1: Point, + extra_2: Point, + special_case: bool, + padding: float, +) -> None: + """ + Used for debugging. Plots two waypoints and the obstacle between them + as well as all the other points used in calculations. + + Args: + pointA (Point): Starting point + pointB (Point): Ending point + obstacle (Point): Obstacle + tangentA1 (Point): First tangent to the starting point + tangentA2 (Point): Second tangent to the starting point + tangentB1 (Point): First tangent to the ending point + tangentB2 (Point): Second tangent to the ending point + new_point (Point): Intersection point on the shorter side + alt_point (Point): Intersection point on the longer side + temp_point (Point): Point that lies on the edge of the circle inline with the center and the new_point + extra_1 (Point): Additional point if special case is true + extra_2 (Point): Additional point if special case is true + special_case (bool): Whether or not the new point is too far from the circle edge + padding (float): Saftey margin around the obstacle + """ + radius = obstacle[2] # plot obstacle @@ -93,7 +133,7 @@ def plot_debug( "blue", "blue", ] - txt = [ + labels = [ "Start", "End", "Obstacle", @@ -113,7 +153,7 @@ def plot_debug( plt.scatter(x[i], y[i], c=col[i]) # plot text labels - for i, v in enumerate(txt): + for i, v in enumerate(labels): plt.gca().annotate(v, (x[i], y[i])) if special_case: From b21c2d2cf9321f4588a70d5fea35695a055fdc9a Mon Sep 17 00:00:00 2001 From: Christopher Spencer Date: Tue, 8 Mar 2022 19:45:58 -0600 Subject: [PATCH 40/45] Resolve documentation issues and dead code --- .../math_functions.py | 51 ++++++++----------- .../obstacle_avoidance.py | 20 +++++++- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/flight/Stationary Obstacle Avoidance/math_functions.py b/flight/Stationary Obstacle Avoidance/math_functions.py index cda9b2f..98b9118 100644 --- a/flight/Stationary Obstacle Avoidance/math_functions.py +++ b/flight/Stationary Obstacle Avoidance/math_functions.py @@ -7,7 +7,16 @@ def distance(a: Point, b: Point) -> float: - """Returns the euclidean distance between two points""" + """ + Calculates the euclidean distance between two points + + Args: + a (Point): First point + b (Point): Second point + + Returns: + float: Returns the distance between points a and b + """ return abs(math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2)) @@ -41,7 +50,16 @@ def tangent_points(point: Point, circle: Point, r: float) -> Tuple[Point, Point] def line(p1: Point, p2: Point) -> Line: - """Returns the standard form of the equation of a line given two points""" + """ + Finds the standard form of the equation of a line given two points + + Args: + p1 (Point): First pair of x,y coordinates + p2 (Point): Second pair of x,y coordinates + + Returns: + Line: The real numbers A, B and -C which make up the standard line equation + """ A = p1[1] - p2[1] B = p2[0] - p1[0] C = p1[0] * p2[1] - p2[0] * p1[1] @@ -70,35 +88,6 @@ def intersection(L1: Line, L2: Line) -> Point: return False -def find_circle(b: Point, c: Point, d: Point) -> Tuple[Point, float]: - """ - Finds the center and radius of a circle given three points on the circumfrence of the circle - - Args: - b (Point): a point on the xy plane - c (Point): a point on the xy plane - d (Point): a point on the xy plane - - Returns: - Tuple[Point, float]: the center coordinates and radius of the circle - """ - temp = c[0] ** 2 + c[1] ** 2 - bc = (b[0] ** 2 + b[1] ** 2 - temp) / 2 - cd = (temp - d[0] ** 2 - d[1] ** 2) / 2 - det = (b[0] - c[0]) * (c[1] - d[1]) - (c[0] - d[0]) * (b[1] - c[1]) - - if abs(det) < 1.0e-10: - return None - - # Center of circle - cx = (bc * (c[1] - d[1]) - cd * (b[1] - c[1])) / det - cy = ((b[0] - c[0]) * cd - (c[0] - d[0]) * bc) / det - - radius = ((cx - b[0]) ** 2 + (cy - b[1]) ** 2) ** 0.5 - - return ((cx, cy), radius) - - def resolve_closest_tangents( start: Point, end: Point, A1: Point, A2: Point, B1: Point, B2: Point ) -> Tuple[Line, Line, Line, Line]: diff --git a/flight/Stationary Obstacle Avoidance/obstacle_avoidance.py b/flight/Stationary Obstacle Avoidance/obstacle_avoidance.py index 7219540..ae8e63c 100644 --- a/flight/Stationary Obstacle Avoidance/obstacle_avoidance.py +++ b/flight/Stationary Obstacle Avoidance/obstacle_avoidance.py @@ -11,7 +11,15 @@ def latlon_to_utm(coords: dict) -> dict: - """Converts latlon coordinates to utm coordinates and adds the data to the dictionary""" + """ + Converts latlon coordinates to utm coordinates and adds the data to the dictionary + + Args: + coords (dict): A dictionary containing lat long coordinates + + Returns: + dict: An updated dictionary with additional keys and values with utm data + """ utm_coords = utm.from_latlon(coords["latitude"], coords["longitude"]) coords["utm_x"] = utm_coords[0] coords["utm_y"] = utm_coords[1] @@ -21,7 +29,15 @@ def latlon_to_utm(coords: dict) -> dict: def all_latlon_to_utm(list_of_coords: list[dict]) -> list[dict]: - """Converts a list of dictionarys with latlon data to add utm data""" + """ + Converts a list of dictionarys with latlon data to add utm data + + Args: + list_of_coords (list[dict]): A list of dictionaries that contain lat long data + + Returns: + list[dict]: An updated list of dictionaries with added utm data + """ for i in range(len(list_of_coords)): list_of_coords[i] = latlon_to_utm(list_of_coords[i]) return list_of_coords From 117921c92ce420940a745f49b0846c22cf00ed0b Mon Sep 17 00:00:00 2001 From: Mohammad Rouie Date: Thu, 10 Mar 2022 22:07:15 -0600 Subject: [PATCH 41/45] Add poetry python dependencies and dev dependencies --- pyproject.toml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0090274 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[tool.poetry] +name = "suas-2022" +version = "0.1.0" +description = "" +authors = [] +license = "" + +[tool.poetry.dependencies] +python = "^3.8" +pip = "^22.0.4" +mavsdk = "*" +numpy = "^1.21.2" +opencv-python = "^4.5.3.56" +pytesseract = "^0.3.9" +lxml = "^4.8.0" +colorlog = "^6.6.0" +pyproj = "^3.3.0" +pyusb = "^1.2.1" + +[tool.poetry.dev-dependencies] +black = "^21.12b0" +pylint = "^2.12.2" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" From aeb91687d25ff8701fdff902e0ee8c50ec1d67d7 Mon Sep 17 00:00:00 2001 From: Mohammad Rouie Date: Thu, 10 Mar 2022 23:06:56 -0600 Subject: [PATCH 42/45] Reformat file. --- .gitignore | 10 - .pre-commit-config.yaml | 2 + LICENSE | 21 ++ poetry.lock | 797 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 +- 5 files changed, 822 insertions(+), 11 deletions(-) create mode 100644 LICENSE create mode 100644 poetry.lock diff --git a/.gitignore b/.gitignore index 5608405..01bb140 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,3 @@ -# Vision -vision_images/ -vision_videos/ -*.jpg -*.png -*.bag - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -119,6 +112,3 @@ venv.bak/ # Spreadsheets *.csv - -# Vision Benchmark Ouputs -results/*.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e56b65d..50528e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,10 +10,12 @@ repos: - id: trailing-whitespace - id: check-added-large-files - id: check-merge-conflict + - id: check-case-conflict - id: end-of-file-fixer - id: check-json - id: check-xml - id: check-yaml + - id: check-toml - id: mixed-line-ending args: ['--fix=lf'] description: Forces line endings to the UNIX LF character diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cf4cd91 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Missouri S&T Multirotor Robot Design Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..10466d0 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,797 @@ +[[package]] +name = "aiogrpc" +version = "1.8" +description = "asyncio wrapper for grpc.io" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +grpcio = ">=1.12.0" + +[[package]] +name = "astroid" +version = "2.9.3" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} +wrapt = ">=1.11,<1.14" + +[[package]] +name = "black" +version = "21.12b0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0,<1" +platformdirs = ">=2" +tomli = ">=0.2.6,<2.0.0" +typing-extensions = [ + {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, + {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, +] + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +python2 = ["typed-ast (>=1.4.3)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "click" +version = "8.0.4" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "colorlog" +version = "6.6.0" +description = "Add colours to the output of Python's logging module." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +development = ["black", "flake8", "mypy", "pytest", "types-colorama"] + +[[package]] +name = "grpcio" +version = "1.44.0" +description = "HTTP/2-based RPC framework" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +six = ">=1.5.2" + +[package.extras] +protobuf = ["grpcio-tools (>=1.44.0)"] + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "lazy-object-proxy" +version = "1.7.1" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "lxml" +version = "4.8.0" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.7)"] + +[[package]] +name = "mavsdk" +version = "0.23.0" +description = "Python wrapper for MAVSDK" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +aiogrpc = ">=1.5" +grpcio = ">=1.11.0" +protobuf = ">=3.13.0" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy" +version = "0.931" +description = "Optional static typing for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = ">=1.1.0" +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<2)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "numpy" +version = "1.22.3" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "opencv-python" +version = "4.5.5.64" +description = "Wrapper package for OpenCV python bindings." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +numpy = [ + {version = ">=1.21.2", markers = "python_version >= \"3.10\" or python_version >= \"3.6\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, + {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, + {version = ">=1.14.5", markers = "python_version >= \"3.7\""}, + {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, +] + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "pillow" +version = "9.0.1" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "platformdirs" +version = "2.5.1" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "protobuf" +version = "3.19.4" +description = "Protocol Buffers" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "pylint" +version = "2.12.2" +description = "python code static checker" +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +astroid = ">=2.9.0,<2.10" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.7" +platformdirs = ">=2.2.0" +toml = ">=0.9.2" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[[package]] +name = "pyparsing" +version = "3.0.7" +description = "Python parsing module" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pyproj" +version = "3.3.0" +description = "Python interface to PROJ (cartographic projections and coordinate transformations library)" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +certifi = "*" + +[[package]] +name = "pytesseract" +version = "0.3.9" +description = "Python-tesseract is a python wrapper for Google's Tesseract-OCR" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +packaging = ">=21.3" +Pillow = ">=8.0.0" + +[[package]] +name = "pyusb" +version = "1.2.1" +description = "Python USB access module" +category = "main" +optional = false +python-versions = ">=3.6.0" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "1.2.3" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.1.1" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "wrapt" +version = "1.13.3" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "459c5ffe5d56da3070f1060c92904a340a98817289941ceb86f1207887cef445" + +[metadata.files] +aiogrpc = [ + {file = "aiogrpc-1.8-py3-none-any.whl", hash = "sha256:d01e6906927649646dcd938c7d46bf718f427473450db0af6343acc2ed2cfa4d"}, + {file = "aiogrpc-1.8.tar.gz", hash = "sha256:472155a52850bd4b9493a994079f9c12c65324f02fe6466e636470127fc32aaf"}, +] +astroid = [ + {file = "astroid-2.9.3-py3-none-any.whl", hash = "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6"}, + {file = "astroid-2.9.3.tar.gz", hash = "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877"}, +] +black = [ + {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, + {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +click = [ + {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, + {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +colorlog = [ + {file = "colorlog-6.6.0-py2.py3-none-any.whl", hash = "sha256:351c51e866c86c3217f08e4b067a7974a678be78f07f85fc2d55b8babde6d94e"}, + {file = "colorlog-6.6.0.tar.gz", hash = "sha256:344f73204009e4c83c5b6beb00b3c45dc70fcdae3c80db919e0a4171d006fde8"}, +] +grpcio = [ + {file = "grpcio-1.44.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:11f811c0fffd84fca747fbc742464575e5eb130fd4fb4d6012ccc34febd001db"}, + {file = "grpcio-1.44.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9a86a91201f8345502ea81dee0a55ae13add5fafadf109b17acd858fe8239651"}, + {file = "grpcio-1.44.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:5f3c54ebb5d9633a557335c01d88d3d4928e9b1b131692283b6184da1edbec0b"}, + {file = "grpcio-1.44.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d47553b8e86ab1e59b0185ba6491a187f94a0239f414c8fc867a22b0405b798"}, + {file = "grpcio-1.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1e22d3a510438b7f3365c0071b810672d09febac6e8ca8a47eab657ae5f347b"}, + {file = "grpcio-1.44.0-cp310-cp310-win32.whl", hash = "sha256:41036a574cab3468f24d41d6ed2b52588fb85ed60f8feaa925d7e424a250740b"}, + {file = "grpcio-1.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:4ee51964edfd0a1293a95bb0d72d134ecf889379d90d2612cbf663623ce832b4"}, + {file = "grpcio-1.44.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:e2149077d71e060678130644670389ddf1491200bcea16c5560d4ccdc65e3f2e"}, + {file = "grpcio-1.44.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:0ac72d4b953b76924f8fa21436af060d7e6d8581e279863f30ee14f20751ac27"}, + {file = "grpcio-1.44.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5c30a9a7d3a05920368a60b080cbbeaf06335303be23ac244034c71c03a0fd24"}, + {file = "grpcio-1.44.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:05467acd391e3fffb05991c76cb2ed2fa1309d0e3815ac379764bc5670b4b5d4"}, + {file = "grpcio-1.44.0-cp36-cp36m-manylinux_2_17_aarch64.whl", hash = "sha256:b81dc7894062ed2d25b74a2725aaa0a6895ce97ce854f432fe4e87cad5a07316"}, + {file = "grpcio-1.44.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46d4843192e7d36278884282e100b8f305cf37d1b3d8c6b4f736d4454640a069"}, + {file = "grpcio-1.44.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:898c159148f27e23c08a337fb80d31ece6b76bb24f359d83929460d813665b74"}, + {file = "grpcio-1.44.0-cp36-cp36m-win32.whl", hash = "sha256:b8d852329336c584c636caa9c2db990f3a332b19bc86a80f4646b58d27c142db"}, + {file = "grpcio-1.44.0-cp36-cp36m-win_amd64.whl", hash = "sha256:790d7493337558ae168477d1be3178f4c9b8f91d8cd9b8b719d06fd9b2d48836"}, + {file = "grpcio-1.44.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:cd61b52d9cf8fcf8d9628c0b640b9e44fdc5e93d989cc268086a858540ed370c"}, + {file = "grpcio-1.44.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:14eefcf623890f3f7dd7831decd2a2116652b5ce1e0f1d4b464b8f52110743b0"}, + {file = "grpcio-1.44.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:bebe90b8020b4248e5a2076b56154cc6ff45691bbbe980579fc9db26717ac968"}, + {file = "grpcio-1.44.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89b390b1c0de909965280d175c53128ce2f0f4f5c0f011382243dd7f2f894060"}, + {file = "grpcio-1.44.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:c122dac5cb299b8ad7308d61bd9fe0413de13b0347cce465398436b3fdf1f609"}, + {file = "grpcio-1.44.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6641a28cc826a92ef717201cca9a035c34a0185e38b0c93f3ce5f01a01a1570a"}, + {file = "grpcio-1.44.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb0a3e0e64843441793923d9532a3a23907b07b2a1e0a7a31f186dc185bb772"}, + {file = "grpcio-1.44.0-cp37-cp37m-win32.whl", hash = "sha256:be857b7ec2ac43455156e6ba89262f7d7ae60227049427d01a3fecd218a3f88d"}, + {file = "grpcio-1.44.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f6a9cf0e77f72f2ac30c9c6e086bc7446c984c51bebc6c7f50fbcd718037edba"}, + {file = "grpcio-1.44.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:19e54f0c7083c8332b5a75a9081fc5127f1dbb67b6c1a32bd7fe896ef0934918"}, + {file = "grpcio-1.44.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:bfd36b959c3c4e945119387baed1414ea46f7116886aa23de0172302b49d7ff1"}, + {file = "grpcio-1.44.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:ccd388b8f37b19d06e4152189726ce309e36dc03b53f2216a4ea49f09a7438e6"}, + {file = "grpcio-1.44.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:9075c0c003c1ff14ebce8f0ba55cc692158cb55c68da09cf8b0f9fc5b749e343"}, + {file = "grpcio-1.44.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:e898194f76212facbaeb6d7545debff29351afa23b53ff8f0834d66611af5139"}, + {file = "grpcio-1.44.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fa6584046a7cf281649975a363673fa5d9c6faf9dc923f261cc0e56713b5892"}, + {file = "grpcio-1.44.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36a7bdd6ef9bca050c7ade8cba5f0e743343ea0756d5d3d520e915098a9dc503"}, + {file = "grpcio-1.44.0-cp38-cp38-win32.whl", hash = "sha256:dc3290d0411ddd2bd49adba5793223de8de8b01588d45e9376f1a9f7d25414f4"}, + {file = "grpcio-1.44.0-cp38-cp38-win_amd64.whl", hash = "sha256:13343e7b840c20f43b44f0e6d3bbdc037c964f0aec9735d7cb685c407731c9ff"}, + {file = "grpcio-1.44.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:c5c2f8417d13386e18ccc8c61467cb6a6f9667a1ff7000a2d7d378e5d7df693f"}, + {file = "grpcio-1.44.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:cf220199b7b4992729ad4d55d5d3f652f4ccfe1a35b5eacdbecf189c245e1859"}, + {file = "grpcio-1.44.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4201c597e5057a9bfef9ea5777a6d83f6252cb78044db7d57d941ec2300734a5"}, + {file = "grpcio-1.44.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:e2de61005118ae59d48d5d749283ebfd1ba4ca68cc1000f8a395cd2bdcff7ceb"}, + {file = "grpcio-1.44.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:871078218fa9117e2a378678f327e32fda04e363ed6bc0477275444273255d4d"}, + {file = "grpcio-1.44.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8d610b7b557a7609fecee80b6dd793ecb7a9a3c3497fbdce63ce7d151cdd705"}, + {file = "grpcio-1.44.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fcb53e4eb8c271032c91b8981df5fc1bb974bc73e306ec2c27da41bd95c44b5"}, + {file = "grpcio-1.44.0-cp39-cp39-win32.whl", hash = "sha256:e50ddea6de76c09b656df4b5a55ae222e2a56e625c44250e501ff3c904113ec1"}, + {file = "grpcio-1.44.0-cp39-cp39-win_amd64.whl", hash = "sha256:d2ec124a986093e26420a5fb10fa3f02b2c232f924cdd7b844ddf7e846c020cd"}, + {file = "grpcio-1.44.0.tar.gz", hash = "sha256:4bae1c99896045d3062ab95478411c8d5a52cb84b91a1517312629fa6cfeb50e"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, + {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, +] +lxml = [ + {file = "lxml-4.8.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:e1ab2fac607842ac36864e358c42feb0960ae62c34aa4caaf12ada0a1fb5d99b"}, + {file = "lxml-4.8.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28d1af847786f68bec57961f31221125c29d6f52d9187c01cd34dc14e2b29430"}, + {file = "lxml-4.8.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b92d40121dcbd74831b690a75533da703750f7041b4bf951befc657c37e5695a"}, + {file = "lxml-4.8.0-cp27-cp27m-win32.whl", hash = "sha256:e01f9531ba5420838c801c21c1b0f45dbc9607cb22ea2cf132844453bec863a5"}, + {file = "lxml-4.8.0-cp27-cp27m-win_amd64.whl", hash = "sha256:6259b511b0f2527e6d55ad87acc1c07b3cbffc3d5e050d7e7bcfa151b8202df9"}, + {file = "lxml-4.8.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1010042bfcac2b2dc6098260a2ed022968dbdfaf285fc65a3acf8e4eb1ffd1bc"}, + {file = "lxml-4.8.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fa56bb08b3dd8eac3a8c5b7d075c94e74f755fd9d8a04543ae8d37b1612dd170"}, + {file = "lxml-4.8.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:31ba2cbc64516dcdd6c24418daa7abff989ddf3ba6d3ea6f6ce6f2ed6e754ec9"}, + {file = "lxml-4.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:31499847fc5f73ee17dbe1b8e24c6dafc4e8d5b48803d17d22988976b0171f03"}, + {file = "lxml-4.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5f7d7d9afc7b293147e2d506a4596641d60181a35279ef3aa5778d0d9d9123fe"}, + {file = "lxml-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a3c5f1a719aa11866ffc530d54ad965063a8cbbecae6515acbd5f0fae8f48eaa"}, + {file = "lxml-4.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6268e27873a3d191849204d00d03f65c0e343b3bcb518a6eaae05677c95621d1"}, + {file = "lxml-4.8.0-cp310-cp310-win32.whl", hash = "sha256:330bff92c26d4aee79c5bc4d9967858bdbe73fdbdbacb5daf623a03a914fe05b"}, + {file = "lxml-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b2582b238e1658c4061ebe1b4df53c435190d22457642377fd0cb30685cdfb76"}, + {file = "lxml-4.8.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2bfc7e2a0601b475477c954bf167dee6d0f55cb167e3f3e7cefad906e7759f6"}, + {file = "lxml-4.8.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a1547ff4b8a833511eeaceacbcd17b043214fcdb385148f9c1bc5556ca9623e2"}, + {file = "lxml-4.8.0-cp35-cp35m-win32.whl", hash = "sha256:a9f1c3489736ff8e1c7652e9dc39f80cff820f23624f23d9eab6e122ac99b150"}, + {file = "lxml-4.8.0-cp35-cp35m-win_amd64.whl", hash = "sha256:530f278849031b0eb12f46cca0e5db01cfe5177ab13bd6878c6e739319bae654"}, + {file = "lxml-4.8.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:078306d19a33920004addeb5f4630781aaeabb6a8d01398045fcde085091a169"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:86545e351e879d0b72b620db6a3b96346921fa87b3d366d6c074e5a9a0b8dadb"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24f5c5ae618395ed871b3d8ebfcbb36e3f1091fd847bf54c4de623f9107942f3"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bbab6faf6568484707acc052f4dfc3802bdb0cafe079383fbaa23f1cdae9ecd4"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7993232bd4044392c47779a3c7e8889fea6883be46281d45a81451acfd704d7e"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d6483b1229470e1d8835e52e0ff3c6973b9b97b24cd1c116dca90b57a2cc613"}, + {file = "lxml-4.8.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ad4332a532e2d5acb231a2e5d33f943750091ee435daffca3fec0a53224e7e33"}, + {file = "lxml-4.8.0-cp36-cp36m-win32.whl", hash = "sha256:db3535733f59e5605a88a706824dfcb9bd06725e709ecb017e165fc1d6e7d429"}, + {file = "lxml-4.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5f148b0c6133fb928503cfcdfdba395010f997aa44bcf6474fcdd0c5398d9b63"}, + {file = "lxml-4.8.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:8a31f24e2a0b6317f33aafbb2f0895c0bce772980ae60c2c640d82caac49628a"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:719544565c2937c21a6f76d520e6e52b726d132815adb3447ccffbe9f44203c4"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:c0b88ed1ae66777a798dc54f627e32d3b81c8009967c63993c450ee4cbcbec15"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fa9b7c450be85bfc6cd39f6df8c5b8cbd76b5d6fc1f69efec80203f9894b885f"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e9f84ed9f4d50b74fbc77298ee5c870f67cb7e91dcdc1a6915cb1ff6a317476c"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1d650812b52d98679ed6c6b3b55cbb8fe5a5460a0aef29aeb08dc0b44577df85"}, + {file = "lxml-4.8.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:80bbaddf2baab7e6de4bc47405e34948e694a9efe0861c61cdc23aa774fcb141"}, + {file = "lxml-4.8.0-cp37-cp37m-win32.whl", hash = "sha256:6f7b82934c08e28a2d537d870293236b1000d94d0b4583825ab9649aef7ddf63"}, + {file = "lxml-4.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e1fd7d2fe11f1cb63d3336d147c852f6d07de0d0020d704c6031b46a30b02ca8"}, + {file = "lxml-4.8.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:5045ee1ccd45a89c4daec1160217d363fcd23811e26734688007c26f28c9e9e7"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0c1978ff1fd81ed9dcbba4f91cf09faf1f8082c9d72eb122e92294716c605428"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cbf2ff155b19dc4d4100f7442f6a697938bf4493f8d3b0c51d45568d5666b5"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ce13d6291a5f47c1c8dbd375baa78551053bc6b5e5c0e9bb8e39c0a8359fd52f"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11527dc23d5ef44d76fef11213215c34f36af1608074561fcc561d983aeb870"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:60d2f60bd5a2a979df28ab309352cdcf8181bda0cca4529769a945f09aba06f9"}, + {file = "lxml-4.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:62f93eac69ec0f4be98d1b96f4d6b964855b8255c345c17ff12c20b93f247b68"}, + {file = "lxml-4.8.0-cp38-cp38-win32.whl", hash = "sha256:20b8a746a026017acf07da39fdb10aa80ad9877046c9182442bf80c84a1c4696"}, + {file = "lxml-4.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:891dc8f522d7059ff0024cd3ae79fd224752676447f9c678f2a5c14b84d9a939"}, + {file = "lxml-4.8.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b6fc2e2fb6f532cf48b5fed57567ef286addcef38c28874458a41b7837a57807"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:74eb65ec61e3c7c019d7169387d1b6ffcfea1b9ec5894d116a9a903636e4a0b1"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:627e79894770783c129cc5e89b947e52aa26e8e0557c7e205368a809da4b7939"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:545bd39c9481f2e3f2727c78c169425efbfb3fbba6e7db4f46a80ebb249819ca"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5a58d0b12f5053e270510bf12f753a76aaf3d74c453c00942ed7d2c804ca845c"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec4b4e75fc68da9dc0ed73dcdb431c25c57775383fec325d23a770a64e7ebc87"}, + {file = "lxml-4.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5804e04feb4e61babf3911c2a974a5b86f66ee227cc5006230b00ac6d285b3a9"}, + {file = "lxml-4.8.0-cp39-cp39-win32.whl", hash = "sha256:aa0cf4922da7a3c905d000b35065df6184c0dc1d866dd3b86fd961905bbad2ea"}, + {file = "lxml-4.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:dd10383f1d6b7edf247d0960a3db274c07e96cf3a3fc7c41c8448f93eac3fb1c"}, + {file = "lxml-4.8.0-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:2403a6d6fb61c285969b71f4a3527873fe93fd0abe0832d858a17fe68c8fa507"}, + {file = "lxml-4.8.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:986b7a96228c9b4942ec420eff37556c5777bfba6758edcb95421e4a614b57f9"}, + {file = "lxml-4.8.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6fe4ef4402df0250b75ba876c3795510d782def5c1e63890bde02d622570d39e"}, + {file = "lxml-4.8.0-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:f10ce66fcdeb3543df51d423ede7e238be98412232fca5daec3e54bcd16b8da0"}, + {file = "lxml-4.8.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:730766072fd5dcb219dd2b95c4c49752a54f00157f322bc6d71f7d2a31fecd79"}, + {file = "lxml-4.8.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8b99ec73073b37f9ebe8caf399001848fced9c08064effdbfc4da2b5a8d07b93"}, + {file = "lxml-4.8.0.tar.gz", hash = "sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23"}, +] +mavsdk = [ + {file = "mavsdk-0.23.0-py3-none-linux_armv6l.whl", hash = "sha256:2d949045cf7f9945bc59d2fa3c718e973cca943405bc8c36e40bb54bcd3d196c"}, + {file = "mavsdk-0.23.0-py3-none-linux_armv7l.whl", hash = "sha256:4dae1997b3c534425e60b925063fa143d449f50542b91c0ff72a5e1a97adb7e5"}, + {file = "mavsdk-0.23.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:c77a7eae6e8b9951253ea571e2ada63357df125fc2763f739f5cfa47dad1f460"}, + {file = "mavsdk-0.23.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:3a063409660b5c64719e5b6f0c34693f3a80646af7e571bbf3c63aeec6bcad18"}, + {file = "mavsdk-0.23.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3083cc06f216049b082dbaf3dda75b8908b5282b84d8d676ef0f7ce4b2f629b"}, + {file = "mavsdk-0.23.0-py3-none-win32.whl", hash = "sha256:ce0f2da0b972543879a20b89c40e406c69631afb10e7b9f90dfa73fcbfc00e87"}, + {file = "mavsdk-0.23.0-py3-none-win_amd64.whl", hash = "sha256:6a641ad177386af5b9d44dc7c93ae0e34bfbd3fc9fa4a43a50c2451921d9c60a"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mypy = [ + {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"}, + {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"}, + {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"}, + {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"}, + {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"}, + {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"}, + {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"}, + {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"}, + {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"}, + {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"}, + {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"}, + {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"}, + {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"}, + {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"}, + {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"}, + {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"}, + {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"}, + {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"}, + {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"}, + {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +numpy = [ + {file = "numpy-1.22.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:92bfa69cfbdf7dfc3040978ad09a48091143cffb778ec3b03fa170c494118d75"}, + {file = "numpy-1.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8251ed96f38b47b4295b1ae51631de7ffa8260b5b087808ef09a39a9d66c97ab"}, + {file = "numpy-1.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48a3aecd3b997bf452a2dedb11f4e79bc5bfd21a1d4cc760e703c31d57c84b3e"}, + {file = "numpy-1.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3bae1a2ed00e90b3ba5f7bd0a7c7999b55d609e0c54ceb2b076a25e345fa9f4"}, + {file = "numpy-1.22.3-cp310-cp310-win32.whl", hash = "sha256:f950f8845b480cffe522913d35567e29dd381b0dc7e4ce6a4a9f9156417d2430"}, + {file = "numpy-1.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:08d9b008d0156c70dc392bb3ab3abb6e7a711383c3247b410b39962263576cd4"}, + {file = "numpy-1.22.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:201b4d0552831f7250a08d3b38de0d989d6f6e4658b709a02a73c524ccc6ffce"}, + {file = "numpy-1.22.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8c1f39caad2c896bc0018f699882b345b2a63708008be29b1f355ebf6f933fe"}, + {file = "numpy-1.22.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:568dfd16224abddafb1cbcce2ff14f522abe037268514dd7e42c6776a1c3f8e5"}, + {file = "numpy-1.22.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ca688e1b9b95d80250bca34b11a05e389b1420d00e87a0d12dc45f131f704a1"}, + {file = "numpy-1.22.3-cp38-cp38-win32.whl", hash = "sha256:e7927a589df200c5e23c57970bafbd0cd322459aa7b1ff73b7c2e84d6e3eae62"}, + {file = "numpy-1.22.3-cp38-cp38-win_amd64.whl", hash = "sha256:07a8c89a04997625236c5ecb7afe35a02af3896c8aa01890a849913a2309c676"}, + {file = "numpy-1.22.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:2c10a93606e0b4b95c9b04b77dc349b398fdfbda382d2a39ba5a822f669a0123"}, + {file = "numpy-1.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fade0d4f4d292b6f39951b6836d7a3c7ef5b2347f3c420cd9820a1d90d794802"}, + {file = "numpy-1.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bfb1bb598e8229c2d5d48db1860bcf4311337864ea3efdbe1171fb0c5da515d"}, + {file = "numpy-1.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97098b95aa4e418529099c26558eeb8486e66bd1e53a6b606d684d0c3616b168"}, + {file = "numpy-1.22.3-cp39-cp39-win32.whl", hash = "sha256:fdf3c08bce27132395d3c3ba1503cac12e17282358cb4bddc25cc46b0aca07aa"}, + {file = "numpy-1.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:639b54cdf6aa4f82fe37ebf70401bbb74b8508fddcf4797f9fe59615b8c5813a"}, + {file = "numpy-1.22.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c34ea7e9d13a70bf2ab64a2532fe149a9aced424cd05a2c4ba662fd989e3e45f"}, + {file = "numpy-1.22.3.zip", hash = "sha256:dbc7601a3b7472d559dc7b933b18b4b66f9aa7452c120e87dfb33d02008c8a18"}, +] +opencv-python = [ + {file = "opencv-python-4.5.5.64.tar.gz", hash = "sha256:f65de0446a330c3b773cd04ba10345d8ce1b15dcac3f49770204e37602d0b3f7"}, + {file = "opencv_python-4.5.5.64-cp36-abi3-macosx_10_15_x86_64.whl", hash = "sha256:a512a0c59b6fec0fac3844b2f47d6ecb1a9d18d235e6c5491ce8dbbe0663eae8"}, + {file = "opencv_python-4.5.5.64-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca6138b6903910e384067d001763d40f97656875487381aed32993b076f44375"}, + {file = "opencv_python-4.5.5.64-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b293ced62f4360d9f11cf72ae7e9df95320ff7bf5b834d87546f844e838c0c35"}, + {file = "opencv_python-4.5.5.64-cp36-abi3-win32.whl", hash = "sha256:6247e584813c00c3b9ed69a795da40d2c153dc923d0182e957e1c2f00a554ac2"}, + {file = "opencv_python-4.5.5.64-cp36-abi3-win_amd64.whl", hash = "sha256:408d5332550287aa797fd06bef47b2dfed163c6787668cc82ef9123a9484b56a"}, + {file = "opencv_python-4.5.5.64-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:7787bb017ae93d5f9bb1b817ac8e13e45dd193743cb648498fcab21d00cf20a3"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +pillow = [ + {file = "Pillow-9.0.1-1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4"}, + {file = "Pillow-9.0.1-1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976"}, + {file = "Pillow-9.0.1-1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc"}, + {file = "Pillow-9.0.1-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd"}, + {file = "Pillow-9.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f"}, + {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a"}, + {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049"}, + {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a"}, + {file = "Pillow-9.0.1-cp310-cp310-win32.whl", hash = "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e"}, + {file = "Pillow-9.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b"}, + {file = "Pillow-9.0.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e"}, + {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360"}, + {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b"}, + {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030"}, + {file = "Pillow-9.0.1-cp37-cp37m-win32.whl", hash = "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669"}, + {file = "Pillow-9.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092"}, + {file = "Pillow-9.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204"}, + {file = "Pillow-9.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e"}, + {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c"}, + {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5"}, + {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae"}, + {file = "Pillow-9.0.1-cp38-cp38-win32.whl", hash = "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c"}, + {file = "Pillow-9.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00"}, + {file = "Pillow-9.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838"}, + {file = "Pillow-9.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28"}, + {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c"}, + {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b"}, + {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7"}, + {file = "Pillow-9.0.1-cp39-cp39-win32.whl", hash = "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7"}, + {file = "Pillow-9.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"}, + {file = "Pillow-9.0.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97"}, + {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56"}, + {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e"}, + {file = "Pillow-9.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70"}, + {file = "Pillow-9.0.1.tar.gz", hash = "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa"}, +] +platformdirs = [ + {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, + {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, +] +protobuf = [ + {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, + {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, + {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, + {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, + {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, + {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, + {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, + {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, + {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, + {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, + {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, + {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, + {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, + {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, + {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, + {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, + {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, + {file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, + {file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, + {file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, + {file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, + {file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"}, + {file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"}, + {file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"}, + {file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"}, + {file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"}, +] +pylint = [ + {file = "pylint-2.12.2-py3-none-any.whl", hash = "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74"}, + {file = "pylint-2.12.2.tar.gz", hash = "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9"}, +] +pyparsing = [ + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, +] +pyproj = [ + {file = "pyproj-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2c41c9b7b5e1a1b0acc2b7b2f5de65b226f7b96c870888e4f679ff96322b1ed0"}, + {file = "pyproj-3.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0e1fd560b509b722db6566fa9685252f25640e93464d09e13d5190ed7ab491ba"}, + {file = "pyproj-3.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277072176a17471c0b1d25d6cae75401d81e9b50ea625ba546f5b79acd757dfc"}, + {file = "pyproj-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eca8ecf2b6b3225d93c723e6a2f51143d9195ac407f69e979363cdde344b93bb"}, + {file = "pyproj-3.3.0-cp310-cp310-win32.whl", hash = "sha256:4d2fc49c73d9f34e932bf37926d56916ba1b6f2f693cd4d8cc1d0d9eacc0e537"}, + {file = "pyproj-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:ce1adec823738e2d7c6af019fc38f58b4204bacfc782e4430373578c672f3833"}, + {file = "pyproj-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e70a1ea6f198cace1a492397bdd0a46e640201120973293d6c48031e370d6a87"}, + {file = "pyproj-3.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:99f171da5f885efeec8d7fb2e2557175ffa8834eeb488842b1f52ac78a9a98e5"}, + {file = "pyproj-3.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3d28b84913cd849832a8f154c0e0c2ee4618057f7389ee68bfdb2145e7ed78cc"}, + {file = "pyproj-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab4baf781721640659db83a6b4da636fc403008f4978c668275754284c946778"}, + {file = "pyproj-3.3.0-cp38-cp38-win32.whl", hash = "sha256:4125e6704751d0e82d8d912d9851da097e8d38599d4c45f9944faaeb21771938"}, + {file = "pyproj-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b15e199c1da8fd132e11dfa68d8cf65d4812dedabc776b308df778ecd0d07658"}, + {file = "pyproj-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fcceb6736085bf19291b707bc67c8cebe05330bd02268e9b8eba6d28a1905fce"}, + {file = "pyproj-3.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dbf479bd481774ad217e9db5674868eee8f01dfe3868f61753328895ae7da61a"}, + {file = "pyproj-3.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:067a5c6099949edd66e9a10b139af4e2f65ebadb9f59583923a1d3feefac749a"}, + {file = "pyproj-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:235b52d8700ffb6be1f3638b1e25d83a9c13edcdb793236d8a98fd39227c5c27"}, + {file = "pyproj-3.3.0-cp39-cp39-win32.whl", hash = "sha256:44b5590c0b8dd002154916e170ef88f57abf91005b34bcb23faef97abb4d42c2"}, + {file = "pyproj-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b48dd9e5736957707fce1d9253fb0772bcf80480198c7790e21fed73fee61240"}, + {file = "pyproj-3.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5a105bfe37c78416d2641cd5d3368c99057d041f15f8d51ea3898953b21395c9"}, + {file = "pyproj-3.3.0.tar.gz", hash = "sha256:ce8bfbc212729e9a643f5f5d77f7a93394e032eda1e2d8799ae902d08add747e"}, +] +pytesseract = [ + {file = "pytesseract-0.3.9-py2.py3-none-any.whl", hash = "sha256:fecda37d1e4eaf744c657cd03a5daab4eb97c61506ac5550274322c8ae32eca2"}, + {file = "pytesseract-0.3.9.tar.gz", hash = "sha256:7e2bafc7f48d1bb71443ce4633a56f5e21925a98f220a36c336297edcd1956d0"}, +] +pyusb = [ + {file = "pyusb-1.2.1-py3-none-any.whl", hash = "sha256:2b4c7cb86dbadf044dfb9d3a4ff69fd217013dbe78a792177a3feb172449ea36"}, + {file = "pyusb-1.2.1.tar.gz", hash = "sha256:a4cc7404a203144754164b8b40994e2849fde1cfff06b08492f12fff9d9de7b9"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, + {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, +] +typing-extensions = [ + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, +] +wrapt = [ + {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, + {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, + {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, + {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, + {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, + {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, + {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, + {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, + {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, + {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, + {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, + {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, + {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, + {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, + {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, + {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, + {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, + {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, + {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, +] diff --git a/pyproject.toml b/pyproject.toml index 0090274..526178b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "suas-2022" version = "0.1.0" description = "" authors = [] -license = "" +license = "MIT" [tool.poetry.dependencies] python = "^3.8" @@ -16,6 +16,7 @@ lxml = "^4.8.0" colorlog = "^6.6.0" pyproj = "^3.3.0" pyusb = "^1.2.1" +mypy = "^0.931" [tool.poetry.dev-dependencies] black = "^21.12b0" From bbe1e1c435365e4e69ff5e5ee5a0673da3c5556e Mon Sep 17 00:00:00 2001 From: Connor Hallemann Date: Thu, 14 Apr 2022 17:19:25 -0700 Subject: [PATCH 43/45] AirDrop & ODLC States --- flight/states/air_drop.py | 36 ++++++++++++++++++++++++++++++++++ flight/states/emergent_odlc.py | 36 ++++++++++++++++++++++++++++++++++ flight/states/offaxis_odlc.py | 36 ++++++++++++++++++++++++++++++++++ flight/states/standard_odlc.py | 36 ++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 flight/states/air_drop.py create mode 100644 flight/states/emergent_odlc.py create mode 100644 flight/states/offaxis_odlc.py create mode 100644 flight/states/standard_odlc.py diff --git a/flight/states/air_drop.py b/flight/states/air_drop.py new file mode 100644 index 0000000..85b1641 --- /dev/null +++ b/flight/states/air_drop.py @@ -0,0 +1,36 @@ +"""State to maneuver the drone to Air Drop location and release UGV""" +import logging +from mavsdk import System +from ..state_settings import StateSettings +from state import State +from states.land import Land + + +class AirDrop(State): + """ + State to move drone to Air Drop GPS location and drop UGV + + Attributes + ---------- + N/A + + Methods + ------- + run() -> Land + Moves drone to Air Drop GPS location and releases UGV for mission + """ + async def run(self, drone: System) -> Land: + """ + Move drone to Air Drop GPS and release UGV for ground mission + + Parameters + ---------- + drone: System + MAVSDK object for drone control + + Returns + ------- + Land : State + State in which drone returns to takeoff location and lands + """ + return Land(self.state_settings) diff --git a/flight/states/emergent_odlc.py b/flight/states/emergent_odlc.py new file mode 100644 index 0000000..576bbab --- /dev/null +++ b/flight/states/emergent_odlc.py @@ -0,0 +1,36 @@ +"""State to search for Emergent ODLC object""" +import logging +from mavsdk import System +from ..state_settings import StateSettings +from state import State +from states.offaxis_odlc import OffAxisODLC + + +class EmergentODLC(State): + """ + State where drone flies to last known GPS location and searches for Emergent ODLC + + Attributes + ---------- + N/A + + Methods + ------- + run() -> Land + State to fly to last known position and scan for ODLC + """ + async def run(self, drone: System) -> OffAxisODLC: + """ + Flies to last known location and searches around location for Emergent ODLC + + Parameters + ---------- + drone: System + MAVSDK object for drone control + + Returns + ------- + Land : State + State where drone returns to takeoff location and lands + """ + return OffAxisODLC(self.state_settings) \ No newline at end of file diff --git a/flight/states/offaxis_odlc.py b/flight/states/offaxis_odlc.py new file mode 100644 index 0000000..6f9ed1b --- /dev/null +++ b/flight/states/offaxis_odlc.py @@ -0,0 +1,36 @@ +"""State to fly towards Off Axis ODLC for image capture""" +import logging +from mavsdk import System +from ..state_settings import StateSettings +from state import State +from states.land import Land + + +class OffAxisODLC(State): + """ + State that handles Off Axis ODLC by flying to closest location w/in boundary and capture image + + Attributes + ---------- + N/A + + Methods + ------- + run() -> Land + Flies to closest location within flight boundary and record off-axis ODLC + """ + async def run(self, drone: System) -> Land: + """ + Flies to closest point within flight boundary and record image of Off-Axis ODLC + + Parameters + ---------- + drone: System + MAVSDK object for drone control + + Returns + ------- + Land : State + State to return to takeoff location and land the drone + """ + return Land(self.state_settings) \ No newline at end of file diff --git a/flight/states/standard_odlc.py b/flight/states/standard_odlc.py new file mode 100644 index 0000000..7bddd41 --- /dev/null +++ b/flight/states/standard_odlc.py @@ -0,0 +1,36 @@ +"""State to fly in ODLC search grid in set pattern""" +import logging +from mavsdk import System +from ..state_settings import StateSettings +from state import State +from states.land import Land + + +class StandardODLC(State): + """ + State to fly ODLC search grid scanning for standard ODLC objects + + Attributes + ---------- + N/A + + Methods + ------- + run() -> Land + Maneuvers drone in set pattern within ODLC search grid + """ + async def run(self, drone: System) -> Land: + """ + Flies drone in ODLC search grid in efficient pattern + + Parameters + ---------- + drone: System + MAVSDK object for drone control + + Returns + ------- + Land : State + State in which drone returns to takeoff location and lands + """ + return Land(self.state_settings) \ No newline at end of file From 5c5a33f1d81b68a14c95c3b0294be376d376bb38 Mon Sep 17 00:00:00 2001 From: Connor Hallemann Date: Thu, 14 Apr 2022 17:36:56 -0700 Subject: [PATCH 44/45] Revert "Documentation updates" This reverts commit 17d9c3a559cc1d0b517a4fdb63d92f13d37e8292. --- flight/state_settings.py | 171 +++++++++----------------------- flight/states/final_state.py | 30 ++---- flight/states/land.py | 30 ++---- flight/states/pre_processing.py | 32 ++---- flight/states/start_state.py | 34 ++----- flight/states/state.py | 79 +++++---------- flight/states/takeoff.py | 30 ++---- flight/states/waypoints.py | 28 ++---- flight/util/json_parsing.py | 50 ++-------- 9 files changed, 136 insertions(+), 348 deletions(-) diff --git a/flight/state_settings.py b/flight/state_settings.py index d1c852c..322dfdc 100644 --- a/flight/state_settings.py +++ b/flight/state_settings.py @@ -8,58 +8,30 @@ class StateSettings: """ Initialize settings for state machine - - Attributes - ---------- + Functions: + __init__: Sets preliminary values for SUAS overheads + simple_takeoff() -> None: Enables access to status of simple_takeoff + -> bool: Setter to determine if testing takeoff procedure desired + num_waypoints() -> None: Enables access to number of waypoints in the competition + -> int: Sets the number of waypoints in the competition + run_title() -> None: Enables access to set name of the current flight + -> str: Sets the name of the current flight for logging + run_description() -> None: Enables access to description of current flight mission + -> str: Sets the description of current flight mission + Member Variables: __num_waypoints: Number of waypoints on flight plan __run_title: Title of Competition __run_description: Description for Competition - - Methods - ------- - __init__ - Sets preliminary values for SUAS overheads - - @Setter - ------- - simple_takeoff() -> None - Enables access to status of simple_takeoff - num_waypoints() -> None - Enables access to number of waypoints in the competition - run_title() -> None - Enables access to set name of the current flight - run_description() -> None - Enables access to description of current flight mission - - @Property - -------- - simple_takeoff() -> bool - Setter to determine if testing takeoff procedure desired - num_waypoints() -> int - Sets the number of waypoints in the competition - run_title() -> str - Sets the name of the current flight for logging - run_description() -> str - Sets the description of current flight mission - """ def __init__(self, takeoff_bool: bool = False, num_waypoints: int = DEFAULT_WAYPOINTS, title: str = DEFAULT_RUN_TITLE, description: str = DEFAULT_RUN_DESCRIPTION) -> None: """Default constructor results in default settings - - Parameters - ---------- - takeoff_bool: bool - determines if a simple takeoff procedure should be executed for testing - num_waypoints: int - Number of waypoints, extracted from SUAS mission plan - title: str - Title of current flight mission, for logging purposes - description: str - Description of current flight mission, for logging purposes - - Returns - ------- + Args: + takeoff_bool: bool - determines if a simple takeoff procedure should be executed for testing + num_waypoints: int - Number of waypoints, extracted from SUAS mission plan + title: str - Title of current flight mission, for logging purposes + description: str - Description of current flight mission, for logging purposes + Returns: None """ self.__simple_takeoff: bool = takeoff_bool @@ -71,30 +43,19 @@ def __init__(self, takeoff_bool: bool = False, num_waypoints: int = DEFAULT_WAYP @property def simple_takeoff(self) -> bool: """Establishes simple_takeoff as a private member variable - - Parameters - ---------- + Args: N/A - - Returns - ------- - __simple_takeoff: bool - Status of simple_takeoff variable + Returns: + bool: Status of simple_takeoff variable """ return self.__simple_takeoff @simple_takeoff.setter def simple_takeoff(self, simple_takeoff: bool) -> None: - """ - Setter for whether to perform simple takeoff instead of regular takeoff - - Parameters - ---------- - simple_takeoff: bool - True for drone to go straight up, False to behave normally - - Returns - ------- + """Setter for whether to perform simple takeoff instead of regular takeoff + Args: + simple_takeoff: bool - True for drone to go straight up, False to behave normally + Returns: None """ self.__simple_takeoff = simple_takeoff @@ -102,32 +63,20 @@ def simple_takeoff(self, simple_takeoff: bool) -> None: # ---- Waypoint Settings ---- # @property def num_waypoints(self) -> int: - """ - Establishes num_waypoints as private member variable - - Parameters - ---------- + """Establishes num_waypoints as private member variable + Args: N/A - - Returns - ------- - __num_waypoints: int - Number of waypoints in the competition + Returns: + int: Number of waypoints in the competition """ return self.__num_waypoints @num_waypoints.setter def num_waypoints(self, waypoints: int) -> None: - """ - Set the number of waypoints given by Interop System - - Parameters - ---------- - waypoints: int - Number of waypoints present in mission plan - - Returns - ------- + """Set the number of waypoints given by Interop System + Args: + waypoints: int - Number of waypoints present in mission plan + Returns: None """ self.__num_waypoints = waypoints @@ -135,64 +84,40 @@ def num_waypoints(self, waypoints: int) -> None: # ---- Other settings ---- # @property def run_title(self) -> str: - """ - Set a title for the run/test to be output in logging - - Parameters - ---------- + """Set a title for the run/test to be output in logging + Args: N/A - - Returns - ------- - __run_title: str - Created title for flight mission + Returns: + str: Created title for flight mission """ return self.__run_title @run_title.setter def run_title(self, title: str) -> None: - """ - Sets the title of the flight mission for logging - - Parameters - ---------- - title: str - Desired title for the flight - - Returns - ------- + """Sets the title of the flight mission for logging + Args: + title: str - Desired title for the flight + Returns: None """ self.__run_title = title @property def run_description(self) -> str: - """ - Set a description for the run/test to be output in logging - - Parameters - ---------- + """Set a description for the run/test to be output in logging + Args: N/A - - Returns - ------- - __run_description: str - Desired description for flight mission + Returns: + str: Desired description for flight mission """ return self.__run_description @run_description.setter def run_description(self, description: str) -> None: - """ - Sets a description of the flight mission - - Parameters - ---------- - description: str - Written description for flight mission for logs - - Returns - ------- + """Sets a description of the flight mission + Args: + description: str - Written description for flight mission for logs + Returns: None """ self.__run_description = description diff --git a/flight/states/final_state.py b/flight/states/final_state.py index 6a91db0..98781df 100644 --- a/flight/states/final_state.py +++ b/flight/states/final_state.py @@ -6,29 +6,17 @@ class Final(State): - """ - Empty state to end flight - - Attributes - ---------- - N/A - - Methods - ------- - run() -> None - Ends the state machine + """Empty state to end flight + Functions: + run() -> None: Ends the state machine + Member Variables: + None """ async def run(self, drone: System) -> None: - """ - Do nothing and end - - Parameters - ---------- - drone: System - drone object that must be passed to any function with control of the drone - - Returns - ------- + """Do nothing and end + Args: + drone: System - drone object that must be passed to any function with control of the drone + Returns: None """ return diff --git a/flight/states/land.py b/flight/states/land.py index e1f53a2..0469db1 100644 --- a/flight/states/land.py +++ b/flight/states/land.py @@ -9,28 +9,16 @@ class Land(State): """ State to land the drone safely after finishing other flight & vision tasks - - Attributes - ---------- - N/A - - Methods - ------- - run() -> Final - Running the landing procedure after returning to home + Functions: + run() -> Final: Running the landing procedure after returning to home + Member Variables: + None """ async def run(self, drone: System) -> Final: - """ - Run landing function to have drone slowly return to home position - - Parameters - ---------- - drone: System - MAVSDK drone object for direct drone control - - Returns - ------- - Final : State - final state for ending state machine + """Run landing function to have drone slowly return to home position + Args: + drone: System - MAVSDK drone object for direct drone control + Returns: + Final: final state for ending state machine """ return Final(self.state_settings) diff --git a/flight/states/pre_processing.py b/flight/states/pre_processing.py index 7723b15..980d915 100644 --- a/flight/states/pre_processing.py +++ b/flight/states/pre_processing.py @@ -9,29 +9,17 @@ class PreProcess(State): """ State to accept data and plan out optimal mission structure based on relative distances - - Attributes - ---------- - N/A - - Methods - ------- - data_retrieval() -> Takeoff - Connects to Interop and downloads JSON data from system - + Functions: + data_retrieval() -> Takeoff: Connects to Interop and downloads JSON data from system + ... # Will add more functions as needed during dev + Member Variables: + None """ async def data_retrieval(self, drone: System) -> Takeoff: - """ - Prelim function to accept data - - Parameters - ---------- - drone: System - MAVSDK drone object for direct drone control - - Returns - ------- - Takeoff : State - the next state, the Takeoff state to advance state machine + """Prelim function to accept data + Args: + drone: System - MAVSDK drone object for direct drone control + Returns: + Takeoff: the next state, the Takeoff state to advance state machine """ return Takeoff(self.state_settings) diff --git a/flight/states/start_state.py b/flight/states/start_state.py index d3b0c2e..28794c1 100644 --- a/flight/states/start_state.py +++ b/flight/states/start_state.py @@ -7,31 +7,17 @@ class Start(State): - """ - Preliminary state, proceed state machine into pre-processing - - Attributes - ---------- - N/A - - Methods - ------- - begin() -> PreProcess - Beginning function to start state machine, proceeding to pre-processing step - + """Preliminary state, proceed state machine into pre-processing + Functions: + begin() -> PreProcess: beginning function to start state machine, proceeding to pre-processing step + Member Variables: + None """ async def begin(self, drone: System) -> PreProcess: - """ - Initialization function to start flight state machine - - Parameters - ---------- - drone: System - drone object for directv drone control - - Returns - ------- - PreProcess : State - data pre-processing state, to advance state machine + """Initialization function to start flight state machine + Args: + drone: System - drone object for directv drone control + Returns: + PreProcess: data pre-processing state, to advance state machine """ return PreProcess(self.state_settings) diff --git a/flight/states/state.py b/flight/states/state.py index 3e295c0..1a3fbe0 100644 --- a/flight/states/state.py +++ b/flight/states/state.py @@ -7,68 +7,40 @@ class State: """ Base State class - - Attributes - ---------- - drone (System) - The drone object; used for flight. - - Methods - ------- - run() -> None - Performs all movement operations contained within the state - _check_arm_or_arm() -> None - Determines if drone is armed, and if not, arms it. - + Functions: + run() -> (State, None): Runs the code present in each state and returns the next state, or None if at the + end of the machine + _check_arm_or_arm() -> None: Determines if drone is armed, and if not, arms it. + Member Variables: + drone (System): The drone object; used for flight. """ def __init__(self, state_settings: StateSettings) -> None: - """ - Initializes base State class using a StateSettings class - - Parameters - ---------- - state_settings: StateSettings - class which contains basic competition data - - Returns - ------- + """ Initializes base State class using a StateSettings class + Args: + state_settings: StateSettings - class which contains basic competition data + Returns: None """ logging.info('State "%s" has begun', self.name) self.state_settings: StateSettings = state_settings async def run(self, drone: System) -> None: - """ - Runs the functions and code present in state for competition goals - - Parameters - ---------- - drone: System - drone object for MAVSDK control - - Returns - ------- + """ Runs the functions and code present in state for competition goals + Args: + drone: System - drone object for MAVSDK control + Returns: None - - Raises - ------ - General - raises if no code is present in run function + Exception: + General: raises if no code is present in run function """ raise Exception("Run not implemented for state") async def _check_arm_or_arm(self, drone: System) -> None: - """ - Verifies that the drone is armed, if not armed, arms the drone - - Parameters - ---------- - drone: System - The drone system; used for flight operations. - - Returns - ------- + """ Verifies that the drone is armed, if not armed, arms the drone + Args: + drone: System - The drone system; used for flight operations. + Returns: None """ async for is_armed in drone.telemetry.armed(): @@ -81,15 +53,10 @@ async def _check_arm_or_arm(self, drone: System) -> None: @property def name(self): - """ - Getter function to return the name of the class - - Parameters - ---------- + """ Getter function to return the name of the class + Args: N/A - - Returns - ------- + Returns: Name property of current state class """ return type(self).__name__ diff --git a/flight/states/takeoff.py b/flight/states/takeoff.py index 42f6977..78a2884 100644 --- a/flight/states/takeoff.py +++ b/flight/states/takeoff.py @@ -9,28 +9,16 @@ class Takeoff(State): """ Runs manual takeoff process, to lift drone to 100ft altitude - - Attributes - ---------- - N/A - - Methods - ------- - run() -> Waypoints - runs drone movement functions to rise to 100ft + Functions: + run() -> Waypoints: runs drone movement functions to rise to 100ft + Member Variables: + None """ async def run(self, drone: System) -> Waypoints: - """ - Run offboard functions to have the drone takeoff - - Parameters - ---------- - drone: System - MAVSDK drone object for direct drone control - - Returns - ------- - Waypoints : State - Next state, Waypoints state to perform waypoint flight plan + """Run offboard functions to have the drone takeoff + Args: + drone: System - MAVSDK drone object for direct drone control + Returns: + Waypoints: next state, Waypoints state to perform waypoint flight plan """ return Waypoints(self.state_settings) diff --git a/flight/states/waypoints.py b/flight/states/waypoints.py index bad6a54..2c94705 100644 --- a/flight/states/waypoints.py +++ b/flight/states/waypoints.py @@ -9,29 +9,17 @@ class Waypoints(State): """ State to fly to each waypoint in mission plan, using Obstacle Avoidance during flight - - Attributes - ---------- - N/A - - Methods - ------- - run() -> Land - For each waypoint, flies to Latlon & avoids any obstacles - + Functions: + run() -> Land: For each waypoint, flies to Latlon & avoids any obstacles + Member Variables: + None """ async def run(self, drone: System) -> Land: - """ - Run waypoint path plan with obstacle avoidance running throughout - - Parameters - ---------- + """Run waypoint path plan with obstacle avoidance running throughout + Args: drone: System - MAVSDK drone object for direct drone control - - Returns - ------- - Land : State - landing state to land drone after finishing tasks # This will change once other tasks become + Returns: + Land: landing state to land drone after finishing tasks # This will change once other tasks become testable within state machine """ return Land(self.state_settings) diff --git a/flight/util/json_parsing.py b/flight/util/json_parsing.py index 28b7d38..d73ccdd 100644 --- a/flight/util/json_parsing.py +++ b/flight/util/json_parsing.py @@ -4,26 +4,11 @@ def waypoint_parsing(filename: str) -> List[Dict[str, float]]: - """ - Accepts name of JSON file and extracts waypoint data for SUAS mission - - Parameters - --------- - filename: str - String of data file to open and access waypoint data - - Returns - ------- - List[Dict[str, float]] - List of dictionaries containing latitude, longitude and altitude of each waypoint in mission - Dict[str, float] - dictionary containing specific coordinate or attribute with string label - - Raises - ------ - General Exception - Designed to catch any error that could arise and prevent corruption - + """Accepts name of JSON file and extracts waypoint data for SUAS mission + Args: + filename: str - String of data file to open and access waypoint data + Returns: + List - List of dictionaries containing latitude, longitude and altitude of each waypoint in mission """ with open(filename) as f: try: @@ -38,26 +23,11 @@ def waypoint_parsing(filename: str) -> List[Dict[str, float]]: def stationary_obstacle_parsing(filename: str) -> List[Dict[str, float]]: - """ - Opens passed JSON file and extracts the Stationary obstacle attributes - - Parameters - ---------- - filename: str - String of JSON file name and file type - - Returns - ------- - stationary_obs: List[Dict[str, float]] - list of dictionaries containing latitude, longitude, radius, and height of obstacles - Dict[str, float] - dictionary containing specific coordinate or attribute with string label - - Raises - ------ - General Exception - Broad exception to catch any error and prevent corruption - + """Opens passed JSON file and extracts the Stationary obstacle attributes + Args: + filename: str - String of JSON file name and file type + Returns: + List - list of dictionaries containing latitude, longitude, radius, and height of obstacles """ with open(filename) as f: try: From 1a6f2e0fd47b392f478d893b0b5fae6a57472eb0 Mon Sep 17 00:00:00 2001 From: Connor Hallemann Date: Thu, 14 Apr 2022 17:59:27 -0700 Subject: [PATCH 45/45] Documentation --- flight/util/json_parsing.py | 42 ++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/flight/util/json_parsing.py b/flight/util/json_parsing.py index d73ccdd..b060c50 100644 --- a/flight/util/json_parsing.py +++ b/flight/util/json_parsing.py @@ -4,11 +4,22 @@ def waypoint_parsing(filename: str) -> List[Dict[str, float]]: - """Accepts name of JSON file and extracts waypoint data for SUAS mission - Args: - filename: str - String of data file to open and access waypoint data - Returns: - List - List of dictionaries containing latitude, longitude and altitude of each waypoint in mission + """ + Accepts name of JSON file and extracts waypoint data for SUAS mission + Parameters + --------- + filename: str + String of data file to open and access waypoint data + Returns + ------- + List[Dict[str, float]] + List of dictionaries containing latitude, longitude and altitude of each waypoint in mission + Dict[str, float] + dictionary containing specific coordinate or attribute with string label + Raises + ------ + General Exception + Designed to catch any error that could arise and prevent corruption """ with open(filename) as f: try: @@ -23,11 +34,22 @@ def waypoint_parsing(filename: str) -> List[Dict[str, float]]: def stationary_obstacle_parsing(filename: str) -> List[Dict[str, float]]: - """Opens passed JSON file and extracts the Stationary obstacle attributes - Args: - filename: str - String of JSON file name and file type - Returns: - List - list of dictionaries containing latitude, longitude, radius, and height of obstacles + """ + Opens passed JSON file and extracts the Stationary obstacle attributes + Parameters + ---------- + filename: str + String of JSON file name and file type + Returns + ------- + stationary_obs: List[Dict[str, float]] + list of dictionaries containing latitude, longitude, radius, and height of obstacles + Dict[str, float] + dictionary containing specific coordinate or attribute with string label + Raises + ------ + General Exception + Broad exception to catch any error and prevent corruption """ with open(filename) as f: try: