Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7-dev] use the new ml pipeline #9

Merged
merged 7 commits into from
Mar 30, 2024
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
1 change: 0 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ show_missing=true
[run]
omit =
cameratokeyboard/app/ui.py
ci_train_and_upload.py
*/__init__.py
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
.vscode
yolov8n.pt
.coverage
/dist
/dist
/cameratokeyboard/model.pt
7 changes: 5 additions & 2 deletions c2k.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# pylint: disable=missing-function-docstring
import asyncio
import sys


from cameratokeyboard.config import Config
from cameratokeyboard.args import parse_args
from cameratokeyboard.app import App
from cameratokeyboard.model.model_downloader import ModelDownloader

args = parse_args(sys.argv[1:])


async def async_main():
app = App(Config.from_args(args))
await app.run()
config = Config.from_args(args)
ModelDownloader(config).run()
await App(config).run()


def main():
Expand Down
7 changes: 3 additions & 4 deletions cameratokeyboard/app/detector.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import logging
import os

import ultralytics

from cameratokeyboard.config import Config
from cameratokeyboard.core.detected_frame import DetectedFrame

MODEL_PATH = os.path.join(os.path.dirname(__file__), "..", "model.pt")
from cameratokeyboard.model.model_downloader import ModelDownloader


class Detector:
Expand All @@ -19,9 +17,10 @@ class Detector:

def __init__(self, config: Config) -> None:
logging.getLogger("ultralytics").setLevel(logging.ERROR)
model_path = ModelDownloader(config).local_path_to_latest_model

self._config = config
self._model = ultralytics.YOLO(MODEL_PATH)
self._model = ultralytics.YOLO(model_path)
self._device = config.processing_device
self._iou = config.iou
self._detected_frame = None
Expand Down
42 changes: 38 additions & 4 deletions cameratokeyboard/config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# pylint: disable=missing-function-docstring,too-many-instance-attributes
from dataclasses import dataclass
import os

import platformdirs

EXCLUDED_KEYS = ["command"]
PRIVATE_KEYS = ["model_path"]


@dataclass
class Config: # pylint: disable=too-many-instance-attributes
class Config:
"""
Application wide configuration.
"""
Expand All @@ -20,8 +24,6 @@ class Config: # pylint: disable=too-many-instance-attributes
image_extension: str = "jpg"
iou: float = 0.5

model_path: str = os.path.join("cameratokeyboard", "model.pt")

resolution: tuple = (1280, 720)
app_fps: int = 30
video_input_device: int = 0
Expand All @@ -35,9 +37,41 @@ class Config: # pylint: disable=too-many-instance-attributes
keyboard_layout: str = "qwerty"
repeating_keys_delay: float = 0.5

remote_models_bucket_region: str = "eu-west-2"
remote_models_bucket_name: str = "c2k"
remote_models_prefix: str = "models/"
models_dir: str = os.path.join(platformdirs.user_data_dir(), "c2k", "models")
_model_path: str = None

@property
def model_path(self) -> str:
if self._model_path:
return self._model_path

models = [x for x in os.listdir(self.models_dir) if x.endswith(".pt")]

def sort_key(model_name):
return os.path.getctime(os.path.join(self.models_dir, model_name))

models = sorted(models, key=sort_key, reverse=True)
try:
return os.path.join(self.models_dir, models[0])
except IndexError:
return None

@model_path.setter
def model_path(self, value):
self._model_path = value

@classmethod
def from_args(cls, args: dict) -> "Config":
"""
Builds a Config object from the given arguments.
"""
return cls(**{k: v for k, v in args.items() if k not in EXCLUDED_KEYS})
processed_args = {k: v for k, v in args.items() if k not in EXCLUDED_KEYS}
for private_key in PRIVATE_KEYS:
if private_key in processed_args:
processed_args[f"_{private_key}"] = processed_args[private_key]
del processed_args[private_key]

return cls(**processed_args)
Binary file removed cameratokeyboard/model.pt
Binary file not shown.
26 changes: 23 additions & 3 deletions cameratokeyboard/model/augmenter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# DEPRECATED: This and all related files will be removed once we move to the new data pipeline
# pylint: skip-file
# pylint: disable=too-many-locals

from ast import literal_eval
import os
Expand All @@ -24,16 +23,37 @@


class ImageAugmenterStrategy:
"""
A class representing an image augmentation strategy.

Attributes:
augmentation_strategies (List[List[ImageAugmenter]]): A list of lists of
ImageAugmenter objects representing the augmentation strategies to be applied.
images_path (str): The path to the directory containing the input images.
labels_path (str): The path to the directory containing the label files.
files (List[str]): A list of filenames in the images_path directory.
"""

def __init__(self, config: Config) -> None:
"""
Initializes an ImageAugmenterStrategy object.

Args:
config (Config): The configuration object containing the necessary parameters.

"""
self.augmentation_strategies = []

self._resolve_strategies()
train_path = config.split_paths[0]
self.images_path = os.path.join(config.dataset_path, "images", train_path)
self.labels_path = os.path.join(config.dataset_path, "labels", train_path)
self.files = [f for f in os.listdir(self.images_path)]
self.files = list(os.listdir(self.images_path))

def run(self) -> None:
"""
Runs the strategy
"""
for strategy in self.augmentation_strategies:
self._run_strategy(strategy)

Expand Down
112 changes: 108 additions & 4 deletions cameratokeyboard/model/augmenters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# DEPRECATED: This and all related files will be removed once we move to the new data pipeline
# pylint: skip-file

from abc import ABC, abstractmethod
from typing import Tuple, Union

Expand All @@ -10,11 +7,25 @@


class ImageAugmenter(ABC):
"""
Abstract base class for image augmenters.
"""

