Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions assets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import darkdetect

from src.utils import get_file_path
from src.path_utils import get_file_path

ASSET_DIRPATH = path.dirname(path.abspath(__file__))

Expand All @@ -20,7 +20,7 @@ def get_icon(
sys_platform = platform.system()
if sys_platform == "Linux":
# Do this since linux doesn't support icons like windows and mac
return None, None
return None, None
is_dark_mode = (
override_is_dark_mode or darkdetect.isDark()
if darkdetect.isDark() is not None
Expand Down
15 changes: 10 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies = [
"pyobjc-framework-Cocoa; sys_platform == 'darwin'",
"pyvirtualcam",
"watchdog>=6.0.0",
"lapx>=0.9.4",
]
name = "commander"
requires-python = "==3.12.*"
Expand All @@ -41,7 +42,13 @@ commander-tk = "talos:tk_interface"
commander-pyside = "talos:pyside_interface"

[dependency-groups]
dev = ["pyinstaller>=6.16.0", "textual-dev>=1.8.0", "pytest", "pytest-cov", "pytest-mock"]
dev = [
"pyinstaller>=6.16.0",
"textual-dev>=1.8.0",
"pytest",
"pytest-cov",
"pytest-mock",
]

[project.optional-dependencies]
mediapipe = [
Expand Down Expand Up @@ -74,7 +81,5 @@ include = ["src*", "assets*", "config*"]
addopts = ["--cov=src", "--cov-report=html"]

[tool.coverage.run]
omit =[
"tests/*"
]
relative_files = true
omit = ["tests/*"]
relative_files = true
3 changes: 2 additions & 1 deletion src/arg_parser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import argparse
from src.model_options import MODEL_OPTIONS

from src.tracking import MODEL_OPTIONS


def _create_arg_parser():
Expand Down
2 changes: 1 addition & 1 deletion src/config/path.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os

from src.utils import get_file_path
from src.path_utils import get_file_path

_CONFIG_DIR = os.path.join(os.path.dirname(__file__), "../../config")
ROBOT_CONFIG_FILENAME = "robot_configs.local.yaml"
Expand Down
3 changes: 1 addition & 2 deletions src/config/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
ROBOT_CONFIGS_PATH,
)
from src.config.schema.app import AppSettings

from ..utils import get_file_path
from src.path_utils import get_file_path


def read_default_robot_config() -> dict[str, Any]:
Expand Down
23 changes: 17 additions & 6 deletions src/config/schema/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
from pydantic import Field
from pydantic_settings import (
BaseSettings,
PydanticBaseSettingsSource,
YamlConfigSettingsSource,
CliSettingsSource,
PydanticBaseSettingsSource,
SettingsConfigDict,
YamlConfigSettingsSource,
)

from src.arg_parser import ARG_PARSER
from src.config.path import APP_SETTINGS_DEFAULT_PATH, APP_SETTINGS_PATH


def _parse_args(root_parser: argparse.ArgumentParser, args) -> argparse.Namespace:
"""
Implement overrides for App Settings that are triggered by providing certain cli args
Expand All @@ -27,6 +27,7 @@ def _parse_args(root_parser: argparse.ArgumentParser, args) -> argparse.Namespac
args.draw_bboxes = True
return args


class AppSettings(BaseSettings):
"""
Application-wide settings that are not specific to individual connections.
Expand All @@ -38,7 +39,9 @@ class AppSettings(BaseSettings):
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
yaml_file=APP_SETTINGS_PATH if os.path.exists(APP_SETTINGS_PATH) else APP_SETTINGS_DEFAULT_PATH,
yaml_file=APP_SETTINGS_PATH
if os.path.exists(APP_SETTINGS_PATH)
else APP_SETTINGS_DEFAULT_PATH,
yaml_file_encoding="utf-8",
cli_parse_args=True,
cli_kebab_case=True,
Expand All @@ -53,15 +56,19 @@ def settings_customise_sources(
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
from src.arg_parser import ARG_PARSER

return (
init_settings,
CliSettingsSource(settings_cls, root_parser=ARG_PARSER, parse_args_method=_parse_args),
CliSettingsSource(
settings_cls, root_parser=ARG_PARSER, parse_args_method=_parse_args
),
YamlConfigSettingsSource(settings_cls),
dotenv_settings,
env_settings,
)

log_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = Field(
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(
default="INFO",
description="Logging level (e.g., DEBUG, INFO, WARNING, ERROR)",
)
Expand All @@ -73,3 +80,7 @@ def settings_customise_sources(
default=30,
description="Frames per second for pulling video streams (can be lower than max_fps to reduce load)",
)
disable_performance_warnings: bool = Field(
default=False,
description="Whether to disable warnings about performance issues (e.g., if processing is taking too long and frames are being dropped or the opposite)",
)
7 changes: 4 additions & 3 deletions src/config/watchers/app_settings_file_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
FileSystemEventHandler,
)

from ...utils import get_file_path
from src.path_utils import get_file_path

from ..path import APP_SETTINGS_PATH, BACKUP_DIR
from ..schema.app import AppSettings

Expand Down Expand Up @@ -65,7 +66,7 @@ def on_deleted(self, event: DirDeletedEvent | FileDeletedEvent) -> None:
logger.info(f"Backed up current app settings to {path}")

def on_created(self, event: DirCreatedEvent | FileCreatedEvent):
self.on_modified(event)
self.on_modified(event) # pyright: ignore[reportArgumentType]

def on_moved(self, event: DirMovedEvent | FileMovedEvent):
self.on_modified(event)
self.on_modified(event) # pyright: ignore[reportArgumentType]
6 changes: 3 additions & 3 deletions src/config/watchers/robot_config_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
FileSystemEventHandler,
)

from ...utils import get_file_path
from ..path import BACKUP_DIR, ROBOT_CONFIGS_PATH
from ..schema.robot import RobotConfigs
from src.config.path import BACKUP_DIR, ROBOT_CONFIGS_PATH
from src.config.schema.robot import RobotConfigs
from src.path_utils import get_file_path


def take_backup() -> str:
Expand Down
6 changes: 3 additions & 3 deletions src/connection/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ class Direction(IntEnum):
"""Directional Enum for interface controls

The values can be summed to get combined directions(from -4 to 4)
-4: DL, -3: L, -2: UL, -1: D, 0: None, 1: U, 2: DR, 3: R, 4: UR
-4: DR, -3: R, -2: UR, -1: D, 0: None, 1: U, 2: DL, 3: L, 4: UL
"""

UP = 1
DOWN = -1
RIGHT = 3
LEFT = -3
LEFT = 3
RIGHT = -3

@staticmethod
def toDirectionTuple(sum_direction: int) -> tuple[int, int]:
Expand Down
22 changes: 11 additions & 11 deletions src/directors/continuous_director.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from loguru import logger

import src.config as config
from src.connection.publisher import Publisher
from src.connection.publisher import Direction, Publisher
from src.directors.base_director import BaseDirector
from src.utils import calculate_acceptable_box, calculate_center_bbox

Expand Down Expand Up @@ -83,27 +83,27 @@ def process_frame(
center_bottom = center + frame_buffer

if average < center_top:
logger.info(f"Move camera up: Average:{average} Center:{center}")
logger.debug(f"Move camera up: Average:{average} Center:{center}")
change_in_y = average - center_top
elif average > center_bottom:
logger.info(f"Move camera down: Average:{average} Center:{center}")
logger.debug(f"Move camera down: Average:{average} Center:{center}")
change_in_y = average - center_bottom

if change_in_x > 0:
publisher.polar_pan_continuous_start(-1, 0)
# print("start")
publisher.polar_pan_continuous_direction_start(Direction.RIGHT)
logger.debug("start right")
self.last_command_stop = False
elif change_in_x < 0:
publisher.polar_pan_continuous_start(1, 0)
# print("start")
publisher.polar_pan_continuous_direction_start(Direction.LEFT)
logger.debug("start left")
self.last_command_stop = False
elif change_in_y < 0:
publisher.polar_pan_continuous_start(0, 1)
# print("start")
publisher.polar_pan_continuous_direction_start(Direction.UP)
logger.debug("start up")
self.last_command_stop = False
elif change_in_y > 0:
publisher.polar_pan_continuous_start(0, -1)
# print("start")
publisher.polar_pan_continuous_direction_start(Direction.DOWN)
logger.debug("start down")
self.last_command_stop = False
elif not self.last_command_stop:
publisher.polar_pan_continuous_stop()
Expand Down
2 changes: 1 addition & 1 deletion src/interface/pyside_gui/main_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from src.interface.pyside_gui.qtwidgets import Toggle
from src.interface.pyside_gui.styles import get_main_stylesheet
from src.talos_app import App, ControlMode
from src.tracking import MODEL_OPTIONS
from src.tracking.options import MODEL_OPTIONS
from src.utils import (
add_termination_handler,
remove_termination_handler,
Expand Down
46 changes: 0 additions & 46 deletions src/model_options.py

This file was deleted.

18 changes: 18 additions & 0 deletions src/path_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import os.path as path
import sys


def get_file_path(relative_path: str) -> str:
"""
Get the absolute file path for a given relative path. This method is necessary
to ensure the program works when compiled with PyInstaller.

Args:
relative_path (str): The relative path to the file.

Returns:
str: The absolute file path if the program is compiled with PyInstaller, otherwise the relative path.
"""
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
return path.join(sys._MEIPASS, relative_path) # pyright: ignore[reportAttributeAccessIssue]
return relative_path
4 changes: 2 additions & 2 deletions src/streaming/streamer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ def draw_visuals(bboxes, frame): # -> Any:

for box in bboxes:
# Draw each bbox
x1, y1, x2, y2 = box
x1, y1, x2, y2 = map(int, box)
cv2.rectangle(frame, (x1, y1), (x2, y2), BBOX_COLOR.GREEN.value, 2)

bbox_center_x, bbox_center_y = calculate_center_bbox(box)
bbox_center_x, bbox_center_y = map(int, calculate_center_bbox(box))
# Draw center of bounding box dot, this the value the commander is using
cv2.circle(
frame,
Expand Down
4 changes: 1 addition & 3 deletions src/talos_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,6 @@ def change_model(self, option: str | None = None) -> bool:
if option is not None and len(self.connections) == 0:
logger.warning("No connections available, skipping model initialization")
return False
if self.director is not None and self.director.is_active():
self.director.stop_auto_control()
logger.info("Director stopped")
if option is None:
self.tracker.swap_model(None)
self.model_selection = None
Expand Down Expand Up @@ -267,6 +264,7 @@ def set_manual_control(self, manual: bool) -> None:
"""Sets the active connection's manual/automatic control mode"""
if self.director is None:
return logger.error("No active director")
logger.debug("Setting manual control to {}", manual)
self.director.set_manual_control(manual=manual)

def set_control_mode(self, ctrl_mode: ControlMode) -> ControlMode:
Expand Down
48 changes: 1 addition & 47 deletions src/tracking/__init__.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,3 @@
from loguru import logger
from src.model_options import MODEL_OPTIONS, ModelOption

from .detector import ObjectModel
from .haar_cascade.basic_model import BasicModel
from .options import MODEL_OPTIONS, USABLE_MODELS, ModelOption

__all__ = ["ModelOption", "MODEL_OPTIONS", "USABLE_MODELS"]


USABLE_MODELS: dict[str, ObjectModel.__class__] = {}

# haar_cascade is imported via opencv-python by default
USABLE_MODELS["basic"] = BasicModel

try:
from .media_pipe import MediaPipeModel, MediaPipePoseModel

# USABLE_MODELS[ModelOption.KEEPAWAY] = (KeepAwayModel, KeepAwayDirector)
USABLE_MODELS[ModelOption.MEDIAPIPEPOSE] = MediaPipeModel
USABLE_MODELS[ModelOption.MEDIAPIPE] = MediaPipePoseModel
except ImportError as e:
logger.warning(
f"""{e}
Failed to import mediapipe. This is an optional import, but may limit the ability to run this model.
This can be installed using `uv sync --extra mediapipe` or `uv sync --all-extras`"""
)

try:
from .yolo.model import (
YOLOLargeModel,
YOLOMediumModel,
YOLONanoModel,
YOLOSmallModel,
YOLOXLargeModel,
)

USABLE_MODELS[ModelOption.YOLO_NANO] = YOLONanoModel
USABLE_MODELS[ModelOption.YOLO_SMALL] = YOLOSmallModel
USABLE_MODELS[ModelOption.YOLO_MEDIUM] = YOLOMediumModel
USABLE_MODELS[ModelOption.YOLO_LARGE] = YOLOLargeModel
USABLE_MODELS[ModelOption.YOLO_XLARGE] = YOLOXLargeModel
except ImportError as e:
logger.warning(
f"""{e}
Failed to import Yolo model. This is an optional import, but may limit the ability to run this model.
This can be installed using `uv sync --extra yolo` or `uv sync --all-extras`"""
)


Loading