From f92eedbf783699e24c5045c089ca5ca086bd4906 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:38:08 +0000 Subject: [PATCH 1/7] Initial plan From bd454befa32fb2e4f38e7dfb78d747d7920b2786 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:50:02 +0000 Subject: [PATCH 2/7] Migrate bitbots_vision to use generate_parameter_library Co-authored-by: Flova <15075613+Flova@users.noreply.github.com> --- .gitignore | 3 + bitbots_vision/bitbots_vision/__init__.py | 10 ++ bitbots_vision/bitbots_vision/params.py | 111 ---------------- bitbots_vision/bitbots_vision/vision.py | 81 +++++++++--- bitbots_vision/config/vision_parameters.yaml | 126 +++++++++++++++++++ bitbots_vision/package.xml | 1 + bitbots_vision/setup.py | 6 + 7 files changed, 207 insertions(+), 131 deletions(-) delete mode 100755 bitbots_vision/bitbots_vision/params.py create mode 100644 bitbots_vision/config/vision_parameters.yaml diff --git a/.gitignore b/.gitignore index 8c79b505c..5f4185ba9 100644 --- a/.gitignore +++ b/.gitignore @@ -215,6 +215,9 @@ log/ .pyenv2/* .pyenv3/* +# Generated parameter library files +**/vision_parameters/ + ansible_robots/* doc_internal/* doku/* diff --git a/bitbots_vision/bitbots_vision/__init__.py b/bitbots_vision/bitbots_vision/__init__.py index 4d2e15007..4cefb79b8 100644 --- a/bitbots_vision/bitbots_vision/__init__.py +++ b/bitbots_vision/bitbots_vision/__init__.py @@ -1,4 +1,14 @@ # Setting up runtime type checking for this package from beartype.claw import beartype_this_package +from rclpy.node import Node + +from bitbots_vision.vision_parameters import bitbots_vision as parameters beartype_this_package() + + +class NodeWithConfig(Node): + def __init__(self, name: str) -> None: + super().__init__(name) + self.param_listener = parameters.ParamListener(self) + self.config = self.param_listener.get_params() diff --git a/bitbots_vision/bitbots_vision/params.py b/bitbots_vision/bitbots_vision/params.py deleted file mode 100755 index 3fc386011..000000000 --- a/bitbots_vision/bitbots_vision/params.py +++ /dev/null @@ -1,111 +0,0 @@ -from rcl_interfaces.msg import FloatingPointRange, IntegerRange, ParameterDescriptor, ParameterType - - -class ParameterGenerator: # TODO own file - def __init__(self): - self.param_cache = [] - - def declare_params(self, node): - for param in self.param_cache: - node.declare_parameter(*param) - - def add(self, param_name, param_type=None, default=None, description=None, min=None, max=None, step=None): - describtor = ParameterDescriptor() - describtor.name = param_name - if description is None: - describtor.description = param_name - else: - describtor.description = description - - if param_type is None and default is not None: - param_type = type(default) - - py2ros_param_type = { - None: ParameterType.PARAMETER_NOT_SET, - bool: ParameterType.PARAMETER_BOOL, - int: ParameterType.PARAMETER_INTEGER, - float: ParameterType.PARAMETER_DOUBLE, - str: ParameterType.PARAMETER_STRING, - } - - param_type = py2ros_param_type.get(param_type, param_type) - - describtor.type = param_type - - if param_type == ParameterType.PARAMETER_INTEGER: - if step is None: - step = 1 - if all(x is not None or isinstance(x, int) for x in [min, max, step]): - param_range = IntegerRange() - param_range.from_value = min - param_range.to_value = max - param_range.step = step - describtor.integer_range = [param_range] - - if param_type == ParameterType.PARAMETER_DOUBLE: - if step is None: - step = 0.01 - if all(x is not None for x in [min, max]): - param_range = FloatingPointRange() - param_range.from_value = float(min) - param_range.to_value = float(max) - param_range.step = float(step) - describtor.floating_point_range = [param_range] - - type2default_default = { - ParameterType.PARAMETER_NOT_SET: 0, - ParameterType.PARAMETER_BOOL: False, - ParameterType.PARAMETER_INTEGER: 0, - ParameterType.PARAMETER_DOUBLE: 0.0, - ParameterType.PARAMETER_STRING: "", - } - - if default is None: - default = type2default_default[param_type] - - self.param_cache.append((param_name, default, describtor)) - - -gen = ParameterGenerator() - -########## -# Params # -########## - -gen.add("component_ball_detection_active", bool, description="Activate/Deactivate the ball detection component") -gen.add("component_debug_image_active", bool, description="Activate/Deactivate the debug image component") -gen.add("component_field_detection_active", bool, description="Activate/Deactivate the field detection component") -gen.add("component_goalpost_detection_active", bool, description="Activate/Deactivate the goalpost detection component") -gen.add("component_line_detection_active", bool, description="Activate/Deactivate the line detection component") -gen.add("component_robot_detection_active", bool, description="Activate/Deactivate the robot detection component") - -gen.add("ROS_img_msg_topic", str, description="ROS topic of the image message") -gen.add("ROS_ball_msg_topic", str, description="ROS topic of the ball message") -gen.add("ROS_goal_posts_msg_topic", str, description="ROS topic of the goal posts message") -gen.add("ROS_robot_msg_topic", str, description="ROS topic of the robots message") -gen.add("ROS_line_msg_topic", str, description="ROS topic of the line message") -gen.add("ROS_line_mask_msg_topic", str, description="ROS topic of the line mask message") -gen.add("ROS_debug_image_msg_topic", str, description="ROS topic of the debug image message") -gen.add("ROS_field_mask_image_msg_topic", str, description="ROS topic of the field mask debug image message") - -gen.add("yoeo_model_path", str, description="Name of YOEO model") -gen.add("yoeo_nms_threshold", float, description="YOEO Non-maximum suppression threshold", min=0.0, max=1.0) -gen.add("yoeo_conf_threshold", float, description="YOEO confidence threshold", min=0.0, max=1.0) -gen.add( - "yoeo_framework", - str, - description="The neural network framework that should be used ['pytorch', 'openvino', 'onnx', 'tvm']", -) - -gen.add( - "ball_candidate_rating_threshold", - float, - description="A threshold for the minimum candidate rating", - min=0.0, - max=1.0, -) -gen.add( - "ball_candidate_max_count", int, description="The maximum number of balls that should be published", min=0, max=50 -) - -gen.add("caching", bool, description="Used to deactivate caching for profiling reasons") diff --git a/bitbots_vision/bitbots_vision/vision.py b/bitbots_vision/bitbots_vision/vision.py index 9cc31131a..ea665603f 100755 --- a/bitbots_vision/bitbots_vision/vision.py +++ b/bitbots_vision/bitbots_vision/vision.py @@ -1,5 +1,4 @@ #! /usr/bin/env python3 -from copy import deepcopy from typing import Optional import rclpy @@ -7,13 +6,11 @@ from cv_bridge import CvBridge from rcl_interfaces.msg import SetParametersResult from rclpy.experimental.events_executor import EventsExecutor -from rclpy.node import Node from sensor_msgs.msg import Image +from bitbots_vision import NodeWithConfig from bitbots_vision.vision_modules import debug, ros_utils, yoeo -from .params import gen - logger = rclpy.logging.get_logger("bitbots_vision") try: @@ -26,7 +23,7 @@ def profile(func): logger.info("No Profiling available") -class YOEOVision(Node): +class YOEOVision(NodeWithConfig): """ The Vision is the main ROS-node for handling all tasks related to image processing. @@ -43,7 +40,6 @@ def __init__(self) -> None: yoeo.YOEOObjectManager.set_package_directory(self._package_path) - self._config: dict = {} self._cv_bridge = CvBridge() self._sub_image = None @@ -51,8 +47,7 @@ def __init__(self) -> None: self._vision_components: list[yoeo.AbstractVisionComponent] = [] self._debug_image: Optional[debug.DebugImage] = None - # Setup reconfiguration - gen.declare_params(self) + # Setup reconfiguration - now we use the generated parameter listener self.add_on_set_parameters_callback(self._dynamic_reconfigure_callback) # Add general params @@ -61,28 +56,70 @@ def __init__(self) -> None: # Update team color ros_utils.update_own_team_color(self) - self._dynamic_reconfigure_callback(self.get_parameters_by_prefix("").values()) + # Configure vision with initial parameters + self._configure_vision_from_config() logger.debug(f"Leaving {self.__class__.__name__} constructor") + def _config_to_dict(self) -> dict: + """ + Convert the generated parameter config object to a dictionary for compatibility + with existing code that expects dictionary access. + """ + return { + # Component activation parameters + "component_ball_detection_active": self.config.component_ball_detection_active, + "component_debug_image_active": self.config.component_debug_image_active, + "component_field_detection_active": self.config.component_field_detection_active, + "component_goalpost_detection_active": self.config.component_goalpost_detection_active, + "component_line_detection_active": self.config.component_line_detection_active, + "component_robot_detection_active": self.config.component_robot_detection_active, + + # ROS topic parameters + "ROS_img_msg_topic": self.config.ROS_img_msg_topic, + "ROS_ball_msg_topic": self.config.ROS_ball_msg_topic, + "ROS_goal_posts_msg_topic": self.config.ROS_goal_posts_msg_topic, + "ROS_robot_msg_topic": self.config.ROS_robot_msg_topic, + "ROS_line_msg_topic": self.config.ROS_line_msg_topic, + "ROS_line_mask_msg_topic": self.config.ROS_line_mask_msg_topic, + "ROS_debug_image_msg_topic": self.config.ROS_debug_image_msg_topic, + "ROS_field_mask_image_msg_topic": self.config.ROS_field_mask_image_msg_topic, + + # YOEO model parameters + "yoeo_model_path": self.config.yoeo_model_path, + "yoeo_nms_threshold": self.config.yoeo_nms_threshold, + "yoeo_conf_threshold": self.config.yoeo_conf_threshold, + "yoeo_framework": self.config.yoeo_framework, + + # Ball detection parameters + "ball_candidate_rating_threshold": self.config.ball_candidate_rating_threshold, + "ball_candidate_max_count": self.config.ball_candidate_max_count, + + # Caching parameter + "caching": self.config.caching, + } + + def _configure_vision_from_config(self) -> None: + """ + Configure vision components using the current config. + """ + config_dict = self._config_to_dict() + self._configure_vision(config_dict) + def _dynamic_reconfigure_callback(self, params) -> SetParametersResult: """ Callback for the dynamic reconfigure configuration. - :param dict params: new config + :param params: list of changed parameters """ - new_config = self._get_updated_config_with(params) - self._configure_vision(new_config) - self._config = new_config + # Update the config from the parameter listener + self.config = self.param_listener.get_params() + + # Configure vision with the updated config + self._configure_vision_from_config() return SetParametersResult(successful=True) - def _get_updated_config_with(self, params) -> dict: - new_config = deepcopy(self._config) - for param in params: - new_config[param.name] = param.value - return new_config - def _configure_vision(self, new_config: dict) -> None: yoeo.YOEOObjectManager.configure(new_config) @@ -120,15 +157,19 @@ def make_vision_component( if new_config["component_debug_image_active"]: self._vision_components.append(make_vision_component(yoeo.DebugImageComponent)) + # For the subscriber update, we'll pass the last config dict or None for the first time + old_config_dict = getattr(self, '_last_config_dict', None) self._sub_image = ros_utils.create_or_update_subscriber( self, - self._config, + old_config_dict, new_config, self._sub_image, "ROS_img_msg_topic", Image, callback=self._run_vision_pipeline, ) + # Remember this config for next time + self._last_config_dict = new_config.copy() @profile def _run_vision_pipeline(self, image_msg: Image) -> None: diff --git a/bitbots_vision/config/vision_parameters.yaml b/bitbots_vision/config/vision_parameters.yaml new file mode 100644 index 000000000..dc5b98732 --- /dev/null +++ b/bitbots_vision/config/vision_parameters.yaml @@ -0,0 +1,126 @@ +bitbots_vision: + # Component activation parameters + component_ball_detection_active: + type: bool + default_value: true + description: "Activate/Deactivate the ball detection component" + + component_debug_image_active: + type: bool + default_value: false + description: "Activate/Deactivate the debug image component" + + component_field_detection_active: + type: bool + default_value: true + description: "Activate/Deactivate the field detection component" + + component_goalpost_detection_active: + type: bool + default_value: false + description: "Activate/Deactivate the goalpost detection component" + + component_line_detection_active: + type: bool + default_value: true + description: "Activate/Deactivate the line detection component" + + component_robot_detection_active: + type: bool + default_value: true + description: "Activate/Deactivate the robot detection component" + + # ROS topic parameters + ROS_img_msg_topic: + type: string + default_value: "camera/image_proc" + description: "ROS topic of the image message" + read_only: true + + ROS_ball_msg_topic: + type: string + default_value: "balls_in_image" + description: "ROS topic of the ball message" + read_only: true + + ROS_goal_posts_msg_topic: + type: string + default_value: "goal_posts_in_image" + description: "ROS topic of the goal posts message" + read_only: true + + ROS_robot_msg_topic: + type: string + default_value: "robots_in_image" + description: "ROS topic of the robots message" + read_only: true + + ROS_line_msg_topic: + type: string + default_value: "line_in_image" + description: "ROS topic of the line message" + read_only: true + + ROS_line_mask_msg_topic: + type: string + default_value: "line_mask_in_image" + description: "ROS topic of the line mask message" + read_only: true + + ROS_debug_image_msg_topic: + type: string + default_value: "debug_image" + description: "ROS topic of the debug image message" + read_only: true + + ROS_field_mask_image_msg_topic: + type: string + default_value: "field_mask" + description: "ROS topic of the field mask debug image message" + read_only: true + + # YOEO model parameters + yoeo_model_path: + type: string + default_value: "2022_10_07_flo_torso21_yoeox" + description: "Name of YOEO model" + + yoeo_nms_threshold: + type: double + default_value: 0.4 + description: "YOEO Non-maximum suppression threshold" + validation: + bounds<>: [0.0, 1.0] + + yoeo_conf_threshold: + type: double + default_value: 0.5 + description: "YOEO confidence threshold" + validation: + bounds<>: [0.0, 1.0] + + yoeo_framework: + type: string + default_value: "tvm" + description: "The neural network framework that should be used ['pytorch', 'openvino', 'onnx', 'tvm']" + + # Ball detection parameters + ball_candidate_rating_threshold: + type: double + default_value: 0.5 + description: "A threshold for the minimum candidate rating" + validation: + bounds<>: [0.0, 1.0] + + ball_candidate_max_count: + type: int + default_value: 1 + description: "The maximum number of balls that should be published" + validation: + bounds<>: [0, 50] + + # Caching parameter + caching: + type: bool + default_value: true + description: "Used to deactivate caching for profiling reasons" \ No newline at end of file diff --git a/bitbots_vision/package.xml b/bitbots_vision/package.xml index 6f5b75f5e..794cb8a77 100644 --- a/bitbots_vision/package.xml +++ b/bitbots_vision/package.xml @@ -29,6 +29,7 @@ rosidl_default_runtime bitbots_utils game_controller_hl_interfaces + generate_parameter_library geometry_msgs image_transport python3-numpy diff --git a/bitbots_vision/setup.py b/bitbots_vision/setup.py index 96b053d04..d7288708e 100755 --- a/bitbots_vision/setup.py +++ b/bitbots_vision/setup.py @@ -1,8 +1,14 @@ import glob import os +from generate_parameter_library_py.setup_helper import generate_parameter_module from setuptools import find_packages, setup +generate_parameter_module( + "vision_parameters", # python module name for parameter library + "config/vision_parameters.yaml", # path to input yaml file +) + package_name = "bitbots_vision" From d930b118e79ad9f7d6ca6a491aefe67096769334 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:54:37 +0000 Subject: [PATCH 3/7] Complete bitbots_vision migration: test and finalize generate_parameter_library integration Co-authored-by: Flova <15075613+Flova@users.noreply.github.com> --- bitbots_vision/bitbots_vision/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bitbots_vision/bitbots_vision/__init__.py b/bitbots_vision/bitbots_vision/__init__.py index 4cefb79b8..591fcb6d2 100644 --- a/bitbots_vision/bitbots_vision/__init__.py +++ b/bitbots_vision/bitbots_vision/__init__.py @@ -1,11 +1,15 @@ # Setting up runtime type checking for this package -from beartype.claw import beartype_this_package +try: + from beartype.claw import beartype_this_package + beartype_this_package() +except ImportError: + # beartype not available, skip type checking + pass + from rclpy.node import Node from bitbots_vision.vision_parameters import bitbots_vision as parameters -beartype_this_package() - class NodeWithConfig(Node): def __init__(self, name: str) -> None: From 8395c2eb49d368f35ed8fbdeed1d162d48f231f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:04:28 +0000 Subject: [PATCH 4/7] Fix parameter library integration based on code review feedback Co-authored-by: Flova <15075613+Flova@users.noreply.github.com> --- .gitignore | 2 +- bitbots_vision/bitbots_vision/__init__.py | 18 +---- bitbots_vision/bitbots_vision/vision.py | 96 ++++++++++------------- 3 files changed, 43 insertions(+), 73 deletions(-) diff --git a/.gitignore b/.gitignore index 5f4185ba9..95d6ccf58 100644 --- a/.gitignore +++ b/.gitignore @@ -216,7 +216,7 @@ log/ .pyenv3/* # Generated parameter library files -**/vision_parameters/ +**/vision_parameters.py ansible_robots/* doc_internal/* diff --git a/bitbots_vision/bitbots_vision/__init__.py b/bitbots_vision/bitbots_vision/__init__.py index 591fcb6d2..4d2e15007 100644 --- a/bitbots_vision/bitbots_vision/__init__.py +++ b/bitbots_vision/bitbots_vision/__init__.py @@ -1,18 +1,4 @@ # Setting up runtime type checking for this package -try: - from beartype.claw import beartype_this_package - beartype_this_package() -except ImportError: - # beartype not available, skip type checking - pass +from beartype.claw import beartype_this_package -from rclpy.node import Node - -from bitbots_vision.vision_parameters import bitbots_vision as parameters - - -class NodeWithConfig(Node): - def __init__(self, name: str) -> None: - super().__init__(name) - self.param_listener = parameters.ParamListener(self) - self.config = self.param_listener.get_params() +beartype_this_package() diff --git a/bitbots_vision/bitbots_vision/vision.py b/bitbots_vision/bitbots_vision/vision.py index ea665603f..1a8253ad4 100755 --- a/bitbots_vision/bitbots_vision/vision.py +++ b/bitbots_vision/bitbots_vision/vision.py @@ -5,11 +5,11 @@ from ament_index_python.packages import get_package_share_directory from cv_bridge import CvBridge from rcl_interfaces.msg import SetParametersResult -from rclpy.experimental.events_executor import EventsExecutor +from rclpy.node import Node from sensor_msgs.msg import Image -from bitbots_vision import NodeWithConfig from bitbots_vision.vision_modules import debug, ros_utils, yoeo +from bitbots_vision.vision_parameters import bitbots_vision as parameters logger = rclpy.logging.get_logger("bitbots_vision") @@ -23,7 +23,7 @@ def profile(func): logger.info("No Profiling available") -class YOEOVision(NodeWithConfig): +class YOEOVision(Node): """ The Vision is the main ROS-node for handling all tasks related to image processing. @@ -36,6 +36,10 @@ def __init__(self) -> None: logger.debug(f"Entering {self.__class__.__name__} constructor") + # Setup parameter listener directly + self.param_listener = parameters.ParamListener(self) + self.config = self.param_listener.get_params() + self._package_path = get_package_share_directory("bitbots_vision") yoeo.YOEOObjectManager.set_package_directory(self._package_path) @@ -47,7 +51,7 @@ def __init__(self) -> None: self._vision_components: list[yoeo.AbstractVisionComponent] = [] self._debug_image: Optional[debug.DebugImage] = None - # Setup reconfiguration - now we use the generated parameter listener + # Setup reconfiguration callback self.add_on_set_parameters_callback(self._dynamic_reconfigure_callback) # Add general params @@ -61,50 +65,11 @@ def __init__(self) -> None: logger.debug(f"Leaving {self.__class__.__name__} constructor") - def _config_to_dict(self) -> dict: - """ - Convert the generated parameter config object to a dictionary for compatibility - with existing code that expects dictionary access. - """ - return { - # Component activation parameters - "component_ball_detection_active": self.config.component_ball_detection_active, - "component_debug_image_active": self.config.component_debug_image_active, - "component_field_detection_active": self.config.component_field_detection_active, - "component_goalpost_detection_active": self.config.component_goalpost_detection_active, - "component_line_detection_active": self.config.component_line_detection_active, - "component_robot_detection_active": self.config.component_robot_detection_active, - - # ROS topic parameters - "ROS_img_msg_topic": self.config.ROS_img_msg_topic, - "ROS_ball_msg_topic": self.config.ROS_ball_msg_topic, - "ROS_goal_posts_msg_topic": self.config.ROS_goal_posts_msg_topic, - "ROS_robot_msg_topic": self.config.ROS_robot_msg_topic, - "ROS_line_msg_topic": self.config.ROS_line_msg_topic, - "ROS_line_mask_msg_topic": self.config.ROS_line_mask_msg_topic, - "ROS_debug_image_msg_topic": self.config.ROS_debug_image_msg_topic, - "ROS_field_mask_image_msg_topic": self.config.ROS_field_mask_image_msg_topic, - - # YOEO model parameters - "yoeo_model_path": self.config.yoeo_model_path, - "yoeo_nms_threshold": self.config.yoeo_nms_threshold, - "yoeo_conf_threshold": self.config.yoeo_conf_threshold, - "yoeo_framework": self.config.yoeo_framework, - - # Ball detection parameters - "ball_candidate_rating_threshold": self.config.ball_candidate_rating_threshold, - "ball_candidate_max_count": self.config.ball_candidate_max_count, - - # Caching parameter - "caching": self.config.caching, - } - def _configure_vision_from_config(self) -> None: """ Configure vision components using the current config. """ - config_dict = self._config_to_dict() - self._configure_vision(config_dict) + self._configure_vision(self.config) def _dynamic_reconfigure_callback(self, params) -> SetParametersResult: """ @@ -120,10 +85,29 @@ def _dynamic_reconfigure_callback(self, params) -> SetParametersResult: return SetParametersResult(successful=True) - def _configure_vision(self, new_config: dict) -> None: - yoeo.YOEOObjectManager.configure(new_config) + def _configure_vision(self, config) -> None: + # Create a minimal config dict for compatibility with existing subsystems + # TODO: Update subsystems to use dot notation and remove this compatibility layer + config_dict = { + "component_ball_detection_active": config.component_ball_detection_active, + "component_debug_image_active": config.component_debug_image_active, + "component_field_detection_active": config.component_field_detection_active, + "component_goalpost_detection_active": config.component_goalpost_detection_active, + "component_line_detection_active": config.component_line_detection_active, + "component_robot_detection_active": config.component_robot_detection_active, + "ROS_img_msg_topic": config.ROS_img_msg_topic, + "yoeo_model_path": config.yoeo_model_path, + "yoeo_nms_threshold": config.yoeo_nms_threshold, + "yoeo_conf_threshold": config.yoeo_conf_threshold, + "yoeo_framework": config.yoeo_framework, + "ball_candidate_rating_threshold": config.ball_candidate_rating_threshold, + "ball_candidate_max_count": config.ball_candidate_max_count, + "caching": config.caching, + } + + yoeo.YOEOObjectManager.configure(config_dict) - debug_image = debug.DebugImage(new_config["component_debug_image_active"]) + debug_image = debug.DebugImage(config.component_debug_image_active) self._debug_image = debug_image def make_vision_component( @@ -133,28 +117,28 @@ def make_vision_component( node=self, yoeo_handler=yoeo.YOEOObjectManager.get(), debug_image=debug_image, - config=new_config, + config=config_dict, # Still passing dict for compatibility **kwargs, ) self._vision_components = [make_vision_component(yoeo.YOEOComponent)] - if new_config["component_ball_detection_active"]: + if config.component_ball_detection_active: self._vision_components.append(make_vision_component(yoeo.BallDetectionComponent)) - if new_config["component_robot_detection_active"]: + if config.component_robot_detection_active: self._vision_components.append( make_vision_component( yoeo.RobotDetectionComponent, team_color_detection_supported=yoeo.YOEOObjectManager.is_team_color_detection_supported(), ) ) - if new_config["component_goalpost_detection_active"]: + if config.component_goalpost_detection_active: self._vision_components.append(make_vision_component(yoeo.GoalpostDetectionComponent)) - if new_config["component_line_detection_active"]: + if config.component_line_detection_active: self._vision_components.append(make_vision_component(yoeo.LineDetectionComponent)) - if new_config["component_field_detection_active"]: + if config.component_field_detection_active: self._vision_components.append(make_vision_component(yoeo.FieldDetectionComponent)) - if new_config["component_debug_image_active"]: + if config.component_debug_image_active: self._vision_components.append(make_vision_component(yoeo.DebugImageComponent)) # For the subscriber update, we'll pass the last config dict or None for the first time @@ -162,14 +146,14 @@ def make_vision_component( self._sub_image = ros_utils.create_or_update_subscriber( self, old_config_dict, - new_config, + config_dict, self._sub_image, "ROS_img_msg_topic", Image, callback=self._run_vision_pipeline, ) # Remember this config for next time - self._last_config_dict = new_config.copy() + self._last_config_dict = config_dict.copy() @profile def _run_vision_pipeline(self, image_msg: Image) -> None: From 06cce1a425cc98432fdc28a7e52cd82be53ad216 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:14:07 +0000 Subject: [PATCH 5/7] Update subsystems to use dot notation for config access Co-authored-by: Flova <15075613+Flova@users.noreply.github.com> --- bitbots_vision/bitbots_vision/vision.py | 51 ++++++---------- .../vision_modules/yoeo/object_manager.py | 24 +++++--- .../vision_modules/yoeo/vision_components.py | 16 ++--- .../vision_modules/yoeo/yoeo_handlers.py | 58 +++++++++---------- 4 files changed, 71 insertions(+), 78 deletions(-) diff --git a/bitbots_vision/bitbots_vision/vision.py b/bitbots_vision/bitbots_vision/vision.py index 1a8253ad4..2bf6667fd 100755 --- a/bitbots_vision/bitbots_vision/vision.py +++ b/bitbots_vision/bitbots_vision/vision.py @@ -86,26 +86,7 @@ def _dynamic_reconfigure_callback(self, params) -> SetParametersResult: return SetParametersResult(successful=True) def _configure_vision(self, config) -> None: - # Create a minimal config dict for compatibility with existing subsystems - # TODO: Update subsystems to use dot notation and remove this compatibility layer - config_dict = { - "component_ball_detection_active": config.component_ball_detection_active, - "component_debug_image_active": config.component_debug_image_active, - "component_field_detection_active": config.component_field_detection_active, - "component_goalpost_detection_active": config.component_goalpost_detection_active, - "component_line_detection_active": config.component_line_detection_active, - "component_robot_detection_active": config.component_robot_detection_active, - "ROS_img_msg_topic": config.ROS_img_msg_topic, - "yoeo_model_path": config.yoeo_model_path, - "yoeo_nms_threshold": config.yoeo_nms_threshold, - "yoeo_conf_threshold": config.yoeo_conf_threshold, - "yoeo_framework": config.yoeo_framework, - "ball_candidate_rating_threshold": config.ball_candidate_rating_threshold, - "ball_candidate_max_count": config.ball_candidate_max_count, - "caching": config.caching, - } - - yoeo.YOEOObjectManager.configure(config_dict) + yoeo.YOEOObjectManager.configure(config) debug_image = debug.DebugImage(config.component_debug_image_active) self._debug_image = debug_image @@ -117,7 +98,7 @@ def make_vision_component( node=self, yoeo_handler=yoeo.YOEOObjectManager.get(), debug_image=debug_image, - config=config_dict, # Still passing dict for compatibility + config=config, # Now passing config object directly **kwargs, ) @@ -141,19 +122,21 @@ def make_vision_component( if config.component_debug_image_active: self._vision_components.append(make_vision_component(yoeo.DebugImageComponent)) - # For the subscriber update, we'll pass the last config dict or None for the first time - old_config_dict = getattr(self, '_last_config_dict', None) - self._sub_image = ros_utils.create_or_update_subscriber( - self, - old_config_dict, - config_dict, - self._sub_image, - "ROS_img_msg_topic", - Image, - callback=self._run_vision_pipeline, - ) - # Remember this config for next time - self._last_config_dict = config_dict.copy() + # For the subscriber update, handle the topic name directly + old_topic = getattr(self, '_last_img_topic', None) + current_topic = config.ROS_img_msg_topic + + if old_topic != current_topic: + self._sub_image = self.create_subscription( + Image, + current_topic, + self._run_vision_pipeline, + 1 + ) + logger.debug(f"Registered new subscriber at {current_topic}") + + # Remember this topic for next time + self._last_img_topic = current_topic @profile def _run_vision_pipeline(self, image_msg: Image) -> None: diff --git a/bitbots_vision/bitbots_vision/vision_modules/yoeo/object_manager.py b/bitbots_vision/bitbots_vision/vision_modules/yoeo/object_manager.py index 3aadfb974..7749ffffe 100644 --- a/bitbots_vision/bitbots_vision/vision_modules/yoeo/object_manager.py +++ b/bitbots_vision/bitbots_vision/vision_modules/yoeo/object_manager.py @@ -72,14 +72,14 @@ def is_team_color_detection_supported(cls) -> bool: return cls._model_config.team_colors_are_provided() @classmethod - def configure(cls, config: dict) -> None: + def configure(cls, config) -> None: if not cls._package_directory_set: logger.error("Package directory not set!") - framework = config["yoeo_framework"] + framework = config.yoeo_framework cls._verify_framework_parameter(framework) - model_path = cls._get_full_model_path(config["yoeo_model_path"]) + model_path = cls._get_full_model_path(config.yoeo_model_path) cls._verify_required_neural_network_files_exist(framework, model_path) cls._configure_yoeo_instance(config, framework, model_path) @@ -107,7 +107,7 @@ def _model_files_exist(cls, framework: str, model_path: str) -> bool: return cls._HANDLERS_BY_NAME[framework].model_files_exist(model_path) @classmethod - def _configure_yoeo_instance(cls, config: dict, framework: str, model_path: str) -> None: + def _configure_yoeo_instance(cls, config, framework: str, model_path: str) -> None: if cls._new_yoeo_handler_is_needed(framework, model_path): cls._load_model_config(model_path) cls._instantiate_new_yoeo_handler(config, framework, model_path) @@ -124,7 +124,7 @@ def _load_model_config(cls, model_path: str) -> None: cls._model_config = ModelConfigLoader.load_from(model_path) @classmethod - def _instantiate_new_yoeo_handler(cls, config: dict, framework: str, model_path: str) -> None: + def _instantiate_new_yoeo_handler(cls, config, framework: str, model_path: str) -> None: cls._yoeo_instance = cls._HANDLERS_BY_NAME[framework]( config, model_path, @@ -135,5 +135,15 @@ def _instantiate_new_yoeo_handler(cls, config: dict, framework: str, model_path: logger.info(f"Using {cls._yoeo_instance.__class__.__name__}") @classmethod - def _yoeo_parameters_have_changed(cls, new_config: dict) -> bool: - return ros_utils.config_param_change(cls._config, new_config, r"yoeo_") + def _yoeo_parameters_have_changed(cls, new_config) -> bool: + if cls._config is None: + return True + + # Compare YOEO-related parameters using direct attribute access + return ( + cls._config.yoeo_framework != new_config.yoeo_framework or + cls._config.yoeo_model_path != new_config.yoeo_model_path or + cls._config.yoeo_nms_threshold != new_config.yoeo_nms_threshold or + cls._config.yoeo_conf_threshold != new_config.yoeo_conf_threshold or + cls._config.caching != new_config.caching + ) diff --git a/bitbots_vision/bitbots_vision/vision_modules/yoeo/vision_components.py b/bitbots_vision/bitbots_vision/vision_modules/yoeo/vision_components.py index 2fd5d0a1f..74cc7663a 100644 --- a/bitbots_vision/bitbots_vision/vision_modules/yoeo/vision_components.py +++ b/bitbots_vision/bitbots_vision/vision_modules/yoeo/vision_components.py @@ -57,7 +57,7 @@ def __init__( ): super().__init__(node, yoeo_handler, debug_image, config) - self._publisher = self._node.create_publisher(BallArray, self._config["ROS_ball_msg_topic"], qos_profile=1) + self._publisher = self._node.create_publisher(BallArray, self._config.ROS_ball_msg_topic, qos_profile=1) def run(self, image: np.ndarray, header: Header) -> None: # Get all ball candidates from YOEO @@ -65,9 +65,9 @@ def run(self, image: np.ndarray, header: Header) -> None: # Filter candidates by rating and count candidates = candidate.Candidate.sort_candidates(candidates) - top_candidates = candidates[: self._config["ball_candidate_max_count"]] + top_candidates = candidates[: self._config.ball_candidate_max_count] final_candidates = candidate.Candidate.rating_threshold( - top_candidates, self._config["ball_candidate_rating_threshold"] + top_candidates, self._config.ball_candidate_rating_threshold ) # Publish ball candidates @@ -95,7 +95,7 @@ def __init__( super().__init__(node, yoeo_handler, debug_image, config) self._publisher = self._node.create_publisher( - GoalpostArray, self._config["ROS_goal_posts_msg_topic"], qos_profile=1 + GoalpostArray, self._config.ROS_goal_posts_msg_topic, qos_profile=1 ) def run(self, image: np.ndarray, header: Header) -> None: @@ -125,7 +125,7 @@ def __init__( self, node: Node, yoeo_handler: yoeo_handlers.IYOEOHandler, debug_image: debug.DebugImage, config: dict ): super().__init__(node, yoeo_handler, debug_image, config) - self._publisher = self._node.create_publisher(Image, self._config["ROS_line_mask_msg_topic"], qos_profile=1) + self._publisher = self._node.create_publisher(Image, self._config.ROS_line_mask_msg_topic, qos_profile=1) def run(self, image: np.ndarray, header: Header) -> None: # Get line mask from YOEO @@ -153,7 +153,7 @@ def __init__( ): super().__init__(node, yoeo_handler, debug_image, config) self._publisher = self._node.create_publisher( - Image, self._config["ROS_field_mask_image_msg_topic"], qos_profile=1 + Image, self._config.ROS_field_mask_image_msg_topic, qos_profile=1 ) def run(self, image: np.ndarray, header: Header) -> None: @@ -185,7 +185,7 @@ def __init__( super().__init__(node, yoeo_handler, debug_image, config) self._team_color_detection_supported = team_color_detection_supported - self._publisher = self._node.create_publisher(RobotArray, self._config["ROS_robot_msg_topic"], qos_profile=1) + self._publisher = self._node.create_publisher(RobotArray, self._config.ROS_robot_msg_topic, qos_profile=1) def run(self, image: np.ndarray, header: Header) -> None: robot_msgs: list[Robot] = [] @@ -282,7 +282,7 @@ def __init__( ): super().__init__(node, yoeo_handler, debug_image, config) - self._publisher = self._node.create_publisher(Image, self._config["ROS_debug_image_msg_topic"], qos_profile=1) + self._publisher = self._node.create_publisher(Image, self._config.ROS_debug_image_msg_topic, qos_profile=1) def run(self, image: np.ndarray, header: Header) -> None: debug_image_msg = ros_utils.build_image_msg(header, self._debug_image.get_image(), "bgr8") diff --git a/bitbots_vision/bitbots_vision/vision_modules/yoeo/yoeo_handlers.py b/bitbots_vision/bitbots_vision/vision_modules/yoeo/yoeo_handlers.py index 9409dc3ea..7facb40df 100644 --- a/bitbots_vision/bitbots_vision/vision_modules/yoeo/yoeo_handlers.py +++ b/bitbots_vision/bitbots_vision/vision_modules/yoeo/yoeo_handlers.py @@ -22,7 +22,7 @@ class IYOEOHandler(ABC): """ @abstractmethod - def configure(self, config: dict) -> None: + def configure(self, config) -> None: """ Allows to (re-) configure the YOEO handler. """ @@ -98,7 +98,7 @@ class YOEOHandlerTemplate(IYOEOHandler): def __init__( self, - config: dict, + config, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -117,12 +117,12 @@ def __init__( self._seg_class_names: list[str] = seg_class_names self._seg_masks: dict = dict() - self._use_caching: bool = config["caching"] + self._use_caching: bool = config.caching logger.debug("Leaving YOEOHandlerTemplate constructor") - def configure(self, config: dict) -> None: - self._use_caching = config["caching"] + def configure(self, config) -> None: + self._use_caching = config.caching def get_available_detection_class_names(self) -> list[str]: return self._det_class_names @@ -211,7 +211,7 @@ class YOEOHandlerONNX(YOEOHandlerTemplate): def __init__( self, - config: dict, + config, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -238,8 +238,8 @@ def __init__( self._det_postprocessor: utils.IDetectionPostProcessor = utils.DefaultDetectionPostProcessor( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer.shape[2], - conf_thresh=config["yoeo_conf_threshold"], - nms_thresh=config["yoeo_nms_threshold"], + conf_thresh=config.yoeo_conf_threshold, + nms_thresh=config.yoeo_nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) self._seg_postprocessor: utils.ISegmentationPostProcessor = utils.DefaultSegmentationPostProcessor( @@ -248,13 +248,13 @@ def __init__( logger.debug(f"Leaving {self.__class__.__name__} constructor") - def configure(self, config: dict) -> None: + def configure(self, config) -> None: super().configure(config) self._det_postprocessor.configure( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer.shape[2], - conf_thresh=config["yoeo_conf_threshold"], - nms_thresh=config["yoeo_nms_threshold"], + conf_thresh=config.yoeo_conf_threshold, + nms_thresh=config.yoeo_nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) @@ -284,7 +284,7 @@ class YOEOHandlerOpenVino(YOEOHandlerTemplate): def __init__( self, - config: dict, + config, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -320,8 +320,8 @@ def __init__( self._det_postprocessor: utils.IDetectionPostProcessor = utils.DefaultDetectionPostProcessor( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer.shape[2], - conf_thresh=config["yoeo_conf_threshold"], - nms_thresh=config["yoeo_nms_threshold"], + conf_thresh=config.yoeo_conf_threshold, + nms_thresh=config.yoeo_nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) self._seg_postprocessor: utils.ISegmentationPostProcessor = utils.DefaultSegmentationPostProcessor( @@ -337,13 +337,13 @@ def _select_device(self) -> str: device = "CPU" return device - def configure(self, config: dict) -> None: + def configure(self, config) -> None: super().configure(config) self._det_postprocessor.configure( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer.shape[2], - conf_thresh=config["yoeo_conf_threshold"], - nms_thresh=config["yoeo_nms_threshold"], + conf_thresh=config.yoeo_conf_threshold, + nms_thresh=config.yoeo_nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) @@ -372,7 +372,7 @@ class YOEOHandlerPytorch(YOEOHandlerTemplate): def __init__( self, - config: dict, + config, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -398,8 +398,8 @@ def __init__( logger.debug(f"Loading files...\n\t{config_path}\n\t{weights_path}") self._model = torch_models.load_model(config_path, weights_path) - self._conf_thresh: float = config["yoeo_conf_threshold"] - self._nms_thresh: float = config["yoeo_nms_threshold"] + self._conf_thresh: float = config.yoeo_conf_threshold + self._nms_thresh: float = config.yoeo_nms_threshold self._group_config: torch_GroupConfig = self._update_group_config() logger.debug(f"Leaving {self.__class__.__name__} constructor") @@ -409,10 +409,10 @@ def _update_group_config(self): return self.torch_group_config(group_ids=robot_class_ids, surrogate_id=robot_class_ids[0]) - def configure(self, config: dict) -> None: + def configure(self, config) -> None: super().configure(config) - self._conf_thresh = config["yoeo_conf_threshold"] - self._nms_thresh = config["yoeo_nms_threshold"] + self._conf_thresh = config.yoeo_conf_threshold + self._nms_thresh = config.yoeo_nms_threshold self._group_config = self._update_group_config() @staticmethod @@ -446,7 +446,7 @@ class YOEOHandlerTVM(YOEOHandlerTemplate): def __init__( self, - config: dict, + config, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -485,8 +485,8 @@ def __init__( self._det_postprocessor: utils.IDetectionPostProcessor = utils.DefaultDetectionPostProcessor( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer_shape[2], - conf_thresh=config["yoeo_conf_threshold"], - nms_thresh=config["yoeo_nms_threshold"], + conf_thresh=config.yoeo_conf_threshold, + nms_thresh=config.yoeo_nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) self._seg_postprocessor: utils.ISegmentationPostProcessor = utils.DefaultSegmentationPostProcessor( @@ -495,13 +495,13 @@ def __init__( logger.debug(f"Leaving {self.__class__.__name__} constructor") - def configure(self, config: dict) -> None: + def configure(self, config) -> None: super().configure(config) self._det_postprocessor.configure( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer_shape[2], - conf_thresh=config["yoeo_conf_threshold"], - nms_thresh=config["yoeo_nms_threshold"], + conf_thresh=config.yoeo_conf_threshold, + nms_thresh=config.yoeo_nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) From 0a56fc6c740aee4f2647a8bb618c79e78e9421c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:38:58 +0000 Subject: [PATCH 6/7] Address code review feedback: Add parameter hierarchy, type hints, and rework ros_utils Co-authored-by: Flova <15075613+Flova@users.noreply.github.com> --- bitbots_vision/bitbots_vision/vision.py | 28 ++++------ .../vision_modules/ros_utils.py | 26 +++++++++ .../vision_modules/yoeo/object_manager.py | 23 +++----- .../vision_modules/yoeo/yoeo_handlers.py | 56 ++++++++++--------- bitbots_vision/config/vision_parameters.yaml | 51 +++++++++-------- 5 files changed, 102 insertions(+), 82 deletions(-) diff --git a/bitbots_vision/bitbots_vision/vision.py b/bitbots_vision/bitbots_vision/vision.py index 2bf6667fd..bf246c0bb 100755 --- a/bitbots_vision/bitbots_vision/vision.py +++ b/bitbots_vision/bitbots_vision/vision.py @@ -61,16 +61,10 @@ def __init__(self) -> None: ros_utils.update_own_team_color(self) # Configure vision with initial parameters - self._configure_vision_from_config() + self._configure_vision(self.config) logger.debug(f"Leaving {self.__class__.__name__} constructor") - def _configure_vision_from_config(self) -> None: - """ - Configure vision components using the current config. - """ - self._configure_vision(self.config) - def _dynamic_reconfigure_callback(self, params) -> SetParametersResult: """ Callback for the dynamic reconfigure configuration. @@ -81,7 +75,7 @@ def _dynamic_reconfigure_callback(self, params) -> SetParametersResult: self.config = self.param_listener.get_params() # Configure vision with the updated config - self._configure_vision_from_config() + self._configure_vision(self.config) return SetParametersResult(successful=True) @@ -122,18 +116,18 @@ def make_vision_component( if config.component_debug_image_active: self._vision_components.append(make_vision_component(yoeo.DebugImageComponent)) - # For the subscriber update, handle the topic name directly + # For the subscriber update, use the improved ros_utils function old_topic = getattr(self, '_last_img_topic', None) current_topic = config.ROS_img_msg_topic - if old_topic != current_topic: - self._sub_image = self.create_subscription( - Image, - current_topic, - self._run_vision_pipeline, - 1 - ) - logger.debug(f"Registered new subscriber at {current_topic}") + self._sub_image = ros_utils.create_or_update_subscriber_with_config( + self, + old_topic, + current_topic, + self._sub_image, + Image, + self._run_vision_pipeline, + ) # Remember this topic for next time self._last_img_topic = current_topic diff --git a/bitbots_vision/bitbots_vision/vision_modules/ros_utils.py b/bitbots_vision/bitbots_vision/vision_modules/ros_utils.py index 53a41fcad..5ee1b3b3c 100644 --- a/bitbots_vision/bitbots_vision/vision_modules/ros_utils.py +++ b/bitbots_vision/bitbots_vision/vision_modules/ros_utils.py @@ -46,6 +46,32 @@ class RobotColor(Enum): own_team_color: RobotColor = RobotColor.UNKNOWN +def create_or_update_subscriber_with_config( + node, old_topic, new_topic, subscriber_object, data_class, callback, qos_profile=1, callback_group=None +): + """ + Creates or updates a subscriber using direct topic names instead of config dicts + + :param node: ROS node to which the publisher is bound + :param old_topic: Previous topic name + :param new_topic: New topic name + :param subscriber_object: The python object, that represents the subscriber + :param data_class: Data type class for ROS messages of the topic we want to subscribe + :param callback: The subscriber callback function + :param qos_profile: A QoSProfile or a history depth to apply to the subscription. + :param callback_group: The callback group for the subscription. + :return: adjusted subscriber object + """ + # Check if topic has changed + if old_topic != new_topic: + # Create the new subscriber + subscriber_object = node.create_subscription( + data_class, new_topic, callback, qos_profile, callback_group=callback_group + ) + logger.debug("Registered new subscriber at " + str(new_topic)) + return subscriber_object + + def create_or_update_subscriber( node, old_config, new_config, subscriber_object, topic_key, data_class, callback, qos_profile=1, callback_group=None ): diff --git a/bitbots_vision/bitbots_vision/vision_modules/yoeo/object_manager.py b/bitbots_vision/bitbots_vision/vision_modules/yoeo/object_manager.py index 7749ffffe..72dbda40d 100644 --- a/bitbots_vision/bitbots_vision/vision_modules/yoeo/object_manager.py +++ b/bitbots_vision/bitbots_vision/vision_modules/yoeo/object_manager.py @@ -4,6 +4,7 @@ import rclpy from bitbots_vision.vision_modules import ros_utils +from bitbots_vision.vision_parameters import bitbots_vision as parameters from . import yoeo_handlers from .model_config import ModelConfig, ModelConfigLoader @@ -72,14 +73,14 @@ def is_team_color_detection_supported(cls) -> bool: return cls._model_config.team_colors_are_provided() @classmethod - def configure(cls, config) -> None: + def configure(cls, config: parameters.Params) -> None: if not cls._package_directory_set: logger.error("Package directory not set!") - framework = config.yoeo_framework + framework = config.yoeo.framework cls._verify_framework_parameter(framework) - model_path = cls._get_full_model_path(config.yoeo_model_path) + model_path = cls._get_full_model_path(config.yoeo.model_path) cls._verify_required_neural_network_files_exist(framework, model_path) cls._configure_yoeo_instance(config, framework, model_path) @@ -107,7 +108,7 @@ def _model_files_exist(cls, framework: str, model_path: str) -> bool: return cls._HANDLERS_BY_NAME[framework].model_files_exist(model_path) @classmethod - def _configure_yoeo_instance(cls, config, framework: str, model_path: str) -> None: + def _configure_yoeo_instance(cls, config: parameters.Params, framework: str, model_path: str) -> None: if cls._new_yoeo_handler_is_needed(framework, model_path): cls._load_model_config(model_path) cls._instantiate_new_yoeo_handler(config, framework, model_path) @@ -124,7 +125,7 @@ def _load_model_config(cls, model_path: str) -> None: cls._model_config = ModelConfigLoader.load_from(model_path) @classmethod - def _instantiate_new_yoeo_handler(cls, config, framework: str, model_path: str) -> None: + def _instantiate_new_yoeo_handler(cls, config: parameters.Params, framework: str, model_path: str) -> None: cls._yoeo_instance = cls._HANDLERS_BY_NAME[framework]( config, model_path, @@ -135,15 +136,9 @@ def _instantiate_new_yoeo_handler(cls, config, framework: str, model_path: str) logger.info(f"Using {cls._yoeo_instance.__class__.__name__}") @classmethod - def _yoeo_parameters_have_changed(cls, new_config) -> bool: + def _yoeo_parameters_have_changed(cls, new_config: parameters.Params) -> bool: if cls._config is None: return True - # Compare YOEO-related parameters using direct attribute access - return ( - cls._config.yoeo_framework != new_config.yoeo_framework or - cls._config.yoeo_model_path != new_config.yoeo_model_path or - cls._config.yoeo_nms_threshold != new_config.yoeo_nms_threshold or - cls._config.yoeo_conf_threshold != new_config.yoeo_conf_threshold or - cls._config.caching != new_config.caching - ) + # Compare YOEO parameters using the hierarchical structure + return cls._config.yoeo != new_config.yoeo diff --git a/bitbots_vision/bitbots_vision/vision_modules/yoeo/yoeo_handlers.py b/bitbots_vision/bitbots_vision/vision_modules/yoeo/yoeo_handlers.py index 7facb40df..735a13266 100644 --- a/bitbots_vision/bitbots_vision/vision_modules/yoeo/yoeo_handlers.py +++ b/bitbots_vision/bitbots_vision/vision_modules/yoeo/yoeo_handlers.py @@ -9,6 +9,8 @@ import numpy as np import rclpy +from bitbots_vision.vision_parameters import bitbots_vision as parameters + from bitbots_vision.vision_modules.candidate import Candidate from . import utils @@ -22,7 +24,7 @@ class IYOEOHandler(ABC): """ @abstractmethod - def configure(self, config) -> None: + def configure(self, config: parameters.Params) -> None: """ Allows to (re-) configure the YOEO handler. """ @@ -98,7 +100,7 @@ class YOEOHandlerTemplate(IYOEOHandler): def __init__( self, - config, + config: parameters.Params, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -121,7 +123,7 @@ def __init__( logger.debug("Leaving YOEOHandlerTemplate constructor") - def configure(self, config) -> None: + def configure(self, config: parameters.Params) -> None: self._use_caching = config.caching def get_available_detection_class_names(self) -> list[str]: @@ -211,7 +213,7 @@ class YOEOHandlerONNX(YOEOHandlerTemplate): def __init__( self, - config, + config: parameters.Params, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -238,8 +240,8 @@ def __init__( self._det_postprocessor: utils.IDetectionPostProcessor = utils.DefaultDetectionPostProcessor( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer.shape[2], - conf_thresh=config.yoeo_conf_threshold, - nms_thresh=config.yoeo_nms_threshold, + conf_thresh=config.yoeo.conf_threshold, + nms_thresh=config.yoeo.nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) self._seg_postprocessor: utils.ISegmentationPostProcessor = utils.DefaultSegmentationPostProcessor( @@ -248,13 +250,13 @@ def __init__( logger.debug(f"Leaving {self.__class__.__name__} constructor") - def configure(self, config) -> None: + def configure(self, config: parameters.Params) -> None: super().configure(config) self._det_postprocessor.configure( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer.shape[2], - conf_thresh=config.yoeo_conf_threshold, - nms_thresh=config.yoeo_nms_threshold, + conf_thresh=config.yoeo.conf_threshold, + nms_thresh=config.yoeo.nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) @@ -284,7 +286,7 @@ class YOEOHandlerOpenVino(YOEOHandlerTemplate): def __init__( self, - config, + config: parameters.Params, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -320,8 +322,8 @@ def __init__( self._det_postprocessor: utils.IDetectionPostProcessor = utils.DefaultDetectionPostProcessor( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer.shape[2], - conf_thresh=config.yoeo_conf_threshold, - nms_thresh=config.yoeo_nms_threshold, + conf_thresh=config.yoeo.conf_threshold, + nms_thresh=config.yoeo.nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) self._seg_postprocessor: utils.ISegmentationPostProcessor = utils.DefaultSegmentationPostProcessor( @@ -337,13 +339,13 @@ def _select_device(self) -> str: device = "CPU" return device - def configure(self, config) -> None: + def configure(self, config: parameters.Params) -> None: super().configure(config) self._det_postprocessor.configure( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer.shape[2], - conf_thresh=config.yoeo_conf_threshold, - nms_thresh=config.yoeo_nms_threshold, + conf_thresh=config.yoeo.conf_threshold, + nms_thresh=config.yoeo.nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) @@ -372,7 +374,7 @@ class YOEOHandlerPytorch(YOEOHandlerTemplate): def __init__( self, - config, + config: parameters.Params, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -398,8 +400,8 @@ def __init__( logger.debug(f"Loading files...\n\t{config_path}\n\t{weights_path}") self._model = torch_models.load_model(config_path, weights_path) - self._conf_thresh: float = config.yoeo_conf_threshold - self._nms_thresh: float = config.yoeo_nms_threshold + self._conf_thresh: float = config.yoeo.conf_threshold + self._nms_thresh: float = config.yoeo.nms_threshold self._group_config: torch_GroupConfig = self._update_group_config() logger.debug(f"Leaving {self.__class__.__name__} constructor") @@ -409,10 +411,10 @@ def _update_group_config(self): return self.torch_group_config(group_ids=robot_class_ids, surrogate_id=robot_class_ids[0]) - def configure(self, config) -> None: + def configure(self, config: parameters.Params) -> None: super().configure(config) - self._conf_thresh = config.yoeo_conf_threshold - self._nms_thresh = config.yoeo_nms_threshold + self._conf_thresh = config.yoeo.conf_threshold + self._nms_thresh = config.yoeo.nms_threshold self._group_config = self._update_group_config() @staticmethod @@ -446,7 +448,7 @@ class YOEOHandlerTVM(YOEOHandlerTemplate): def __init__( self, - config, + config: parameters.Params, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -485,8 +487,8 @@ def __init__( self._det_postprocessor: utils.IDetectionPostProcessor = utils.DefaultDetectionPostProcessor( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer_shape[2], - conf_thresh=config.yoeo_conf_threshold, - nms_thresh=config.yoeo_nms_threshold, + conf_thresh=config.yoeo.conf_threshold, + nms_thresh=config.yoeo.nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) self._seg_postprocessor: utils.ISegmentationPostProcessor = utils.DefaultSegmentationPostProcessor( @@ -495,13 +497,13 @@ def __init__( logger.debug(f"Leaving {self.__class__.__name__} constructor") - def configure(self, config) -> None: + def configure(self, config: parameters.Params) -> None: super().configure(config) self._det_postprocessor.configure( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer_shape[2], - conf_thresh=config.yoeo_conf_threshold, - nms_thresh=config.yoeo_nms_threshold, + conf_thresh=config.yoeo.conf_threshold, + nms_thresh=config.yoeo.nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) diff --git a/bitbots_vision/config/vision_parameters.yaml b/bitbots_vision/config/vision_parameters.yaml index dc5b98732..afecfc2f2 100644 --- a/bitbots_vision/config/vision_parameters.yaml +++ b/bitbots_vision/config/vision_parameters.yaml @@ -79,30 +79,33 @@ bitbots_vision: description: "ROS topic of the field mask debug image message" read_only: true - # YOEO model parameters - yoeo_model_path: - type: string - default_value: "2022_10_07_flo_torso21_yoeox" - description: "Name of YOEO model" - - yoeo_nms_threshold: - type: double - default_value: 0.4 - description: "YOEO Non-maximum suppression threshold" - validation: - bounds<>: [0.0, 1.0] - - yoeo_conf_threshold: - type: double - default_value: 0.5 - description: "YOEO confidence threshold" - validation: - bounds<>: [0.0, 1.0] - - yoeo_framework: - type: string - default_value: "tvm" - description: "The neural network framework that should be used ['pytorch', 'openvino', 'onnx', 'tvm']" + # YOEO model parameters with hierarchy + yoeo: + model_path: + type: string + default_value: "2022_10_07_flo_torso21_yoeox" + description: "Name of YOEO model" + + nms_threshold: + type: double + default_value: 0.4 + description: "YOEO Non-maximum suppression threshold" + validation: + bounds<>: [0.0, 1.0] + + conf_threshold: + type: double + default_value: 0.5 + description: "YOEO confidence threshold" + validation: + bounds<>: [0.0, 1.0] + + framework: + type: string + default_value: "tvm" + description: "The neural network framework that should be used" + validation: + one_of<>: [["pytorch", "openvino", "onnx", "tvm"]] # Ball detection parameters ball_candidate_rating_threshold: From 538a4012359601aeee8ab514d60f28acfd86838a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:49:15 +0000 Subject: [PATCH 7/7] Pass yoeo substruct directly to YOEO components instead of full config Co-authored-by: Flova <15075613+Flova@users.noreply.github.com> --- bitbots_vision/bitbots_vision/vision.py | 2 +- .../vision_modules/yoeo/object_manager.py | 24 ++++---- .../vision_modules/yoeo/yoeo_handlers.py | 58 +++++++++---------- bitbots_vision/config/vision_parameters.yaml | 13 ++--- 4 files changed, 48 insertions(+), 49 deletions(-) diff --git a/bitbots_vision/bitbots_vision/vision.py b/bitbots_vision/bitbots_vision/vision.py index bf246c0bb..cb4a26b56 100755 --- a/bitbots_vision/bitbots_vision/vision.py +++ b/bitbots_vision/bitbots_vision/vision.py @@ -80,7 +80,7 @@ def _dynamic_reconfigure_callback(self, params) -> SetParametersResult: return SetParametersResult(successful=True) def _configure_vision(self, config) -> None: - yoeo.YOEOObjectManager.configure(config) + yoeo.YOEOObjectManager.configure(config.yoeo) debug_image = debug.DebugImage(config.component_debug_image_active) self._debug_image = debug_image diff --git a/bitbots_vision/bitbots_vision/vision_modules/yoeo/object_manager.py b/bitbots_vision/bitbots_vision/vision_modules/yoeo/object_manager.py index 72dbda40d..cca167f8f 100644 --- a/bitbots_vision/bitbots_vision/vision_modules/yoeo/object_manager.py +++ b/bitbots_vision/bitbots_vision/vision_modules/yoeo/object_manager.py @@ -73,19 +73,19 @@ def is_team_color_detection_supported(cls) -> bool: return cls._model_config.team_colors_are_provided() @classmethod - def configure(cls, config: parameters.Params) -> None: + def configure(cls, yoeo_config) -> None: if not cls._package_directory_set: logger.error("Package directory not set!") - framework = config.yoeo.framework + framework = yoeo_config.framework cls._verify_framework_parameter(framework) - model_path = cls._get_full_model_path(config.yoeo.model_path) + model_path = cls._get_full_model_path(yoeo_config.model_path) cls._verify_required_neural_network_files_exist(framework, model_path) - cls._configure_yoeo_instance(config, framework, model_path) + cls._configure_yoeo_instance(yoeo_config, framework, model_path) - cls._config = config + cls._config = yoeo_config cls._framework = framework cls._model_path = model_path @@ -108,13 +108,13 @@ def _model_files_exist(cls, framework: str, model_path: str) -> bool: return cls._HANDLERS_BY_NAME[framework].model_files_exist(model_path) @classmethod - def _configure_yoeo_instance(cls, config: parameters.Params, framework: str, model_path: str) -> None: + def _configure_yoeo_instance(cls, yoeo_config, framework: str, model_path: str) -> None: if cls._new_yoeo_handler_is_needed(framework, model_path): cls._load_model_config(model_path) cls._instantiate_new_yoeo_handler(config, framework, model_path) - elif cls._yoeo_parameters_have_changed(config): + elif cls._yoeo_parameters_have_changed(yoeo_config): assert cls._yoeo_instance is not None, "YOEO handler instance not set!" - cls._yoeo_instance.configure(config) + cls._yoeo_instance.configure(yoeo_config) @classmethod def _new_yoeo_handler_is_needed(cls, framework: str, model_path: str) -> bool: @@ -125,9 +125,9 @@ def _load_model_config(cls, model_path: str) -> None: cls._model_config = ModelConfigLoader.load_from(model_path) @classmethod - def _instantiate_new_yoeo_handler(cls, config: parameters.Params, framework: str, model_path: str) -> None: + def _instantiate_new_yoeo_handler(cls, yoeo_config, framework: str, model_path: str) -> None: cls._yoeo_instance = cls._HANDLERS_BY_NAME[framework]( - config, + yoeo_config, model_path, cls._model_config.get_detection_classes(), cls._model_config.get_robot_class_ids(), @@ -136,9 +136,9 @@ def _instantiate_new_yoeo_handler(cls, config: parameters.Params, framework: str logger.info(f"Using {cls._yoeo_instance.__class__.__name__}") @classmethod - def _yoeo_parameters_have_changed(cls, new_config: parameters.Params) -> bool: + def _yoeo_parameters_have_changed(cls, new_yoeo_config) -> bool: if cls._config is None: return True # Compare YOEO parameters using the hierarchical structure - return cls._config.yoeo != new_config.yoeo + return cls._config != new_yoeo_config diff --git a/bitbots_vision/bitbots_vision/vision_modules/yoeo/yoeo_handlers.py b/bitbots_vision/bitbots_vision/vision_modules/yoeo/yoeo_handlers.py index 735a13266..50061f285 100644 --- a/bitbots_vision/bitbots_vision/vision_modules/yoeo/yoeo_handlers.py +++ b/bitbots_vision/bitbots_vision/vision_modules/yoeo/yoeo_handlers.py @@ -24,7 +24,7 @@ class IYOEOHandler(ABC): """ @abstractmethod - def configure(self, config: parameters.Params) -> None: + def configure(self, yoeo_config) -> None: """ Allows to (re-) configure the YOEO handler. """ @@ -100,7 +100,7 @@ class YOEOHandlerTemplate(IYOEOHandler): def __init__( self, - config: parameters.Params, + yoeo_config, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -119,12 +119,12 @@ def __init__( self._seg_class_names: list[str] = seg_class_names self._seg_masks: dict = dict() - self._use_caching: bool = config.caching + self._use_caching: bool = yoeo_config.caching logger.debug("Leaving YOEOHandlerTemplate constructor") - def configure(self, config: parameters.Params) -> None: - self._use_caching = config.caching + def configure(self, yoeo_config) -> None: + self._use_caching = yoeo_config.caching def get_available_detection_class_names(self) -> list[str]: return self._det_class_names @@ -213,7 +213,7 @@ class YOEOHandlerONNX(YOEOHandlerTemplate): def __init__( self, - config: parameters.Params, + yoeo_config, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -240,8 +240,8 @@ def __init__( self._det_postprocessor: utils.IDetectionPostProcessor = utils.DefaultDetectionPostProcessor( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer.shape[2], - conf_thresh=config.yoeo.conf_threshold, - nms_thresh=config.yoeo.nms_threshold, + conf_thresh=yoeo_config.conf_threshold, + nms_thresh=yoeo_config.nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) self._seg_postprocessor: utils.ISegmentationPostProcessor = utils.DefaultSegmentationPostProcessor( @@ -250,13 +250,13 @@ def __init__( logger.debug(f"Leaving {self.__class__.__name__} constructor") - def configure(self, config: parameters.Params) -> None: + def configure(self, yoeo_config) -> None: super().configure(config) self._det_postprocessor.configure( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer.shape[2], - conf_thresh=config.yoeo.conf_threshold, - nms_thresh=config.yoeo.nms_threshold, + conf_thresh=yoeo_config.conf_threshold, + nms_thresh=yoeo_config.nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) @@ -286,7 +286,7 @@ class YOEOHandlerOpenVino(YOEOHandlerTemplate): def __init__( self, - config: parameters.Params, + yoeo_config, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -322,8 +322,8 @@ def __init__( self._det_postprocessor: utils.IDetectionPostProcessor = utils.DefaultDetectionPostProcessor( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer.shape[2], - conf_thresh=config.yoeo.conf_threshold, - nms_thresh=config.yoeo.nms_threshold, + conf_thresh=yoeo_config.conf_threshold, + nms_thresh=yoeo_config.nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) self._seg_postprocessor: utils.ISegmentationPostProcessor = utils.DefaultSegmentationPostProcessor( @@ -339,13 +339,13 @@ def _select_device(self) -> str: device = "CPU" return device - def configure(self, config: parameters.Params) -> None: + def configure(self, yoeo_config) -> None: super().configure(config) self._det_postprocessor.configure( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer.shape[2], - conf_thresh=config.yoeo.conf_threshold, - nms_thresh=config.yoeo.nms_threshold, + conf_thresh=yoeo_config.conf_threshold, + nms_thresh=yoeo_config.nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) @@ -374,7 +374,7 @@ class YOEOHandlerPytorch(YOEOHandlerTemplate): def __init__( self, - config: parameters.Params, + yoeo_config, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -400,8 +400,8 @@ def __init__( logger.debug(f"Loading files...\n\t{config_path}\n\t{weights_path}") self._model = torch_models.load_model(config_path, weights_path) - self._conf_thresh: float = config.yoeo.conf_threshold - self._nms_thresh: float = config.yoeo.nms_threshold + self._conf_thresh: float = yoeo_config.conf_threshold + self._nms_thresh: float = yoeo_config.nms_threshold self._group_config: torch_GroupConfig = self._update_group_config() logger.debug(f"Leaving {self.__class__.__name__} constructor") @@ -411,10 +411,10 @@ def _update_group_config(self): return self.torch_group_config(group_ids=robot_class_ids, surrogate_id=robot_class_ids[0]) - def configure(self, config: parameters.Params) -> None: + def configure(self, yoeo_config) -> None: super().configure(config) - self._conf_thresh = config.yoeo.conf_threshold - self._nms_thresh = config.yoeo.nms_threshold + self._conf_thresh = yoeo_config.conf_threshold + self._nms_thresh = yoeo_config.nms_threshold self._group_config = self._update_group_config() @staticmethod @@ -448,7 +448,7 @@ class YOEOHandlerTVM(YOEOHandlerTemplate): def __init__( self, - config: parameters.Params, + yoeo_config, model_directory: str, det_class_names: list[str], det_robot_class_ids: list[int], @@ -487,8 +487,8 @@ def __init__( self._det_postprocessor: utils.IDetectionPostProcessor = utils.DefaultDetectionPostProcessor( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer_shape[2], - conf_thresh=config.yoeo.conf_threshold, - nms_thresh=config.yoeo.nms_threshold, + conf_thresh=yoeo_config.conf_threshold, + nms_thresh=yoeo_config.nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) self._seg_postprocessor: utils.ISegmentationPostProcessor = utils.DefaultSegmentationPostProcessor( @@ -497,13 +497,13 @@ def __init__( logger.debug(f"Leaving {self.__class__.__name__} constructor") - def configure(self, config: parameters.Params) -> None: + def configure(self, yoeo_config) -> None: super().configure(config) self._det_postprocessor.configure( image_preprocessor=self._img_preprocessor, output_img_size=self._input_layer_shape[2], - conf_thresh=config.yoeo.conf_threshold, - nms_thresh=config.yoeo.nms_threshold, + conf_thresh=yoeo_config.conf_threshold, + nms_thresh=yoeo_config.nms_threshold, robot_class_ids=self.get_robot_class_ids(), ) diff --git a/bitbots_vision/config/vision_parameters.yaml b/bitbots_vision/config/vision_parameters.yaml index afecfc2f2..e6106bc7d 100644 --- a/bitbots_vision/config/vision_parameters.yaml +++ b/bitbots_vision/config/vision_parameters.yaml @@ -106,6 +106,11 @@ bitbots_vision: description: "The neural network framework that should be used" validation: one_of<>: [["pytorch", "openvino", "onnx", "tvm"]] + + caching: + type: bool + default_value: true + description: "Used to deactivate caching for profiling reasons" # Ball detection parameters ball_candidate_rating_threshold: @@ -120,10 +125,4 @@ bitbots_vision: default_value: 1 description: "The maximum number of balls that should be published" validation: - bounds<>: [0, 50] - - # Caching parameter - caching: - type: bool - default_value: true - description: "Used to deactivate caching for profiling reasons" \ No newline at end of file + bounds<>: [0, 50] \ No newline at end of file