@abstractmethod
def __init__(self, *args, **kwargs):
pass

def apply(self, image: np.ndarray, bounding_boxes: str) -> Tuple[np.ndarray, str]:
"""
Apply image augmentation to the input image and bounding boxes.

Args:
image (np.ndarray): The input image.
bounding_boxes (str): The bounding boxes in string format.

Returns:
Tuple[np.ndarray, str]: The augmented image and the serialized bounding boxes.
"""
bbs = self.parse_bounding_boxes(bounding_boxes, image.shape)
aug_image, aug_bounding_boxes = self.augmenter(image=image, bounding_boxes=bbs)

Expand All @@ -23,6 +34,16 @@ def apply(self, image: np.ndarray, bounding_boxes: str) -> Tuple[np.ndarray, str
def parse_bounding_boxes(
self, bounding_boxes: str, image_shape: Tuple[int, int]
) -> BoundingBoxesOnImage:
"""
Parse the bounding boxes from the string format to BoundingBoxesOnImage object.

Args:
bounding_boxes (str): The bounding boxes in string format.
image_shape (Tuple[int, int]): The shape of the input image.

Returns:
BoundingBoxesOnImage: The parsed bounding boxes.
"""
if not bounding_boxes:
return None

Expand All @@ -45,7 +66,17 @@ def parse_bounding_boxes(

return BoundingBoxesOnImage(parsed_bounding_boxes, shape=image_shape)

def serialize_bounding_boxes(bounding_boxes: BoundingBoxesOnImage) -> str:
@classmethod
def serialize_bounding_boxes(cls, bounding_boxes: BoundingBoxesOnImage) -> str:
"""
Serialize the bounding boxes from BoundingBoxesOnImage object to string format.

Args:
bounding_boxes (BoundingBoxesOnImage): The bounding boxes.

Returns:
str: The serialized bounding boxes.
"""
if not bounding_boxes:
return None

Expand All @@ -65,6 +96,16 @@ def serialize_bounding_boxes(bounding_boxes: BoundingBoxesOnImage) -> str:


class ScaleAugmenter(ImageAugmenter):
"""
A class representing a scale augmenter.

This augmenter applies scaling transformations to images.

Args:
min_scale (float): The minimum scale factor to apply.
max_scale (float): The maximum scale factor to apply.
"""

def __init__(self, min_scale: float, max_scale: float):
self.min_scale = min_scale
self.max_scale = max_scale
Expand All @@ -77,6 +118,16 @@ def __repr__(self) -> str:


class RotationAugmenter(ImageAugmenter):
"""
A class representing a rotation augmenter for images.

Attributes:
min_angle (float): The minimum angle of rotation.
max_angle (float): The maximum angle of rotation.
augmenter (iaa.Affine): The image augmentation object.

"""

def __init__(self, min_angle: float, max_angle: float):
self.min_angle = min_angle
self.max_angle = max_angle
Expand All @@ -87,6 +138,16 @@ def __repr__(self) -> str:


class VerticalFlipAugmenter(ImageAugmenter):
"""
A class representing a vertical flip augmenter.

This augmenter flips images vertically.

Attributes:
augmenter (imgaug.augmenters.Flipud): The vertical flip augmenter.

"""

def __init__(self):
self.augmenter = iaa.Flipud(True)

Expand All @@ -95,6 +156,10 @@ def __repr__(self) -> str:


class HorizontalFlipAugmenter(ImageAugmenter):
"""
Augmenter that performs horizontal flipping on images.
"""

def __init__(self):
self.augmenter = iaa.Fliplr(True)

Expand All @@ -103,13 +168,33 @@ def __repr__(self) -> str:


class BlurAugmenter(ImageAugmenter):
"""
A class representing a blur augmenter that applies Gaussian blur to an image.

Args:
min_sigma (float): The minimum standard deviation for the Gaussian blur.
max_sigma (float): The maximum standard deviation for the Gaussian blur.
"""

def __init__(self, min_sigma: float, max_sigma: float):
self.min_sigma = min_sigma
self.max_sigma = max_sigma
self.augmenter = None

def apply(
self, image: np.ndarray, bounding_boxes: str
) -> Tuple[Union[np.ndarray, str]]:
"""
Applies Gaussian blur to the input image.

Args:
image (np.ndarray): The input image to be augmented.
bounding_boxes (str): The bounding boxes associated with the image.

Returns:
Tuple[Union[np.ndarray, str]]: A tuple containing the augmented image and the
bounding boxes.
"""
sigma = np.random.uniform(self.min_sigma, self.max_sigma)
self.augmenter = iaa.GaussianBlur(sigma=sigma)
return super().apply(image, bounding_boxes)
Expand All @@ -119,6 +204,15 @@ def __repr__(self) -> str:


class ShearAugmenter(ImageAugmenter):
"""
A class representing a shear augmenter for image data.

Attributes:
min_shear (float): The minimum shear value to apply.
max_shear (float): The maximum shear value to apply.
augmenter (iaa.ShearX): The shear augmentation object.
"""

def __init__(self, min_shear: float, max_shear: float):
self.min_shear = min_shear
self.max_shear = max_shear
Expand All @@ -129,6 +223,16 @@ def __repr__(self) -> str:


class PerspectiveAugmenter(ImageAugmenter):
"""
A class representing a perspective augmenter for image data.

This augmenter applies perspective transformations to images.

Args:
min_scale (float): The minimum scale factor for the perspective transformation.
max_scale (float): The maximum scale factor for the perspective transformation.
"""

def __init__(self, min_scale: float, max_scale: float):
self.min_scale = min_scale
self.max_scale = max_scale
Expand Down
Loading
Loading