From caeda2a35e6e736a70019691da4d0585aa7d47d0 Mon Sep 17 00:00:00 2001 From: xavi-burgos99 <101738194+xavi-burgos99@users.noreply.github.com> Date: Sun, 29 Sep 2024 19:30:32 +0200 Subject: [PATCH] Version 1.1.0 * WindowService and Some improvements * Hand Tracking Example and Some improvements * Delete .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 3 + README.md | 281 +++++++++--- examples/camera_timer/main.py | 4 +- .../camera_timer/services/camera_timer.py | 3 - examples/hand_tracking/config.json | 10 + examples/hand_tracking/main.py | 19 + .../hand_tracking/services/hand_tracking.py | 61 +++ examples/timer/main.py | 1 - eymos/service_manager.py | 58 ++- eymos/services/__init__.py | 3 +- eymos/services/camera.py | 6 +- eymos/services/window.py | 425 ++++++++++++++++++ requirements.txt | 3 +- 14 files changed, 803 insertions(+), 74 deletions(-) delete mode 100644 .DS_Store create mode 100644 examples/hand_tracking/config.json create mode 100644 examples/hand_tracking/main.py create mode 100644 examples/hand_tracking/services/hand_tracking.py create mode 100644 eymos/services/window.py diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 + ## Table of contents - [Introduction](#introduction) @@ -32,18 +33,24 @@ - [Utilities](#utilities) - [Logger](#logger) - [My OS](#my-os) +- [Predefined services](#predefined-services) + - [CameraService](#cameraservice) + - [WindowService](#windowservice) - [Bugs and feature requests](#bugs-and-feature-requests) - [Creators](#creators) - [Collaborators](#collaborators) - [License](#license) + ## Introduction EymOS is a lightweight and efficient middleware designed for robotics applications, inspired by the flexibility and modularity of systems like ROS, but optimized to be faster and lighter. Originally developed for the robot Eymo, EymOS has evolved into an independent platform that provides all the essential functionalities for node communication, device control, and process management, with a focus on simplicity and performance. + + ## Features * Lightweight and fast communication between nodes @@ -51,6 +58,8 @@ Originally developed for the robot Eymo, EymOS has evolved into an independent p * Minimal resource usage, ideal for embedded systems and small robots * Easy integration with cloud services for advanced features + + ## What's included Within the download you'll find the following directories and files: @@ -60,15 +69,17 @@ root/ ├── eymos/ │ ├── services/ │ │ ├── __init__.py -│ │ └── camera.py +│ │ ├── camera.py +│ │ └── window.py │ ├── __init__.py │ ├── logger.py │ ├── service.py │ ├── service_manager.py │ └── utils.py ├── examples/ -│ ├── timer/ -│ └── camera_timer/ +│ ├── camera_timer/ +│ ├── hand_tracking/ +│ └── timer/ ├── LICENSE ├── logo.png ├── README.md @@ -76,42 +87,78 @@ root/ └── setup.py ``` + + ## Installation + ### Prerequisites Ensure you have the following installed on your system: + - Python 3.9 or higher +- Tkinter (for the `WindowService` class) + +To install Tkinter, use the following commands: + +#### On Windows: +No additional installation is required. Tkinter is included with Python by default. + +#### On MacOS: +No additional installation is required too, but if any issue occurs, you can install it using [brew](https://brew.sh/): + +```bash +brew install tcl-tk +brew install python +``` + +#### On Linux: + +```bash +sudo apt-get update +sudo apt-get install python3-tk +``` + ### Setup with pip (recommended) Use `pip` to install the package from PyPI. 1. Install the package using `pip`: + ```sh pip install eymos ``` + 2. Import the package and [start using it](#usage). ### Setup from source Alternatively, you can clone the repository and install the package from the source code. 1. Clone the repository: + ```sh git clone https://github.com/EymoLabs/eymos.git ``` + 2. Navigate to the project directory and install the required dependencies: + ```sh cd eymos pip install -r requirements.txt ``` + 3. You’re ready to use EymOS! [Start using it](#usage). + + ## Usage After installing the package, you need to create the services for your robot, which will handle the communication between nodes and devices. Next, you could initialize them and start the service. + ### Create a new service Create a new Python file for your service, and define the class for the service. The service should inherit from the `Service` class provided by EymOS. + ```python -from eymos import Service, utils, log +from eymos import Service, log class TimerService(Service): def init(self): @@ -159,8 +206,10 @@ class TimerService(Service): return self.__time ``` + ### Initialize and start the service Create a new Python file for your main program, and initialize the service manager. Add the services to the manager, and start the services. + ```python from eymos import ServiceManager from eymos.services import CameraService @@ -184,6 +233,7 @@ if __name__ == "__main__": main() ``` + ### Configuration file Create a configuration file for the services, and specify the parameters for each service. ```json @@ -208,9 +258,12 @@ service_manager.set_config({ }) ``` + + ## Services EymOS provides a set of services that can be used to control devices and processes in your robot. Each service is defined as a class that inherits from the `Service` class, and implements the required methods and attributes. + ### First steps When a service is initialized, some actions are performed automatically, to ensure the service is correctly set up and ready to run. @@ -218,6 +271,7 @@ When a service is initialized, some actions are performed automatically, to ensu **On stop**, when a service is stopped, the `destroy` method is called, which cleans up the resources used by the service. This method is required for all services. + ### Dependencies A service can define dependencies on other services, which will be started before the service is started. This is useful when a service requires another service to be running before it can start. @@ -228,56 +282,65 @@ class MyService(Service): DEPENDENCIES = ["camera", "motor"] ``` + ### Methods A service can define custom methods to perform specific actions or operations. These methods can be created as needed, and can be called from other services or external programs. -| Method | Description | Required | -| --- | --- | --- | -| `init` | Initialize the service, setting up the variables and configurations. | Yes | -| `destroy` | Destroy the service, cleaning up the resources used by the service. | Yes | -| `before` | Perform actions before the loop starts. This method is called in the service thread, before the loop method. | No | -| `loop` | Perform the main actions of the service. This method is called in the service thread, in a loop with a delay specified by the `loop_delay` attribute or the `LOOP_DELAY` constant. | No | +| Method | Description | Required | +|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------| +| `init` | Initialize the service, setting up the variables and configurations. | Yes | +| `destroy` | Destroy the service, cleaning up the resources used by the service. | Yes | +| `before` | Perform actions before the loop starts. This method is called in the service thread, before the loop method. | No | +| `loop` | Perform the main actions of the service. This method is called in the service thread, in a loop with a delay specified by the `loop_delay` attribute or the `LOOP_DELAY` constant. | No | + +> [!WARNING] +> When defining the `before` or `loop` methods, a new thread will be created automatically for the service. Remember to call blocking methods here, to avoid blocking the main thread. + +> [!TIP] +> If you need to perform actions that require some delay, you can call them in `before` method instead of `init` method, this will make that your program executes faster. Additionally, a service has some reserved methods that should not be overridden: -| Method | Description | -| --- | --- | -| `start` | Start the service, initializing the service and starting the service thread. When the service is started, automatically starts all the other services that have not been started yet. If the service has dependencies, starts the dependencies first. | -| `stop` | Stop the service, stopping the service thread and cleaning up the resources used by the service. When the service is stopped, automatically stops all the other services that depend on this service. | -| `is_initialized` | Check if the service has been initialized. | -| `__init__` | Initialize the service instance, setting up the service attributes. | -| `__reboot` | Reboot all the services, stopping and starting all the services. | -| `__thread__execution` | Execute the service thread, calling the `before` and `loop` methods in a loop with the specified delay. | -| `__start_other_service` | Start another service that has not been started yet. | +| Method | Description | +|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `start` | Start the service, initializing the service and starting the service thread. When the service is started, automatically starts all the other services that have not been started yet. If the service has dependencies, starts the dependencies first. | +| `stop` | Stop the service, stopping the service thread and cleaning up the resources used by the service. When the service is stopped, automatically stops all the other services that depend on this service. | +| `is_initialized` | Check if the service has been initialized. | +| `__init__` | Initialize the service instance, setting up the service attributes. | +| `__reboot` | Reboot all the services, stopping and starting all the services. | +| `__thread__execution` | Execute the service thread, calling the `before` and `loop` methods in a loop with the specified delay. | +| `__start_other_service` | Start another service that has not been started yet. | + ### Attributes The service class has some attributes that can be used to configure the service during the execution, or to access some information. -| Attribute | Description | Default | -| --- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- | -| _config | The configuration dictionary for the service. It contains the parameters specified in the configuration file (only the configuration dictionary identified by the service name). | `{}` (set in constructor) | -| _global_config | The global configuration dictionary for all the services. It contains the parameters specified in the global configuration file. | `None` (set in constructor) | -| _loop_delay | The delay between each loop iteration, in seconds. It controls the frequency of the loop method. | `self.LOOP_DELAY` | -| _manager | The service manager object that is running the service. | `None` | -| _name | The name of the service. It allows, for example, identifying other services and accessing to them (e.g., `self._services["camera"].get_frame()`). | `None` (set in constructor) | -| _services | The dictionary of all the services in the service manager. It allows accessing other services and calling their methods. | `None` | -| __errors | The number of consecutive errors that have occurred during the execution of the service. | `0` | -| __initialized | A flag that indicates if the service has been initialized. | `False` | -| __init_try | The number of times the service has tried to initialize. | `0` | -| __thread | The thread object that is running in the service. | `None` | -| __thread_stop_event | The event object that controls the stopping of the service thread. | `None` | +| Attribute | Description | Default | +|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| _config | The configuration dictionary for the service. It contains the parameters specified in the configuration file (only the configuration dictionary identified by the service name). | `{}` (set in constructor) | +| _global_config | The global configuration dictionary for all the services. It contains the parameters specified in the global configuration file. | `None` (set in constructor) | +| _loop_delay | The delay between each loop iteration, in seconds. It controls the frequency of the loop method. | `self.LOOP_DELAY` | +| _manager | The service manager object that is running the service. | `None` | +| _name | The name of the service. It allows, for example, identifying other services and accessing to them (e.g., `self._services["camera"].get_frame()`). | `None` (set in constructor) | +| _services | The dictionary of all the services in the service manager. It allows accessing other services and calling their methods. | `None` | +| __errors | The number of consecutive errors that have occurred during the execution of the service. | `0` | +| __initialized | A flag that indicates if the service has been initialized. | `False` | +| __init_try | The number of times the service has tried to initialize. | `0` | +| __thread | The thread object that is running in the service. | `None` | +| __thread_stop_event | The event object that controls the stopping of the service thread. | `None` | ### Definitions Some constants are defined in the service class, which can be used to configure the service or to define some properties. -| Constant | Description | Default | -| --- |--------------------------------------------------------------------------------------------------------------------------------------| --- | -| DEPENDENCIES | The list of services that the service depends on. The services in the list will be started before the service is started. | `[]` -| ERROR_INTERVAL | The interval between each error, in seconds. It controls the frequency of the error messages and helps to avoid flooding the logs. | `5` | -| LOOP_DELAY | The default delay between each loop iteration, in seconds. It controls the frequency of the loop method. | `0.25` (4 times per second) | -| MAX_ERRORS | The maximum number of consecutive errors that can occur before the service is stopped. Can be disabled by setting it to `-1`. | `5` | -| MAX_ERRORS_REBOOT | The maximum number of consecutive errors that can occur before **all the services** are rebooted. Can be disabled by setting it to `-1`. | `-1` (disabled) | +| Constant | Description | Default | +|-------------------|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| DEPENDENCIES | The list of services that the service depends on. The services in the list will be started before the service is started. | `[]` | +| ERROR_INTERVAL | The interval between each error, in seconds. It controls the frequency of the error messages and helps to avoid flooding the logs. | `5` | +| LOOP_DELAY | The default delay between each loop iteration, in seconds. It controls the frequency of the loop method. | `0.25` (4 times per second) | +| MAX_ERRORS | The maximum number of consecutive errors that can occur before the service is stopped. Can be disabled by setting it to `-1`. | `5` | +| MAX_ERRORS_REBOOT | The maximum number of consecutive errors that can occur before **all the services** are rebooted. Can be disabled by setting it to `-1`. | `-1` (disabled) | + ## ServiceManager @@ -285,23 +348,38 @@ The `ServiceManager` class is used to manage the services in the robot, includin The class provides the following methods to manage the services: -| Method | Description | -| --- |----------------------------------------------------------------------------------------------------------------------| -| `add` | Add a new service to the manager. | -| `get` | Get a service instance from the manager. | -| `get_services` | Get a dictionary with all the services in the manager. | -| `start` | Start all the services in the manager. | -| `stop` | Stop all the services in the manager. | -| `restart` | Restart all the services in the manager. | -| `reboot` | Reboot the system, forcing all the services to be stopped. This method requires elevated permissions to be executed. | -| `exit` | Stop all the services and exit the program. | -| `set_config` | Set the global configuration dictionary for all the services. | -| `set_config_file` | Set the global configuration file for all the services. | +| Method | Description | +|-------------------|----------------------------------------------------------------------------------------------------------------------| +| `add` | Add a new service to the manager. | +| `get` | Get a service instance from the manager. | +| `get_services` | Get a dictionary with all the services in the manager. | +| `on_stop` | Perform a callback function when all the services are stopped. | +| `start` | Start all the services in the manager. | +| `stop` | Stop all the services in the manager. | +| `restart` | Restart all the services in the manager. | +| `reboot` | Reboot the system, forcing all the services to be stopped. This method requires elevated permissions to be executed. | +| `exit` | Stop all the services and exit the program. | +| `set_config` | Set the global configuration dictionary for all the services. | +| `set_config_file` | Set the global configuration file for all the services. | + + +### Configuration +The `ServiceManager` class can be configured with the following parameters in the `system` dictionary: + +| Parameter | Description | Default | +|------------------|---------------------------------------------------------------------------|----------------------------------------------------------| +| `debug` | Enable the debug mode, which shows additional information in the console. | `False` | +| `escape` | Enable pressing `ESC` to stop the program. | `True` | +| `logging.enable` | Enable the logging to the console. | `True` | +| `logging.format` | The format of the logging messages. | `"%(asctime)s - %(name)s - %(levelname)s - %(message)s"` | +| `logging.level` | The level of the logging messages. | `logging.INFO` | + ## Utilities EymOS provides some utilities that can be used to simplify the development of services and programs, including a logger and an OS utility. + ### Logger The `log(, )` function allows you to log messages to the console, with different levels of severity. @@ -310,13 +388,14 @@ The first argument is the message to log, and the second argument is the level o #### Levels The following levels are available (require the `import logging` statement): -- `logging.DEBUG` -- `logging.INFO` -- `logging.WARNING` -- `logging.ERROR` -- `logging.CRITICAL` +- `logging.DEBUG`: Detailed information, useful for debugging. +- `logging.INFO`: General information. +- `logging.WARNING`: Warning messages, indicating a potential problem. +- `logging.ERROR`: Error messages, indicating a problem. +- `logging.CRITICAL`: Critical messages, indicating a critical problem. #### Example + ```python import logging from eymos import log @@ -328,6 +407,7 @@ log("This is an error message", logging.ERROR) log("This is a critical message", logging.CRITICAL) ``` + ### My OS The `utils.my_os()` function allows you to get the name of the operating system running on the device. @@ -335,6 +415,7 @@ The `utils.my_os()` function allows you to get the name of the operating system The function returns a string with the name of the operating system. The possible values are: `"Windows"`, `"Linux"`, `"Mac"` and `"Rpi"`. If the operating system is not recognized, the function returns `None`. #### Example + ```python from eymos import utils @@ -349,36 +430,122 @@ else: print("Do something generic") ``` + + +## Predefined services +EymOS provides some predefined services that can be used to control common devices and processes in your robot. + + +### CameraService +The `CameraService` class is a predefined service that allows you to control a camera device, capturing frames and streaming video. + +#### FrameType enum +The service includes the `FrameType` enum, which defines the possible image formats that can be returned: + +- `FrameType.IMAGE`: PIL image (from the `Pillow` library). +- `FrameType.NUMPY`: NumPy array (from the `numpy` library). +- `FrameType.LIST`: List. +- `FrameType.BYTES`: Bytes. +- `FrameType.BASE64`: Base64 string. + +#### Methods +The service provides the following method to control the camera device: + +| Method | Description | +|-------------|--------------------------------------------------------------| +| `get_frame` | Get a frame from the camera device, in the specified format. | + +#### Configuration +The service can be configured with the following parameters: + +| Parameter | Description | Default | +|----------------|------------------------------------------------------------------------------------|--------------| +| `fps` | The frames per second of the camera device. | `25` | +| `resolution` | The resolution of the camera device, as a list of two integers (width and height). | `[640, 480]` | +| `rotate_frame` | Rotate the frame by 180 degrees. | `False` | + + +### WindowService +The `WindowService` class is a predefined service that allows you to create and control a window to display images and videos. + +#### ImageSize enum +The service includes the `ImageSize` enum, which defines the possible image sizes that can be used: + +- `ImageSize.COVER`: Crop the image to cover the window. +- `ImageSize.CONTAIN`: Resize the image to fit the window. +- `ImageSize.STRETCH`: Stretch the image to the window size. + +#### Methods +The service provides the following methods to control the window: + +| Method | Description | +|-------------|-----------------------------------------------------------------------------| +| `clear` | Clear the window, removing all the elements. | +| `draw` | Draw an image in the window, with the specified size and position. | +| `mainloop` | Start the main loop of the window, which handles the events and updates. | + +> [!WARNING] +> The `mainloop` method is a blocking method that should be called at the end of the program. If the method is not called after the manager is started, the window will not be displayed. + +#### Configuration +The service can be configured with the following parameters: + +| Parameter | Description | Default | +|---------------------|------------------------------------------|---------------------------------------| +| `always_on_top` | Keep the window always on top. | `True` | +| `background` | The background color of the window. | `"black"` | +| `border` | If the window has a border. | `False` | +| `draggable` | Allow the window to be dragged. | `True` | +| `fps` | The frames per second of the window. | `60` | +| `height` | The height of the window. | `270` | +| `resizable` | Allow the window to be resized. | `False` | +| `title` | The title of the window. | `"EymOS"` | +| `toggle_visibility` | Allow the window to be hidden and shown. | `{"key": "h", "modifiers": ["ctrl"]}` | +| `width` | The width of the window. | `480` | + + + ## Bugs and feature requests Have a bug or a feature request? Please search for existing and closed issues. If your problem or idea is not addressed yet, [open a new issue](https://github.com/EymoLabs/eymos/issues/new). + + ## Creators This project was developed entirely by Xavi Burgos, as part of the [EymoLabs](https://github.com/EymoLabs) team. + ### Xavi Burgos - Website: [xburgos.es](https://xburgos.es) - LinkedIn: [@xavi-burgos](https://www.linkedin.com/in/xavi-burgos/) - GitHub: [@xavi-burgos99](https://github.com/xavi-burgos99) - X (Twitter): [@xavi_burgos14](https://x.com/xavi_burgos14) + + ## Collaborators Special thanks to the following people from the [EymoLabs](https://github.com/EymoLabs) team for their contributions to the project: + ### Yeray Cordero - GitHub: [@yeray142](https://github.com/yeray142/) - LinkedIn: [@yeray142](https://linkedin.com/in/yeray142/) + ### Javier Esmorris - GitHub: [@jaesmoris](https://github.com/jaesmoris/) - LinkedIn: [Javier Esmoris Cerezuela](https://www.linkedin.com/in/javier-esmoris-cerezuela-50840b253/) + ### Gabriel Juan - GitHub: [@GabrielJuan349](https://github.com/GabrielJuan349/) - LinkedIn: [@gabi-juan](https://linkedin.com/in/gabi-juan/) + ### Samya Karzazi - GitHub: [@SamyaKarzaziElBachiri](https://github.com/SamyaKarzaziElBachiri) - LinkedIn: [Samya Karzazi](https://linkedin.com/in/samya-k-2ba678235/) + + ## License -This project is licensed under a custom license, that allows you to **use and modify the code** for any purpose, as long as you **provide attribution to the original authors**, but you **cannot distribute the code** or any derivative work **without permission**. For more information, see the [LICENSE](LICENSE) file. +This project is licensed under a custom license, that allows you to **use and modify the code** for any purpose, as long as you **provide attribution to the original authors**, but you **cannot distribute the code** or any derivative work **without permission**. For more information, see the [LICENSE](LICENSE). diff --git a/examples/camera_timer/main.py b/examples/camera_timer/main.py index 09a9842..b973900 100644 --- a/examples/camera_timer/main.py +++ b/examples/camera_timer/main.py @@ -1,7 +1,5 @@ -import time - +from eymos.services import CameraService from eymos.service_manager import ServiceManager -from eymos.services.camera import CameraService from services.camera_timer import CameraTimerService diff --git a/examples/camera_timer/services/camera_timer.py b/examples/camera_timer/services/camera_timer.py index 3036926..6a1860e 100644 --- a/examples/camera_timer/services/camera_timer.py +++ b/examples/camera_timer/services/camera_timer.py @@ -1,6 +1,3 @@ -import cv2 -from PIL import Image - from eymos import Service, log from eymos.services import FrameType diff --git a/examples/hand_tracking/config.json b/examples/hand_tracking/config.json new file mode 100644 index 0000000..d8d69f4 --- /dev/null +++ b/examples/hand_tracking/config.json @@ -0,0 +1,10 @@ +{ + "timer": { + "max_time": 300 + }, + "hand_tracking": { + "model_complexity": 0, + "min_detection_confidence": 0.5, + "min_tracking_confidence": 0.5 + } +} \ No newline at end of file diff --git a/examples/hand_tracking/main.py b/examples/hand_tracking/main.py new file mode 100644 index 0000000..4346f49 --- /dev/null +++ b/examples/hand_tracking/main.py @@ -0,0 +1,19 @@ +from eymos import ServiceManager, log +from eymos.services import CameraService, WindowService +from services.hand_tracking import HandTrackingService + + +# Initialize the service manager +manager = ServiceManager() + +# Add the services to the manager +camera = manager.add("camera", CameraService) +window = manager.add("window", WindowService) +hand_tracking = manager.add("hand_tracking", HandTrackingService) + +# Start the services +manager.start() + +# Start the window main loop +log("Starting tkinter main loop...") +window.mainloop() diff --git a/examples/hand_tracking/services/hand_tracking.py b/examples/hand_tracking/services/hand_tracking.py new file mode 100644 index 0000000..b5c7a86 --- /dev/null +++ b/examples/hand_tracking/services/hand_tracking.py @@ -0,0 +1,61 @@ +import cv2 +import mediapipe as mp +from eymos import Service + + +class HandTrackingService(Service): + def init(self): + """Initialize the service.""" + # Setting up initial attributes + self._loop_delay = 0.04 # Delay between each loop + self.__hand_detector = None # Placeholder for the hand detection model + self.__model_complexity = self._config.get('model_complexity', 0) # Simplified hand model for real-time + self.__min_detection_confidence = self._config.get('min_detection_confidence', 0.5) # Minimum confidence to detect hands + self.__min_tracking_confidence = self._config.get('min_tracking_confidence', 0.5) # Minimum confidence to track hands + + def destroy(self): + """Clean up resources before stopping the service.""" + if self.__hand_detector: + self.__hand_detector.close() # Clean up MediaPipe hand detector + self._hand_detector = None + self._model_complexity = None + self._min_detection_confidence = None + self._min_tracking_confidence = None + + def before(self): + """Prepare anything that needs to be initialized outside the main thread.""" + self.__hand_detector = mp.solutions.hands.Hands( + model_complexity=self.__model_complexity, + min_detection_confidence=self.__min_detection_confidence, + min_tracking_confidence=self.__min_tracking_confidence + ) + + def loop(self): + """Main loop where the hand detection logic will run.""" + # Get the CameraService from the service manager + camera_service = self._services.get('camera') + if camera_service is None: + return + + # Get the latest frame from CameraService + frame = camera_service.get_frame() + if frame is None: + return + + # Convert the frame from BGR to RGB as required by MediaPipe + image_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # Process the frame to detect hands + results = self.__hand_detector.process(image_rgb) + + # If hands are detected, draw landmarks on the frame + if results.multi_hand_landmarks: + for hand_landmarks in results.multi_hand_landmarks: + mp.solutions.drawing_utils.draw_landmarks( + frame, hand_landmarks, mp.solutions.hands.HAND_CONNECTIONS + ) + + # Display the processed frame in WindowService + window_service = self._services.get('window') + if window_service: + window_service.draw(frame) diff --git a/examples/timer/main.py b/examples/timer/main.py index 1bf35ab..311abdd 100644 --- a/examples/timer/main.py +++ b/examples/timer/main.py @@ -1,5 +1,4 @@ import time - from eymos.service_manager import ServiceManager from services.timer import TimerService diff --git a/eymos/service_manager.py b/eymos/service_manager.py index 7c11f03..6defb0a 100644 --- a/eymos/service_manager.py +++ b/eymos/service_manager.py @@ -1,7 +1,10 @@ import os +import sys import json import logging +import warnings import platform +from pynput import keyboard from .logger import LoggerManager, log from .service import Service @@ -21,12 +24,31 @@ def __init__(self, config=None, services=None): if services is None: services = {} - # Service manager information - self._config = None + # Set the services self._services = services # Set the configuration - self.set_config(config) + self._config = self.set_config(config) + + # Initialize stop queue + self.stop_queue = [] + + # Listen if the escape key is pressed + if self._config['escape']: + listener = keyboard.Listener(on_press=self.__on_press) + listener.start() + + # Hide logs + logging.getLogger('tensorflow').setLevel(logging.ERROR) + logging.getLogger('absl').setLevel(logging.ERROR) + logging.getLogger('cv2').setLevel(logging.ERROR) + logging.getLogger('PIL').setLevel(logging.ERROR) + logging.getLogger('matplotlib').setLevel(logging.ERROR) + logging.getLogger('urllib3').setLevel(logging.ERROR) + logging.getLogger('requests').setLevel(logging.ERROR) + + # Hide warnings + warnings.filterwarnings("ignore", category=UserWarning) def add(self, name: str, service: type[Service]): """Add a service to the manager. @@ -95,6 +117,22 @@ def stop(self): for name in self._services: self._services[name].stop() + # Call the stop queue + for callback in self.stop_queue: + + # Call the callback + callback() + + # Clear the stop queue + self.stop_queue.clear() + + def on_stop(self, callback: callable): + """Add a callback to call when the services are stopped. + Args: + callback (callable): The callback to call. + """ + self.stop_queue.append(callback) + def restart(self): """Restart all services (in order).""" # Stop all services @@ -126,7 +164,7 @@ def exit(self): # Exit the system log('Exiting the system...') - exit() + sys.exit() def set_config(self, config: dict): """Load the configuration. @@ -148,6 +186,8 @@ def set_config(self, config: dict): config['logging']['format'] = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' if ('enable' not in config['logging']) or (type(config['logging']['enable']) is not bool): config['logging']['enable'] = True + if ('escape' not in config) or (type(config['escape']) is not bool): + config['escape'] = True # Set the configuration self._config = config @@ -174,6 +214,14 @@ def set_config_file(self, file: str): # Set the configuration return self.set_config(config) + def __on_press(self, key): + """Listen if the escape key is pressed. + Args: + key (keyboard.Key): The key pressed. + """ + if key == keyboard.Key.esc: + self.exit() + def __update_logging(self): """Update the logging configuration.""" # Disable logging in LoggingManager @@ -182,4 +230,4 @@ def __update_logging(self): return # Enable logging in LoggingManager - LoggerManager.enable(self._config['logging']['level'], self._config['logging']['format']) + LoggerManager.enable(self._config['logging']['level'], self._config['logging']['format']) \ No newline at end of file diff --git a/eymos/services/__init__.py b/eymos/services/__init__.py index 897f9be..0d89322 100644 --- a/eymos/services/__init__.py +++ b/eymos/services/__init__.py @@ -1,5 +1,6 @@ # eymos/services/__init__.py from .camera import CameraService, FrameType +from .window import WindowService, ImageSize -__all__ = ['CameraService', 'FrameType'] +__all__ = ['CameraService', 'FrameType', 'WindowService', 'ImageSize'] diff --git a/eymos/services/camera.py b/eymos/services/camera.py index 5291fe9..d15a7c2 100644 --- a/eymos/services/camera.py +++ b/eymos/services/camera.py @@ -1,5 +1,5 @@ -import base64 import cv2 +import base64 from PIL import Image from eymos.service import Service @@ -13,8 +13,8 @@ class FrameType: class CameraService(Service): - DEFAULT_FPS = 10 - DEFAULT_RESOLUTION = [512, 288] + DEFAULT_FPS = 25 + DEFAULT_RESOLUTION = [640, 480] def __init__(self, name: str, config: dict, services: dict): """Initialize the service. diff --git a/eymos/services/window.py b/eymos/services/window.py new file mode 100644 index 0000000..279b33e --- /dev/null +++ b/eymos/services/window.py @@ -0,0 +1,425 @@ +import io +import cv2 +import base64 +import logging +import numpy as np +from PIL import Image +from pynput import keyboard +from eymos import Service, log + + +class DependencyLoader: + TKINTER = False + + @staticmethod + def is_tkinter(): + """Check if tkinter is installed.""" + return DependencyLoader.TKINTER + + +try: + import tkinter as tk + from tkinter import Canvas + from PIL import ImageTk + DependencyLoader.TKINTER = True +except ImportError: + pass + + +class ImageSize: + COVER = 0 + CONTAIN = 1 + STRETCH = 2 + + +class WindowService(Service): + DEFAULT_FPS = 60 + DEFAULT_RESOLUTION = [480, 270] + APP_LOGO = "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" + + def __init__(self, name: str, config: dict, services: dict): + """Initialize the service. + Args: + name (str): The name of the service. + config (dict): The system configuration. + services (dict, optional): The services to use. Defaults to {}. + """ + # Initialize the service attributes + self.__fps = None + self.__title = None + self.__resizable = None + self.__always_on_top = None + self.__background = None + self.__border = None + self.__width = None + self.__height = None + self.__draggable = None + self.__toggle_visibility = None + self.__frame = None + self.__tk_root = None + self.__tk_icon = None + self.__tk_image = None + self.__tk_photo = None + self.__tk_canvas = None + self.__tk_canvas_img = None + self.__tk_is_visible = None + + # Call the parent class constructor + super().__init__(name, config, services) + + def init(self): + """Initialize the service.""" + self.__fps = self._config.get('fps', self.DEFAULT_FPS) + self._loop_delay = 1 / self.__fps + self.__title = self._config.get('title', 'EymOS') + self.__resizable = self._config.get('resizable', False) + self.__always_on_top = self._config.get('always_on_top', True) + self.__background = self._config.get('background', 'black') + self.__border = self._config.get('border', False) + self.__width = self._config.get('width', self.DEFAULT_RESOLUTION[0]) + self.__height = self._config.get('height', self.DEFAULT_RESOLUTION[1]) + self.__draggable = self._config.get('draggable', True) + self.__toggle_visibility = self._config.get('toggle_visibility', {'key': 'h', 'modifiers': ['ctrl']}) + self.__frame = 0 + self.__tk_root = None + self.__tk_icon = self.APP_LOGO + self.__tk_image = None + self.__tk_photo = None + self.__tk_canvas = None + self.__tk_canvas_img = None + self.__tk_is_visible = True + + # Initialize the tkinter window + self.__tk_init() + + def destroy(self): + """Destroy the service.""" + self.__fps = None + self.__title = None + self.__resizable = None + self.__always_on_top = None + self.__background = None + self.__border = None + self.__width = None + self.__height = None + self.__draggable = None + self.__toggle_visibility = None + self.__frame = None + self.__tk_icon = None + self.__tk_image = None + self.__tk_photo = None + self.__tk_canvas = None + self.__tk_canvas_img = None + self.__tk_is_visible = None + + # Destroy the tkinter window from manager + if self._manager: + self._manager.on_stop(self.__tk_root.quit) + + def loop(self): + """Service loop.""" + if not DependencyLoader.is_tkinter(): + return + + # Update the tkinter window + if self.__tk_root is not None: + self.__tk_update() + + def mainloop(self): + """Start the tkinter main loop.""" + # Check if tkinter is installed + if self.__tk_root is None: + return + + # Start the tkinter main loop + self.__tk_root.mainloop() + + def draw(self, image: np.ndarray, size: int = ImageSize.COVER): + """Draw the image on the window. + Args: + image (np.ndarray): The image to draw. + """ + # Draw the image on the tkinter canvas + self.__tk_draw(image, size) + + def __tk_init(self): + """Initializes tkinter for PCs to emulate the screen.""" + # Check if tkinter is installed + if not DependencyLoader.is_tkinter(): + log('Tkinter is not installed. The window service is disabled.', logging.WARNING) + return + + # Create the tkinter window + geometry = f'{self.__width}x{self.__height}' + self.__tk_root = tk.Tk() + self.__tk_root.title(self.__title) + self.__tk_root.geometry(geometry) + self.__tk_root.resizable(self.__resizable, self.__resizable) + + # Set the window always on top + if self.__always_on_top: + self.__tk_root.lift() + + # Remove the window border + if not self.__border: + self.__tk_root.overrideredirect(True) + self.__tk_root.config(bg=self.__background) + + # Create the tkinter canvas + self.__tk_image = Image.new('RGB', (self.__width, self.__height), 'black') + self.__tk_photo = ImageTk.PhotoImage(self.__tk_image) + self.__tk_canvas = Canvas(self.__tk_root, width=self.__width, height=self.__height, bg=self.__background, highlightthickness=0) + self.__tk_canvas_img = self.__tk_canvas.create_image(0, 0, anchor='nw', image=self.__tk_photo) + self.__tk_canvas.pack() + + # Set the window icon + if self.__tk_icon: + icon_data = base64.b64decode(self.__tk_icon) + icon_image = Image.open(io.BytesIO(icon_data)) + self.__tk_icon = ImageTk.PhotoImage(icon_image) + self.__tk_root.iconphoto(False, self.__tk_icon) + + # Enable dragging + if self.__draggable: + self.__tk_drag(self.__tk_root) + + # Enable visibility toggle + if self.__toggle_visibility: + self.__tk_visibility(self.__tk_root) + + def __tk_drag(self, root): + """Enable dragging for the tkinter window. + Args: + root (tk.Tk): The tkinter window. + """ + # Check if tkinter is installed + if not DependencyLoader.is_tkinter(): + return + + # Initialize the start coordinates + coords = {"x": 0, "y": 0} + + # Function to start dragging + def start_drag(event, coords): + coords["x"] = event.x + coords["y"] = event.y + + # Function to stop dragging + def stop_drag(event, coords): + coords["x"] = None + coords["y"] = None + + # Function to drag the window + def drag(event, coords): + x = root.winfo_x() + event.x - coords["x"] + y = root.winfo_y() + event.y - coords["y"] + root.geometry(f'+{x}+{y}') + + # Remove the window border + if not self.__border: + self.__tk_root.overrideredirect(True) + + # Bind the mouse events + root.bind('', lambda event: start_drag(event, coords)) + root.bind('', lambda event: stop_drag(event, coords)) + root.bind('', lambda event: drag(event, coords)) + + def __tk_visibility(self, root): + """Enable visibility toggle for the tkinter window. + Args: + root (tk.Tk): The tkinter window. + """ + # Check if tkinter is installed + if not DependencyLoader.is_tkinter(): + return + + # Check if the toggle key is set + if not self.__toggle_visibility: + return + + # Initialize the modifier keys + modifiers = self.__toggle_visibility.get('modifiers', []) + key_char = self.__toggle_visibility.get('key', 'h') + + # Initialize the pressed keys + keys = {"ctrl": False, "shift": False, "alt": False, "meta": False} + + # Function to check pressed keys + def on_press(key, keys): + if keyboard.Key.ctrl_l == key or keyboard.Key.ctrl_r == key: + keys["ctrl"] = True + elif keyboard.Key.shift_l == key or keyboard.Key.shift_r == key: + keys["shift"] = True + elif keyboard.Key.alt_l == key or keyboard.Key.alt_r == key: + keys["alt"] = True + elif keyboard.Key.cmd_l == key or keyboard.Key.cmd_r == key: + keys["meta"] = True + is_pressed = True + for modifier in modifiers: + if not keys[modifier]: + is_pressed = False + break + if type(key) != keyboard.KeyCode: + return + if is_pressed and key_char == key.char: + self.__tk_is_visible = not self.__tk_is_visible + if self.__tk_is_visible: + log('The window is visible') + root.deiconify() + else: + log('The window is hidden') + root.withdraw() + + # Function to check released keys + def on_release(key, keys): + if keyboard.Key.ctrl_l == key or keyboard.Key.ctrl_r == key: + keys["ctrl"] = False + elif keyboard.Key.shift_l == key or keyboard.Key.shift_r == key: + keys["shift"] = False + elif keyboard.Key.alt_l == key or keyboard.Key.alt_r == key: + keys["alt"] = False + elif keyboard.Key.cmd_l == key or keyboard.Key.cmd_r == key: + keys["meta"] = False + + # Start the listener + listener = keyboard.Listener(on_press=lambda key: on_press(key, keys), on_release=lambda key: on_release(key, keys)) + listener.start() + + def __tk_update(self): + """Update the tkinter window.""" + # Check if the window is visible or the canvas is not set + if not self.__tk_is_visible or self.__tk_canvas is None: + return + + # Update the frame + self.__frame += 1 + + # Update width and height + width = self.__tk_root.winfo_width() + height = self.__tk_root.winfo_height() + if width != self.__width: + self.__width = width + if height != self.__height: + self.__height = height + + # Set the always on top + if self.__always_on_top: + self.__tk_root.attributes('-topmost', True) + + # Draw the image + self.__tk_draw() + + # Update the window + self.__tk_root.update_idletasks() + self.__tk_root.update() + + def __tk_clear(self): + """Clear the tkinter canvas.""" + # Draw a black image + self.__tk_image = Image.new('RGB', (self.__width, self.__height), 'black') + + # Convert the image to tkinter + self.__tk_photo = ImageTk.PhotoImage(self.__tk_image) + + # Update the canvas + self.__tk_canvas.itemconfig(self.__tk_canvas_img, image=self.__tk_photo) + + def __tk_draw(self, image: np.ndarray = None, size: int = ImageSize.COVER): + """Draw the image on the tkinter canvas. + Args: + image (Image, optional): The image to draw. Defaults to None. + """ + # Check if the image is set + if image is None: + return + + # Resize the image + if size == ImageSize.COVER: + image = self.__image_resize_cover(image, self.__width, self.__height) + elif size == ImageSize.CONTAIN: + image = self.__image_resize_contain(image, self.__width, self.__height) + elif size == ImageSize.STRETCH: + image = self.__image_resize_stretch(image, self.__width, self.__height) + + # Convert the image to PIL + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + image = Image.fromarray(image) + + # Convert the image to tkinter + self.__tk_photo = ImageTk.PhotoImage(image) + + # Update the canvas + self.__tk_canvas.itemconfig(self.__tk_canvas_img, image=self.__tk_photo) + + @staticmethod + def __image_resize_cover(image: np.ndarray, width: int, height: int) -> np.ndarray: + """Resize the image to cover the window. + Args: + image (np.ndarray): The image to resize. + width (int): The width of the window. + height (int): The height of the window. + Returns: + np.ndarray: The resized image. + """ + # Get the image size + image_width, image_height = image.shape[1], image.shape[0] + + # Calculate the resize ratio + ratio = max(width / image_width, height / image_height) + + # Resize the image + image = cv2.resize(image, (int(image_width * ratio), int(image_height * ratio))) + + # Calculate the cropping + crop_x = (image.shape[1] - width) // 2 + crop_y = (image.shape[0] - height) // 2 + + # Crop the image + image = image[crop_y:crop_y + height, crop_x:crop_x + width] + + # Return the image + return image + + @staticmethod + def __image_resize_contain(image: np.ndarray, width: int, height: int) -> np.ndarray: + """Resize the image to contain the window. + Args: + image (np.ndarray): The image to resize. + width (int): The width of the window. + height (int): The height of the window. + Returns: + np.ndarray: The resized image. + """ + # Get the image size + image_width, image_height = image.shape[1], image.shape[0] + + # Calculate the resize ratio + ratio = min(width / image_width, height / image_height) + + # Resize the image + image = cv2.resize(image, (int(image_width * ratio), int(image_height * ratio))) + + # Calculate the padding + padding = (height - image.shape[0]) // 2 + + # Add the padding + image = cv2.copyMakeBorder(image, padding, padding, 0, 0, cv2.BORDER_CONSTANT, value=[0, 0, 0]) + + # Return the image + return image + + @staticmethod + def __image_resize_stretch(image: np.ndarray, width: int, height: int) -> np.ndarray: + """Resize the image to stretch the window. + Args: + image (np.ndarray): The image to resize. + width (int): The width of the window. + height (int): The height of the window. + Returns: + np.ndarray: The resized image. + """ + # Resize the image + image = cv2.resize(image, (width, height)) + + # Return the image + return image diff --git a/requirements.txt b/requirements.txt index 44869fa..e9d79bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -numpy==2.1.1 +numpy~=1.26.4 opencv-python==4.10.0.84 pillow==10.4.0 +pynput~=1.7.7 \ No newline at end of file