Skip to content

Conversation

@Juliaj
Copy link
Collaborator

@Juliaj Juliaj commented Jan 8, 2026

Purpose

Proposed Changes

  • Code refactor based on Rethinking Usability and API Usability Design Considerations.

  • There are breaking changes introduced, see section below for details. To reduce the impact on applications using the old service names, a legacy service names flag (enable_legacy_service_names) has been introduced and the default behavior is backward compatible (defaults to true). For new applications using only the new service names (/detection, /segmentation), you can disable legacy names by setting the flag to false:

    Via launch file argument:

    ros2 launch examples/manipulation-demo.launch.py game_launcher:=... enable_legacy_service_names:=false

    Via environment variable:

    ENABLE_LEGACY_SERVICE_NAMES=false ros2 run rai_perception run_perception_services

    Via ROS2 parameter (in code):

    connector.node.declare_parameter("enable_legacy_service_names", False)

Migration Guide

Issues

  • Links to relevant issues

Testing

  • Unit tests added and ran.
  • manipulation demo, v1, v2. Prompt: swap any two cubes
  • manipulation-streamlit with both versions. Prompt: "Place each apple on top of a cube", "Build a tower from cubes" and "Arrange objects in a line".
  • ROSBot - XL demo. Prompt: drive to kitchen
  • rai_semap
  • rai_bench. Test: manipulation_o3de.py started with manipulation_o3de.py without any failure.

To start manipulation-streamlit with both versions

# v2
AGENT_VERSION=v2 streamlit run examples/manipulation-demo-streamlit.py

# v1
AGENT_VERSION=v1 streamlit run examples/manipulation-demo-streamlit.py

Summary by CodeRabbit

  • New Features

    • Added service-based perception architecture for object detection and segmentation.
    • Introduced gripping point estimation tool with configurable filtering and estimation strategies (centroid, top-plane, biggest-plane).
    • Added debug visualization and intermediate pipeline stage publishing for perception workflows.
    • New perception presets for rapid configuration setup.
  • Documentation

    • Added comprehensive API design considerations guide for framework extensions.
    • Added usability redesign documentation for perception module.
  • Bug Fixes & Improvements

    • Enhanced ROS2 error handling and parameter utilities.
    • Improved test exclusion filtering to skip manual tests.

✏️ Tip: You can customize this high-level summary in your review settings.

@codecov
Copy link

codecov bot commented Jan 8, 2026

Codecov Report

❌ Patch coverage is 98.37398% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.74%. Comparing base (d506429) to head (64d74fd).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/rai_core/rai/tools/ros2/manipulation/custom.py 92.30% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #750      +/-   ##
==========================================
+ Coverage   65.79%   69.74%   +3.95%     
==========================================
  Files          78       82       +4     
  Lines        3403     3514     +111     
==========================================
+ Hits         2239     2451     +212     
+ Misses       1164     1063     -101     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@Juliaj Juliaj marked this pull request as draft January 8, 2026 23:51
@Juliaj Juliaj changed the title feat: redesign API with tiered structure and improve 3D gripping point detection feat: redesign rai_perception API with tiered structure and improve 3D gripping point detection Jan 9, 2026
@Juliaj Juliaj force-pushed the jj/feat/3dpipe_and_usability branch from 7aca530 to 09674aa Compare January 12, 2026 05:28
@Juliaj Juliaj marked this pull request as ready for review January 12, 2026 10:14
@Juliaj Juliaj force-pushed the jj/feat/3dpipe_and_usability branch from 32d8802 to 4af6d1b Compare January 13, 2026 03:53
@jmatejcz
Copy link
Contributor

Regarding rai_sim and rai_bench, I don't think a lot of changes will be required. Mainly naming changes, switching to new tools or changing paths. For example in src/rai_bench/rai_bench/manipulation_o3de/predefined/configs/o3de_config.yaml

@maciejmajek
Copy link
Member

I will start with coderabbit to speed things up.

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

@maciejmajek I'll conduct a full review of the PR now. This will cover all the changes including the API redesign, breaking changes, and the perception logic merger from rai_semap.

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

Walkthrough

Major architectural refactoring and expansion of the RAI perception module introducing new algorithms, services, and components alongside deprecation of legacy agents. Updates include new ROS2 communication utilities, timeout decorators, comprehensive gripping-point extraction pipeline, model registries, and extensive test coverage. Multiple packages reorganized with service-oriented architecture replacing direct agent instantiation.

Changes

Cohort / File(s) Summary
CI & Configuration
.github/workflows/poetry-test.yml, pyproject.toml, src/rai_core/pyproject.toml
Updated pytest markers to exclude both "billable" and "manual" tests; bumped rai_core version from 2.7.0 to 2.8.0; added manual marker to pytest configuration.
Documentation
docs/API_documentation/connectors/ROS_2_Connectors.md, docs/api_design_considerations.md, docs/extensions/rethinking_usability.md, src/rai_extensions/rai_perception/README.md, src/rai_extensions/rai_perception/follow-ups.md
Added ROS2 utilities documentation (ROS2ServiceError, ROS2ParameterError, get_param_value); introduced comprehensive API design guidelines and usability considerations; documented debug mode for gripping points tool; detailed post-refactor migration path for agents to services.
ROS2 Communication Utilities
src/rai_core/rai/communication/ros2/exceptions.py, src/rai_core/rai/communication/ros2/parameters.py, src/rai_core/rai/communication/ros2/__init__.py
New exception classes ROS2ServiceError and ROS2ParameterError with contextual error information; added get_param_value helper for safe parameter retrieval with type extraction; exposed new utilities in public API.
Timeout Utilities
src/rai_core/rai/tools/timeout.py, src/rai_core/rai/tools/__init__.py
Introduced RaiTimeoutError exception and timeout/timeout_method decorators using ThreadPoolExecutor for per-call timeouts; re-exported as public API; high-density logic with resource management.
Examples
examples/manipulation-demo.py, examples/manipulation-demo-v1.py
Created self-contained create_agent() function for manipulation demo with ROS2 initialization, tool configuration, and agent creation; added simpler interactive loop example; internal implementation replaces external dependency.
Perception Algorithms (New)
src/rai_extensions/rai_perception/rai_perception/algorithms/__init__.py, rai_perception/algorithms/boxer.py, rai_perception/algorithms/segmenter.py, rai_perception/algorithms/point_cloud.py
New detection (GDBoxer) and segmentation (GDSegmenter) algorithm implementations with device management and model initialization; depth-to-point-cloud conversion utility; organized as low-level algorithm package.
Perception Components (New)
src/rai_extensions/rai_perception/rai_perception/components/*
Comprehensive perception pipeline: PointCloudFromSegmentation (extraction & transformation), PointCloudFilter (configurable outlier removal), GrippingPointEstimator (multiple strategies), configuration classes; exception hierarchy (PerceptionError, PerceptionAlgorithmError, PerceptionValidationError); utilities (perception_utils, service_utils, topic_utils, visualization_utils); presets for grasp strategies. High-density logic (~1400 LOC across multiple files).
Perception Services (New)
src/rai_extensions/rai_perception/rai_perception/services/*, scripts/run_perception_services.py
BaseVisionService base class with model registry support; DetectionService and SegmentationService with dynamic model loading and ROS2 service integration; weight management utilities (download, load with corruption recovery); service orchestration script.
Perception Tools (Refactored)
src/rai_extensions/rai_perception/rai_perception/tools/gdino_tools.py, gripping_points_tools.py, segmentation_tools.py, __init__.py
GetDetectionTool and GetDistanceToObjectsTool now use dynamic service names and error handling (ROS2ServiceError); new GetObjectGrippingPointsTool with full pipeline orchestration, debug visualization, and service introspection (~700 LOC); expanded tool exports.
Perception Agents (Deprecated)
src/rai_extensions/rai_perception/rai_perception/agents/*
GroundingDinoAgent and GroundedSamAgent refactored to delegate to services (BaseAgent inheritance, deprecation warnings); BaseVisionAgent simplified with utility imports; removed in-process model loading; service wrapper helper added.
Perception Module Organization
src/rai_extensions/rai_perception/rai_perception/__init__.py, vision_markup/__init__.py, models/__init__.py
Consolidated public API exports for tools, components, algorithms; introduced model registries (detection, segmentation) for dynamic model selection; deprecated vision_markup module with delegation to algorithms; updated service name constants.
Configuration & Scripts
src/rai_extensions/rai_perception/configs/*, examples/talker.py, src/rai_semap/ros2/config/detection_publisher.yaml
New detection_publisher and perception_utils YAML configs; example service endpoints updated to use new /detection and /segmentation service names; launch script path updated to use rai_perception component.
Tests - New Suites
tests/communication/ros2/test_exceptions.py, test_parameters.py, tests/rai_perception/algorithms/*, components/*, services/*, tools/*, vision_markup/*
Comprehensive test coverage for ROS2 utilities, new algorithms, perception components, services, and deprecated wrappers; extensive use of mocks, fixtures, and parameterization; high test density.
Tests - Configuration & Helpers
tests/conftest.py, tests/rai_perception/conftest.py, test_helpers.py, test_mocks.py
Enhanced pytest configuration with strategy and grasp options; comprehensive ROS2 mocking infrastructure (parameter tracking, service clients); helper functions for weights, fixtures, and patching; mock implementations (MockGDBoxer, MockGDSegmenter, EmptyBoxer, EmptySegmenter).
Tests - Removed
tests/rai_perception/test_grounded_sam.py, test_grounding_dino.py, test_run_perception_agents.py, tests/rai_semap/test_perception_utils.py
Removed deprecated agent test files as agents are transitioned to service-based architecture; moved tests to agent-specific modules under agents/.
Version Bump
src/rai_extensions/rai_perception/pyproject.toml
Bumped rai_perception from 0.1.5 to 0.2.0; updated authors list.
Public API Exports
src/rai_core/rai/__init__.py
Exported timeout decorator as public API from tools module.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • refactor: base api #518: Removed constructor arguments from BaseAgent class, directly enabling the refactored agent initialization pattern used in this PR's GroundingDinoAgent and GroundedSamAgent changes.
  • feat: various enhancements #508: Introduced ROS2 tool updates and manipulation demo examples that align with changes to examples/manipulation-demo.py and ROS2 integration patterns in this PR.
  • refactor: internal ros communication #335: Modified ROS2 communication patterns with improved service/client handling and future-based waiting that inform the service utilities and tool refactoring in this PR.

Suggested reviewers

  • jmatejcz
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately describes the main changes: redesigning the rai_perception API with a tiered structure and improving 3D gripping point detection through refactoring and merging of perception logic.
Description check ✅ Passed The PR description includes all required sections (Purpose, Proposed Changes, Issues, Testing) and provides substantial detail about the redesign, migration guide, and testing performed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In
`@src/rai_extensions/rai_perception/rai_perception/components/detection_publisher.py`:
- Around line 401-411: The image subscription callback image_callback currently
calls _process_image which can raise ROS2ServiceError; catch ROS2ServiceError
inside image_callback (around the call to _process_image) so the exception does
not escape the callback, log a clear message via the node/process logger
including the service name and error details, and return early to skip
processing this image; keep other exceptions unchanged or optionally log them
separately to avoid disrupting the executor.

In
`@src/rai_extensions/rai_perception/rai_perception/components/gripping_points.py`:
- Around line 55-62: Add a configurable timeout to
PointCloudFromSegmentationConfig (e.g., service_timeout: float =
Field(default=15.0, ...)) and update all places that call get_future_result to
pass this config value instead of relying on the 5s default; specifically modify
the code paths in this module that call get_future_result (referenced as
get_future_result) so they accept/obtain the PointCloudFromSegmentationConfig
instance and use its service_timeout when calling get_future_result (also update
any constructors or callers that instantiate PointCloudFromSegmentationConfig to
propagate the new field).
- Around line 629-707: The package is missing the scikit-learn dependency used
by _filter_dbscan, _filter_kmeans_largest, _filter_isolation_forest, and
_filter_lof; add scikit-learn = ">=1.0.0, <1.4.0" to the dependencies section of
rai_perception's pyproject.toml
(src/rai_extensions/rai_perception/pyproject.toml) so runtime imports of
sklearn.cluster, sklearn.ensemble, and sklearn.neighbors do not raise
ImportError.

In
`@src/rai_extensions/rai_perception/rai_perception/components/service_utils.py`:
- Around line 75-94: The function check_service_available currently calls
wait_for_ros2_services which treats timeout 0 as “wait indefinitely”; add a
guard at the top of check_service_available that if timeout_sec <= 0 performs a
non-blocking lookup using the connector’s node service registry (e.g., call
connector.node.get_service_names_and_types() and check whether service_name
appears) and return True/False accordingly, otherwise keep the existing
try/except around wait_for_ros2_services(connector, [service_name],
timeout=timeout_sec) returning True on success and False on
TimeoutError/ValueError.

In `@src/rai_extensions/rai_perception/rai_perception/components/topic_utils.py`:
- Around line 106-145: The detection-only branch currently assigns the method
object to detection_service and never sets segmentation_service, causing the
subsequent runtime check to always raise; fix by treating tools with a
service_name attribute as detection-only: if hasattr(tool, "service_name") set
detection_service = tool.service_name() if callable (or tool.service_name
otherwise), leave segmentation_service as None, and do not break validation that
requires GetObjectGrippingPointsTool for topics — instead adjust the later
validation so required_services = [detection_service] plus segmentation_service
if not None, and only require required_topics/get_config() when a
GetObjectGrippingPointsTool is present; ensure references in your edits point to
GetObjectGrippingPointsTool, service_name (call or attribute),
detection_service, segmentation_service, get_config, and required_topics.

In `@src/rai_semap/rai_semap/ros2/node.py`:
- Line 28: The project imports rai_perception in node.py (from
rai_perception.components.perception_utils import extract_pointcloud_from_bbox)
but pyproject.toml lacks that dependency; open rai_semap/pyproject.toml and add
rai_perception to the [tool.poetry.dependencies] (or equivalent) section—either
as a path-based dependency like the existing rai_core entry or with an
appropriate version constraint—then run your dependency tool (poetry
install/update) to update lockfiles so the package is available at runtime.
🟡 Minor comments (23)
src/rai_extensions/rai_perception/rai_perception/components/perception_utils.py-1-1 (1)

1-1: Typo in copyright header.

"Robotech.AI" should be "Robotec.AI".

Proposed fix
-# Copyright (C) 2025 Robotech.AI
+# Copyright (C) 2025 Robotec.AI
src/rai_extensions/rai_perception/rai_perception/agents/_helpers.py-30-31 (1)

30-31: Avoid calling Path.home() in default argument.

Path.home() is evaluated once at function definition time, not at each call. This can cause issues in testing or if the home directory changes during runtime.

Proposed fix
+_DEFAULT_WEIGHTS_PATH = Path.home() / Path(".cache/rai")
+
+
 def create_service_wrapper(
     service_class: Type,
     ros2_name: str,
     model_name: str,
     service_name: str,
-    weights_root_path: str | Path = Path.home() / Path(".cache/rai"),
+    weights_root_path: str | Path | None = None,
 ) -> tuple[ROS2Connector, object]:
     """Create a service instance with ROS2 parameters configured.
     ...
     """
+    if weights_root_path is None:
+        weights_root_path = _DEFAULT_WEIGHTS_PATH
+
     ros2_connector = ROS2Connector(ros2_name, executor_type="single_threaded")
tests/rai_perception/test_mocks.py-21-68 (1)

21-68: Silence unused mock parameters to keep ruff clean.

These mock signatures are correct, but ruff’s ARG002 will flag unused arguments. Prefix them with _ to keep the interface while avoiding lint failures.

🧹 Suggested cleanup
-    def get_boxes(self, image_msg, classes, box_threshold, text_threshold):
+    def get_boxes(self, _image_msg, classes, _box_threshold, _text_threshold):
@@
-    def get_segmentation(self, image, boxes):
+    def get_segmentation(self, _image, _boxes):
@@
-    def get_boxes(self, image_msg, classes, box_threshold, text_threshold):
+    def get_boxes(self, _image_msg, _classes, _box_threshold, _text_threshold):
@@
-    def get_segmentation(self, image, boxes):
+    def get_segmentation(self, _image, _boxes):
tests/rai_perception/conftest.py-88-125 (1)

88-125: Mock node doesn't persist declared parameters.

declare_parameter is a MagicMock, so any code that declares and then reads parameters will raise ParameterNotDeclaredException. The _parameters dict is properly used by get_parameter, has_parameter, and set_parameters, but declare_parameter needs to actually store parameters to keep the mock aligned with ROS2 behavior.

🧩 Suggested mock fix
+    def declare_parameter(name, value=None):
+        from rclpy.parameter import Parameter
+        param = Parameter(name, value=value)
+        _parameters[name] = param
+        return param
@@
-    mock_node.declare_parameter = MagicMock()
+    mock_node.declare_parameter = declare_parameter
src/rai_extensions/rai_perception/rai_perception/components/visualization_utils.py-161-185 (1)

161-185: Docstring contradicts implementation: function documents Raises: RuntimeError but actually catches exceptions and returns original points.

The docstring states the function raises RuntimeError on transform failure, but the implementation catches all exceptions and returns the original points instead. This mismatch can confuse callers who might try to handle the exception.

🐛 Proposed fix: Update docstring to match behavior
     Returns:
         List of transformed 3D points (Nx3 arrays) in target frame.
         Returns original points if transform fails.
-
-    Raises:
-        RuntimeError: If transform lookup fails
     """
examples/manipulation-demo-v1.py-1-1 (1)

1-1: Minor: Copyright year inconsistency.

This file uses 2024 while other new files in this PR use 2025. Consider updating for consistency.

docs/api_design_considerations.md-241-242 (1)

241-242: Minor grammar issue: hyphenation.

"Multi Agent Systems" should be hyphenated as "Multi-Agent Systems" for grammatical correctness.

📝 Suggested fix
-7. **Rachwał et al. (2025)** - "RAI: Flexible Agent Framework for Embodied AI" - https://arxiv.org/abs/2505.07532  
-   Introduces the RAI framework for creating embodied Multi Agent Systems for robotics, with integration for ROS 2, Large Language Models, and simulations. Describes the framework's architecture, tools, and mechanisms for agent embodiment.
+7. **Rachwał et al. (2025)** - "RAI: Flexible Agent Framework for Embodied AI" - https://arxiv.org/abs/2505.07532  
+   Introduces the RAI framework for creating embodied Multi-Agent Systems for robotics, with integration for ROS 2, Large Language Models, and simulations. Describes the framework's architecture, tools, and mechanisms for agent embodiment.
docs/api_design_considerations.md-78-78 (1)

78-78: Typographical error: double period.

The sentence ends with mechanisms.". which has a period both inside and outside the quote.

📝 Suggested fix
-The [RAI framework paper](https://arxiv.org/abs/2505.07532}) (Rachwał et al., 2025) states Tools are "compatible with langchain, enabling seamless integration with tool-calling-enabled LLMs" while also being "used by Agents utilizing other decision-making mechanisms.". Based on this paper, RAI serves four distinct roles at different architectural levels:
+The [RAI framework paper](https://arxiv.org/abs/2505.07532) (Rachwał et al., 2025) states Tools are "compatible with langchain, enabling seamless integration with tool-calling-enabled LLMs" while also being "used by Agents utilizing other decision-making mechanisms." Based on this paper, RAI serves four distinct roles at different architectural levels:

Note: Also removes the extra } in the URL.

src/rai_extensions/rai_perception/follow-ups.md-87-88 (1)

87-88: Use heading syntax instead of italic emphasis.

markdownlint flags emphasis used as a heading.

✅ Replace emphasis with heading
-_Proposed Solution: Service-Level Abstraction_
+#### Proposed Solution: Service-Level Abstraction
src/rai_extensions/rai_perception/follow-ups.md-5-12 (1)

5-12: Fix list indentation to satisfy markdownlint.

The TOC list uses extra indentation; reduce to standard two-space nesting.

✅ Lint-friendly list indentation
--   [Breaking Changes](`#breaking-changes`)
-    -   [Agent Service Renaming and Service Name Changes](`#agent-service-renaming-and-service-name-changes`)
-    -   [Agents and Vision Markup Deprecation](`#agents-and-vision-markup-deprecation`)
-    -   [Related Package Updates](`#related-package-updates`)
--   [Post-PR Follow-ups](`#post-pr-follow-ups`)
-    -   [Generic Detection Tools Abstraction](`#generic-detection-tools-abstraction`)
-    -   [Progressive Evaluation: GetDistanceToObjectsTool](`#progressive-evaluation-getdistancetoobjectstool`)
-    -   [Service Name Updates in rai_bench](`#service-name-updates-in-rai_bench`)
+- [Breaking Changes](`#breaking-changes`)
+  - [Agent Service Renaming and Service Name Changes](`#agent-service-renaming-and-service-name-changes`)
+  - [Agents and Vision Markup Deprecation](`#agents-and-vision-markup-deprecation`)
+  - [Related Package Updates](`#related-package-updates`)
+- [Post-PR Follow-ups](`#post-pr-follow-ups`)
+  - [Generic Detection Tools Abstraction](`#generic-detection-tools-abstraction`)
+  - [Progressive Evaluation: GetDistanceToObjectsTool](`#progressive-evaluation-getdistancetoobjectstool`)
+  - [Service Name Updates in rai_bench](`#service-name-updates-in-rai_bench`)
tests/rai_perception/agents/test_run_perception_agents.py-12-13 (1)

12-13: License header appears truncated.

Line 12 reads "See the specific language governing" but should be "See the License for the specific language governing" to match the standard Apache 2.0 license text used in other files.

📝 Suggested fix
-# See the specific language governing permissions and
+# See the License for the specific language governing permissions and
src/rai_extensions/rai_perception/rai_perception/services/base_vision_service.py-36-62 (1)

36-62: Missing validation for WEIGHTS_URL before download attempt.

The class validates that WEIGHTS_FILENAME is set (line 49-50), but WEIGHTS_URL defaults to an empty string and isn't validated. If a subclass forgets to set WEIGHTS_URL and weights don't exist, download_weights will be called with an empty URL, causing a confusing wget failure.

Proposed fix to validate WEIGHTS_URL
     def __init__(
         self,
         weights_root_path: str | Path = DEFAULT_WEIGHTS_ROOT_PATH,
         ros2_name: str = "",
         ros2_connector: Optional[ROS2Connector] = None,
     ):
         # TODO: After agents are deprecated, make ros2_connector a required parameter
         # (remove Optional and default None). Services should always receive a connector.
         if not self.WEIGHTS_FILENAME:
             raise ValueError("WEIGHTS_FILENAME is not set")
+        if not self.WEIGHTS_URL:
+            raise ValueError("WEIGHTS_URL is not set")
         self.weights_root_path = Path(weights_root_path)
docs/extensions/rethinking_usability.md-216-216 (1)

216-216: Fix typo: "avaialble" → "available".

-Three tools are avaialble for RAI agents:`GetDetectionTool`, `GetObjectGrippingPointsTool` and `GetDistanceToObjectsTool`.
+Three tools are available for RAI agents: `GetDetectionTool`, `GetObjectGrippingPointsTool` and `GetDistanceToObjectsTool`.
tests/rai_perception/services/test_run_perception_services.py-71-88 (1)

71-88: Silence Ruff ARG001 for unused tracking args.

*args/**kwargs are unused in these helpers, which Ruff flags. Rename to underscore-prefixed to avoid lint failures.

✅ Suggested tweak
-        def track_connector_init(*args, **kwargs):
+        def track_connector_init(*_args, **_kwargs):
             call_order.append("connector_init")
             return MagicMock()

-        def track_detection_init(*args, **kwargs):
+        def track_detection_init(*_args, **_kwargs):
             call_order.append("detection_init")
             mock_instance = MagicMock()
             mock_instance.run.side_effect = lambda: call_order.append("detection_run")
             return mock_instance

-        def track_segmentation_init(*args, **kwargs):
+        def track_segmentation_init(*_args, **_kwargs):
             call_order.append("segmentation_init")
             mock_instance = MagicMock()
             mock_instance.run.side_effect = lambda: call_order.append(
                 "segmentation_run"
             )
             return mock_instance
src/rai_core/rai/communication/ros2/parameters.py-22-24 (1)

22-24: Resolve or track the TODO before release.

If this reconsideration is still open, please capture it in an issue/ADR so it doesn’t get lost.

Do you want me to draft an issue/ADR template for this decision?

src/rai_extensions/rai_perception/rai_perception/vision_markup/segmenter.py-40-55 (1)

40-55: Align docstring with actual config_path handling.

The docstring says config_path is ignored, but it’s forwarded to super().__init__. Please clarify to avoid confusing callers.

✏️ Suggested docstring tweak
-            config_path: Ignored (kept for API compatibility, SAM2 uses Hydra config module)
+            config_path: Forwarded to algorithms.GDSegmenter for API compatibility
tests/rai_perception/services/test_weights.py-42-43 (1)

42-43: Patch target should be where subprocess.run is used.

The patch should target rai_perception.services.weights.subprocess.run instead of subprocess.run to ensure the mock is applied where the function is imported/used, not where it's defined.

Proposed fix
-        with patch("subprocess.run", side_effect=mock_wget):
+        with patch("rai_perception.services.weights.subprocess.run", side_effect=mock_wget):

Apply the same fix to all other patch("subprocess.run", ...) calls in this file (lines 53-54, 73, 120).

examples/manipulation-demo.py-41-54 (1)

41-54: Missing rclpy.shutdown() cleanup could cause resource leaks.

rclpy.init() is called at line 53, but there's no corresponding rclpy.shutdown() when the agent is done or on error. This can lead to resource leaks, especially if create_agent() is called multiple times or if the program exits abnormally.

Suggested approach

Consider adding cleanup in the main() function or returning resources that need cleanup:

 def main():
-    agent = create_agent()
-    messages: List[BaseMessage] = []
-
-    while True:
-        prompt = input("Enter a prompt: ")
-        messages.append(HumanMessage(content=prompt))
-        output = agent.invoke({"messages": messages})
-        output["messages"][-1].pretty_print()
+    try:
+        agent = create_agent()
+        messages: List[BaseMessage] = []
+
+        while True:
+            prompt = input("Enter a prompt: ")
+            messages.append(HumanMessage(content=prompt))
+            output = agent.invoke({"messages": messages})
+            output["messages"][-1].pretty_print()
+    except KeyboardInterrupt:
+        pass
+    finally:
+        rclpy.shutdown()
src/rai_extensions/rai_perception/rai_perception/services/detection_service.py-75-77 (1)

75-77: Guard against empty class names in requests.

split(",") can yield empty strings from trailing, leading, or consecutive commas (e.g., "dinosaur," or "dinosaur,,dragon"). The current code strips whitespace but keeps empty strings, which propagate to the class dictionary and model. Filter empty strings to ensure only valid class names are processed.

Proposed fix
-            class_array = request.classes.split(",")
-            class_array = [class_name.strip() for class_name in class_array]
+            class_array = [c.strip() for c in request.classes.split(",") if c.strip()]
src/rai_extensions/rai_perception/rai_perception/services/weights.py-112-118 (1)

112-118: Make remove_weights idempotent.

os.remove raises if the file is already gone, which can short‑circuit recovery paths. Handle FileNotFoundError.

Proposed fix
 def remove_weights(weights_path: Path):
     """Remove weights file.
@@
     Args:
         weights_path: Path to weights file to remove
     """
-    os.remove(weights_path)
+    try:
+        os.remove(weights_path)
+    except FileNotFoundError:
+        pass
src/rai_extensions/rai_perception/rai_perception/__init__.py-21-36 (1)

21-36: Drop unused E402 noqa directives.
Ruff flags these as unused; removing them keeps lint clean.

🧹 Proposed cleanup
-from .agents import GroundedSamAgent, GroundingDinoAgent  # noqa: E402
-from .algorithms.point_cloud import depth_to_point_cloud  # noqa: E402
-from .components.gripping_points import (  # noqa: E402
+from .agents import GroundedSamAgent, GroundingDinoAgent
+from .algorithms.point_cloud import depth_to_point_cloud
+from .components.gripping_points import (
@@
-from .components.topic_utils import (  # noqa: E402
+from .components.topic_utils import (
@@
-from .tools import GetDetectionTool, GetDistanceToObjectsTool  # noqa: E402
-from .tools.gripping_points_tools import (  # noqa: E402
+from .tools import GetDetectionTool, GetDistanceToObjectsTool
+from .tools.gripping_points_tools import (
src/rai_extensions/rai_perception/rai_perception/components/service_utils.py-97-112 (1)

97-112: Explicitly mark unused timeout_sec to satisfy lint.
Ruff flags timeout_sec as unused (ARG001); keep the signature but mark it intentionally unused.

🧹 Proposed lint fix
 def create_service_client(
     connector: ROS2Connector,
     service_type: Type,
     service_name: str,
     timeout_sec: float = 1.0,
     max_wait_time: float = 0.0,
 ) -> object:
@@
-    try:
+    _ = timeout_sec  # kept for backward-compatible signature
+    try:
         wait_for_ros2_services(connector, [service_name], timeout=max_wait_time)
src/rai_extensions/rai_perception/rai_perception/components/perception_presets.py-94-110 (1)

94-110: Fix return type hint to use typing.Any instead of built-in any.
Line 94 uses lowercase any, which type checkers will not recognize as a valid type. The correct annotation is Dict[str, Any]. Note that Any is already imported on line 16.

🛠️ Proposed fix
-def get_preset(preset_name: PresetName) -> Dict[str, any]:
+def get_preset(preset_name: PresetName) -> Dict[str, Any]:
🧹 Nitpick comments (41)
src/rai_extensions/rai_perception/rai_perception/configs/detection_publisher.yaml (1)

9-10: Consider using >- or a plain scalar to avoid trailing newline.

The YAML literal block scalar (|) preserves trailing newlines, which may require trimming in the parsing code. Since the content is a single logical line, a folded/stripped scalar or plain quoted string would be cleaner:

♻️ Suggested alternatives

Option 1: Plain scalar (simplest)

-    detection_classes: |
-      person, cup, bottle, box, bag, chair, table, shelf, door, window, couch, sofa, bed, stove
+    detection_classes: "person, cup, bottle, box, bag, chair, table, shelf, door, window, couch, sofa, bed, stove"

Option 2: Block scalar with strip indicator

-    detection_classes: |
+    detection_classes: |-
       person, cup, bottle, box, bag, chair, table, shelf, door, window, couch, sofa, bed, stove
src/rai_semap/rai_semap/ros2/config/detection_publisher.yaml (1)

5-5: Service endpoint updated; parameter naming reflects transitional state.

The service endpoint change from /grounding_dino_classify to /detection is correctly configured and the /detection service is implemented and available throughout the codebase. The parameter dino_service now points to this generic endpoint, creating a naming mismatch that could be addressed by renaming to detection_service for clarity. However, this would require coordinated updates across multiple configuration files and the detection_publisher component. Consider this change as part of a future refactoring effort to complete the API transition.

src/rai_core/rai/tools/ros2/manipulation/custom.py (1)

280-280: Good deprecation notice with clear migration path.

The decorator properly marks GetObjectPositionsTool as deprecated and directs users to the new GetObjectGrippingPointsTool. Consider adding a version parameter to track when the deprecation was introduced, which helps with release notes and migration timelines.

♻️ Optional: Add version tracking to the deprecation
-@deprecated("Use GetObjectGrippingPointsTool from rai_perception instead")
+@deprecated(version="0.x.0", reason="Use GetObjectGrippingPointsTool from rai_perception instead")

Replace 0.x.0 with the current package version.

src/rai_extensions/rai_perception/pyproject.toml (1)

13-18: Python version constraint mismatch with rai_core dependency.

rai_perception declares python = "^3.8" but depends on rai_core which requires python = "^3.10, <3.13". This could cause confusing installation failures on Python 3.8 or 3.9.

Consider aligning the Python constraint:

Proposed fix
 [tool.poetry.dependencies]
-python = "^3.8"
+python = "^3.10, <3.13"
tests/conftest.py (1)

21-30: Consider documenting available strategy values in the help text.

The implementation is correct. For better discoverability, consider listing the valid strategy options in the help text.

Proposed improvement
 def pytest_addoption(parser):
     parser.addoption(
-        "--strategy", action="store", default="centroid", help="Gripping point strategy"
+        "--strategy",
+        action="store",
+        default="centroid",
+        help="Gripping point strategy (e.g., centroid, top_plane, biggest_plane)",
     )
src/rai_extensions/rai_perception/rai_perception/components/perception_utils.py (2)

32-45: Consider adding bounds validation for camera intrinsics extraction.

The function assumes camera_info.k has at least 6 elements (indices 0, 2, 4, 5). If a malformed CameraInfo message is passed, this will raise an IndexError. Consider adding validation or documenting the precondition.

Optional defensive check
 def get_camera_intrinsics(camera_info: CameraInfo) -> Tuple[float, float, float, float]:
     """Extract camera intrinsics from CameraInfo message.
 
     Args:
         camera_info: Camera info message with intrinsics
 
     Returns:
         Tuple of (fx, fy, cx, cy) camera intrinsics
+
+    Raises:
+        ValueError: If camera_info.k does not contain valid intrinsics
     """
+    if len(camera_info.k) < 6:
+        raise ValueError("CameraInfo.k must contain at least 6 elements for intrinsics")
     fx = float(camera_info.k[0])
     fy = float(camera_info.k[4])
     cx = float(camera_info.k[2])
     cy = float(camera_info.k[5])
     return fx, fy, cx, cy

113-116: Inconsistent depth conversion - use the new helper.

This function has inline depth conversion logic that duplicates convert_depth_to_meters. For consistency and maintainability, consider using the helper here as well.

Proposed fix
         # Convert depth to meters (assuming depth image is in mm, adjust if needed)
         # Common depth encodings: 16UC1 (mm), 32FC1 (m)
-        if depth_image.encoding in ["16UC1", "mono16"]:
-            depth_value = depth_value / 1000.0  # mm to meters
+        # Create a single-element array to use the helper, then extract scalar
+        depth_arr = np.array([depth_value])
+        depth_value = float(convert_depth_to_meters(depth_arr, depth_image)[0])

Alternatively, extract the encoding check into a simpler helper or refactor convert_depth_to_meters to also accept scalar values.

tests/rai_perception/components/test_gripping_points_integration.py (1)

114-182: Avoid indefinite waits in the manual test.

wait_for_ros2_services/topics default to an infinite wait. A bounded timeout makes the manual test fail fast when services are missing.

⏱️ Add an explicit timeout
-def run_gripping_points_test(
+def run_gripping_points_test(
     test_object: str,
     grasp: str,
     topics: dict[str, str],
     frames: dict[str, str],
     debug_enabled: bool = False,
+    timeout_sec: float = 30.0,
 ) -> None:
@@
-        wait_for_ros2_services(connector, REQUIRED_SERVICES)
-        wait_for_ros2_topics(connector, list(topics.values()))
+        wait_for_ros2_services(connector, REQUIRED_SERVICES, timeout=timeout_sec)
+        wait_for_ros2_topics(connector, list(topics.values()), timeout=timeout_sec)
@@
 def test_gripping_points_manipulation_demo(grasp) -> None:
@@
     run_gripping_points_test(
         test_object="cube",
         grasp=grasp,
         topics=MANIPULATION_DEMO_TOPICS,
         frames=MANIPULATION_DEMO_FRAMES,
-        debug_enabled=True,
+        debug_enabled=True,
+        timeout_sec=30.0,
     )
tests/rai_perception/agents/test_base_vision_agent.py (1)

92-103: Consistent ROS2 setup/teardown pattern applied across test classes.

The setup_method/teardown_method pattern for ROS2 context management is consistent with other test files (test_base_segmenter.py, test_base_boxer.py).

The try-except-pass in teardown is intentional for robustness during test cleanup. However, per static analysis hints, consider logging exceptions rather than silently swallowing them, which could help diagnose intermittent test failures.

♻️ Optional: Log exceptions in teardown for better diagnostics
     def teardown_method(self):
         """Clean up ROS2 context after each test."""
         try:
             if rclpy.ok():
-                rclpy.shutdown()
+                time.sleep(0.1)  # Allow pending callbacks to complete
+                rclpy.shutdown()
         except Exception:
-            pass
+            pass  # Intentionally swallowed - cleanup should not fail tests

Or add logging:

except Exception as e:
    import logging
    logging.debug(f"ROS2 shutdown during teardown: {e}")
src/rai_extensions/rai_perception/rai_perception/components/visualization_utils.py (2)

32-60: Code duplication: _quaternion_to_rotation_matrix is duplicated from gripping_points.py.

This function is an exact duplicate of the implementation in src/rai_extensions/rai_perception/rai_perception/components/gripping_points.py (lines 340-360). Consider extracting it to a shared math utilities module to maintain DRY principles.

♻️ Suggested refactor

Create a shared module like math_utils.py:

# In rai_perception/components/math_utils.py
def quaternion_to_rotation_matrix(qx: float, qy: float, qz: float, qw: float) -> np.ndarray:
    ...

Then import from both gripping_points.py and visualization_utils.py.


181-185: Silent failure on transform errors may hide issues in downstream processing.

Catching a broad Exception and returning original (untransformed) points could lead to incorrect visualizations without any indication to the caller that the transform failed. The error is logged, but callers have no programmatic way to detect the failure.

Consider either:

  1. Re-raising after logging (if callers should handle failures)
  2. Returning a tuple (points, success: bool) to indicate transform status
  3. Keeping current behavior if silent fallback is intentional (document this clearly)
examples/manipulation-demo-v1.py (1)

25-33: Consider adding graceful exit handling and basic error handling.

The infinite loop with input() will raise KeyboardInterrupt on Ctrl+C without cleanup. For a demo script, consider wrapping with try-except for cleaner exit:

♻️ Suggested improvement
 def main():
     agent, _ = create_agent()
     messages: List[BaseMessage] = []

-    while True:
-        prompt = input("Enter a prompt: ")
-        messages.append(HumanMessage(content=prompt))
-        output = agent.invoke({"messages": messages})
-        output["messages"][-1].pretty_print()
+    try:
+        while True:
+            prompt = input("Enter a prompt: ")
+            if not prompt.strip():
+                continue
+            messages.append(HumanMessage(content=prompt))
+            output = agent.invoke({"messages": messages})
+            output["messages"][-1].pretty_print()
+    except KeyboardInterrupt:
+        print("\nExiting...")
src/rai_extensions/rai_perception/rai_perception/scripts/run_perception_services.py (1)

23-45: Consider adding cleanup handling for initialization failures.

If DetectionService or SegmentationService initialization fails (e.g., model loading error), rclpy.shutdown() won't be called, potentially leaving ROS2 in an unclean state.

♻️ Suggested improvement
 def main():
     rclpy.init()
-
-    # Create ROS2 connectors for each service
-    # TODO(juliaj): Re-evaluate executor_type choice (single_threaded vs multi_threaded)
-    # Current: single_threaded for consistency with BaseVisionService pattern
-    # Consider: multi_threaded if concurrent request handling is needed
-    detection_connector = ROS2Connector(
-        "detection_service", executor_type="single_threaded"
-    )
-    segmentation_connector = ROS2Connector(
-        "segmentation_service", executor_type="single_threaded"
-    )
-
-    # Services read model_name from ROS2 params (defaults: "grounding_dino", "grounded_sam")
-    detection_service = DetectionService(ros2_connector=detection_connector)
-    segmentation_service = SegmentationService(ros2_connector=segmentation_connector)
-
-    detection_service.run()
-    segmentation_service.run()
-
-    wait_for_shutdown([detection_service, segmentation_service])
-    rclpy.shutdown()
+    try:
+        # Create ROS2 connectors for each service
+        # TODO(juliaj): Re-evaluate executor_type choice (single_threaded vs multi_threaded)
+        # Current: single_threaded for consistency with BaseVisionService pattern
+        # Consider: multi_threaded if concurrent request handling is needed
+        detection_connector = ROS2Connector(
+            "detection_service", executor_type="single_threaded"
+        )
+        segmentation_connector = ROS2Connector(
+            "segmentation_service", executor_type="single_threaded"
+        )
+
+        # Services read model_name from ROS2 params (defaults: "grounding_dino", "grounded_sam")
+        detection_service = DetectionService(ros2_connector=detection_connector)
+        segmentation_service = SegmentationService(ros2_connector=segmentation_connector)
+
+        detection_service.run()
+        segmentation_service.run()
+
+        wait_for_shutdown([detection_service, segmentation_service])
+    finally:
+        rclpy.shutdown()
tests/rai_perception/algorithms/test_point_cloud.py (1)

65-83: Make the coordinate test order-agnostic.

This test currently assumes a specific point ordering; if the implementation changes its flattening order, it will become brittle. Consider comparing sorted arrays instead.

♻️ Proposed refactor (order-agnostic comparison)
-        # Verify coordinates for each pixel
-        for i, point in enumerate(points):
-            u, v = i % 3, i // 3
-            expected_x = (u - cx) * depth_value / fx
-            expected_y = (v - cy) * depth_value / fy
-
-            np.testing.assert_almost_equal(point[0], expected_x, decimal=5)
-            np.testing.assert_almost_equal(point[1], expected_y, decimal=5)
-            np.testing.assert_almost_equal(point[2], depth_value, decimal=5)
+        expected = []
+        for v in range(3):
+            for u in range(3):
+                expected.append(
+                    [
+                        (u - cx) * depth_value / fx,
+                        (v - cy) * depth_value / fy,
+                        depth_value,
+                    ]
+                )
+        expected = np.array(expected, dtype=np.float32)
+
+        # Order-agnostic comparison
+        points_sorted = points[np.lexsort((points[:, 2], points[:, 1], points[:, 0]))]
+        expected_sorted = expected[
+            np.lexsort((expected[:, 2], expected[:, 1], expected[:, 0]))
+        ]
+        np.testing.assert_allclose(points_sorted, expected_sorted, rtol=1e-5, atol=1e-5)
src/rai_extensions/rai_perception/rai_perception/models/detection.py (1)

57-74: Consider aligning return type with segmentation module for API consistency.

The segmentation module's get_model returns Tuple[Type, str | None] while this returns Tuple[Type, str]. If future detection models might not require config paths, consider using the nullable type for consistency across model registries.

tests/rai_perception/tools/test_pcl_detection_tools.py (3)

19-24: Simplify the ROS2 import check.

The noqa directives are flagged as unused by static analysis. The import check can be simplified:

♻️ Suggested simplification
 try:
-    import rclpy  # noqa: F401
-
-    _ = rclpy  # noqa: F841
+    import rclpy
 except ImportError:
     pytest.skip("ROS2 is not installed", allow_module_level=True)

98-138: Consider seeding random number generator for test reproducibility.

The test uses np.random.normal without a seed, which could theoretically cause flaky tests. While the assertions appear tolerant enough, seeding ensures deterministic behavior:

♻️ Optional fix for reproducibility
 def test_point_cloud_filter():
     """Test point cloud filtering strategies."""
+    np.random.seed(42)  # Ensure reproducible test data
     # Create test data with noise points
     main_cluster = np.random.normal([0, 0, 0], 0.1, (50, 3)).astype(np.float32)

170-172: Prefix unused parameter with underscore to indicate intentional non-use.

The obj_name parameter is needed to match the mocked method's signature but isn't used. Prefixing with underscore silences the linter and documents the intent:

♻️ Suggested fix
-    def slow_operation(obj_name):
+    def slow_operation(_obj_name):
         time.sleep(2.0)  # Longer than timeout
         return []
tests/rai_perception/components/test_topic_utils.py (1)

17-22: Simplify the ROS2 import check (same as other test file).

♻️ Suggested simplification
 try:
-    import rclpy  # noqa: F401
-
-    _ = rclpy  # noqa: F841
+    import rclpy
 except ImportError:
     pytest.skip("ROS2 is not installed", allow_module_level=True)
src/rai_extensions/rai_perception/rai_perception/services/base_vision_service.py (1)

93-97: Consider extracting the deferred import to reduce duplication.

The get_param_value import is duplicated in both _initialize_model_from_registry (line 93) and _get_service_name (line 119). While deferred imports help avoid circular dependencies, the duplication could be consolidated.

Optional: Extract to a module-level lazy import helper or import once at method entry
+    def _get_param_value(self, name: str, default=None):
+        """Get ROS2 parameter value with lazy import."""
+        from rai.communication.ros2 import get_param_value
+        return get_param_value(self.ros2_connector.node, name, default=default)
+
     def _initialize_model_from_registry(
         self, get_model_func, default_model_name: str, model_type_name: str
     ):
         ...
-        from rai.communication.ros2 import get_param_value
-
-        model_name = get_param_value(
-            self.ros2_connector.node, "model_name", default=default_model_name
-        )
+        model_name = self._get_param_value("model_name", default=default_model_name)

Also applies to: 119-123

docs/extensions/rethinking_usability.md (1)

251-260: Add language specifier to fenced code block.

The code block describing the workflow lacks a language specifier. Adding one improves rendering in documentation viewers.

-```
+```text
 1. list_available_models() → ["grounding_dino", "yolo", ...]
src/rai_core/rai/tools/timeout.py (2)

49-49: Use explicit str | None type annotation instead of implicit Optional.

Per PEP 484, timeout_message: str = None implicitly creates an Optional type which is discouraged. Use explicit str | None for clarity.

Proposed fix
-def timeout(seconds: float, timeout_message: str = None) -> Callable[[F], F]:
+def timeout(seconds: float, timeout_message: str | None = None) -> Callable[[F], F]:
-def timeout_method(seconds: float, timeout_message: str = None) -> Callable[[F], F]:
+def timeout_method(seconds: float, timeout_message: str | None = None) -> Callable[[F], F]:

Also applies to: 103-103


91-96: Chain exceptions using raise ... from for better tracebacks.

When raising RaiTimeoutError in the except clause, chain it with from err (or from None to suppress the original) to provide clearer exception context and satisfy B904.

Proposed fix for both decorators
                 except concurrent.futures.TimeoutError:
                     message = (
                         timeout_message
                         or f"Function '{func.__name__}' timed out after {seconds} seconds"
                     )
-                    raise RaiTimeoutError(message)
+                    raise RaiTimeoutError(message) from None
                 except concurrent.futures.TimeoutError:
                     message = (
                         timeout_message
                         or f"Method '{func.__name__}' of {self.__class__.__name__} timed out after {seconds} seconds"
                     )
-                    raise RaiTimeoutError(message)
+                    raise RaiTimeoutError(message) from None

Also applies to: 150-155

tests/tools/test_timeout.py (1)

1-61: Add test coverage for the standalone timeout decorator.

The tests thoroughly cover timeout_method behavior, but the standalone timeout decorator (for regular functions) lacks test coverage. Both decorators are implemented in the module but serve different purposes: timeout for functions and timeout_method for instance methods, with different default error messages.

Consider adding a test for the standalone timeout decorator:

Suggested test code
from rai.tools.timeout import RaiTimeoutError, timeout, timeout_method

`@timeout`(0.5)
def slow_function():
    time.sleep(1.0)
    return "should not reach here"

def test_timeout_decorator_raises_timeout_error():
    with pytest.raises(RaiTimeoutError) as exc_info:
        slow_function()
    assert "slow_function" in str(exc_info.value)
    assert "0.5 seconds" in str(exc_info.value)
tests/rai_perception/tools/test_segmentation_tools.py (1)

223-326: Consider removing duplicate parameter setup in the grab‑point test.

conversion_ratio is set twice, which is redundant and makes the test longer than needed.

♻️ Optional cleanup
-        # Set ROS2 parameters
-        mock_connector.node.set_parameters(
-            [
-                Parameter(
-                    "conversion_ratio", rclpy.parameter.Parameter.Type.DOUBLE, 0.001
-                ),
-            ]
-        )
src/rai_extensions/rai_perception/rai_perception/algorithms/point_cloud.py (1)

40-48: Guard against invalid intrinsics and non‑finite depth values.

If fx/fy are zero or depth contains non‑finite values, the output can include inf/nan points that pass the z > 0 filter. Consider validating intrinsics and filtering finite points.

🛠️ Suggested hardening
 def depth_to_point_cloud(
     depth_image: NDArray[np.float32], fx: float, fy: float, cx: float, cy: float
 ) -> NDArray[np.float32]:
@@
-    height, width = depth_image.shape
+    if fx == 0 or fy == 0:
+        raise ValueError("fx and fy must be non-zero")
+    height, width = depth_image.shape
@@
-    points = np.stack((x, y, z), axis=-1).reshape(-1, 3)
-    points = points[points[:, 2] > 0]
+    points = np.stack((x, y, z), axis=-1).reshape(-1, 3)
+    valid = (points[:, 2] > 0) & np.isfinite(points).all(axis=1)
+    points = points[valid]
     return points.astype(np.float32, copy=False)
tests/rai_perception/algorithms/test_base_boxer.py (1)

43-55: Consider more robust ROS2 teardown.

The time.sleep(0.1) in teardown is a workaround for thread cleanup timing. While functional, this could be flaky in CI environments with varying loads.

The current implementation works but the sleep-based approach is inherently racy. Consider documenting why the sleep is needed or investigating if there's a more deterministic cleanup method.

src/rai_extensions/rai_perception/rai_perception/services/segmentation_service.py (1)

115-128: Consider tracking partial mask failures.

When individual mask processing fails (line 122-126), the error is logged but the response still returns successfully processed masks. This is generally good for resilience, but callers have no way to know that some masks failed.

If partial failures need to be surfaced, consider either:

  1. Adding a status field to the response message.
  2. Logging a summary at the end (e.g., "Processed X/Y masks successfully").

Current behavior is acceptable if callers can tolerate partial results silently.

src/rai_extensions/rai_perception/rai_perception/tools/gdino_tools.py (3)

226-234: Avoid mutable class-level defaults for required_services.
Use ClassVar (or tuples) so this metadata isn’t treated as a mutable instance field. Add ClassVar to the typing imports.

♻️ Proposed fix
-    required_services: list[dict[str, str]] = [
+    required_services: ClassVar[list[dict[str, str]]] = [

284-304: Also mark required_services / pipeline_stages as ClassVar.
Same mutability concern here; these are metadata and shouldn’t be mutable instance fields.

♻️ Proposed fix
-    required_services: list[dict[str, str]] = [
+    required_services: ClassVar[list[dict[str, str]]] = [
@@
-    pipeline_stages: list[dict[str, str]] = [
+    pipeline_stages: ClassVar[list[dict[str, str]]] = [

405-461: Consider inheriting service info helpers from the base class.
get_service_info and check_service_dependencies duplicate the base implementation; removing them would reduce drift while keeping get_pipeline_info here.

src/rai_extensions/rai_perception/rai_perception/services/detection_service.py (1)

45-52: Prefer a shared constant for the default weights root.
Using a centralized constant avoids evaluating Path.home() in the signature and keeps defaults consistent.

♻️ Proposed fix
-        weights_root_path: str | Path = Path.home() / Path(".cache/rai"),
+        weights_root_path: str | Path = BaseVisionService.DEFAULT_WEIGHTS_ROOT_PATH,
src/rai_extensions/rai_perception/rai_perception/algorithms/boxer.py (2)

50-52: class_dict is unused.
Either remove it or use it to map class IDs to avoid confusing callers (update call sites accordingly).

♻️ Possible simplification
-    def to_detection_msg(
-        self, class_dict: Dict[str, int], timestamp: Time
-    ) -> Detection2D:
+    def to_detection_msg(self, timestamp: Time) -> Detection2D:

56-65: Resolve the timestamp type ambiguity.
Consider widening the type hint (e.g., a union) or normalizing at call sites so this TODO doesn’t linger.

Want me to draft the signature update and call‑site fixes?

src/rai_extensions/rai_perception/rai_perception/services/weights.py (1)

64-109: Add a timeout to avoid hanging downloads.

A stalled wget can block startup indefinitely; consider a subprocess timeout (and optionally retries) so failures surface promptly. Please verify this aligns with the runtime image’s wget availability and timeout expectations.

Proposed tweak
         subprocess.run(
             [
                 "wget",
                 weights_url,
                 "-O",
                 str(weights_path),
                 "--progress=dot:giga",
             ],
             check=True,
             capture_output=True,
             text=True,
+            timeout=600,
         )
src/rai_extensions/rai_perception/rai_perception/algorithms/segmenter.py (1)

43-78: Guard Hydra global initialization.

GlobalHydra.instance().clear() resets global state for the whole process; if other components rely on Hydra, this can cause interference. Consider initializing only when needed (or isolating this init) and verify behavior against the Hydra version in use.

tests/rai_perception/algorithms/test_base_segmenter.py (1)

45-57: Avoid swallowing teardown errors silently.

Catching broad exceptions and pass can hide flaky ROS2 teardown issues; consider logging for visibility. Please verify against your pytest/ROS2 noise tolerance.

Proposed tweak
 import time
+import logging
@@
     def teardown_method(self):
         """Clean up ROS2 context after each test."""
         try:
             if rclpy.ok():
                 time.sleep(0.1)
                 rclpy.shutdown()
-        except Exception:
-            pass
+        except Exception:
+            logging.getLogger(__name__).exception("rclpy shutdown failed")
src/rai_extensions/rai_perception/rai_perception/agents/grounding_dino.py (1)

24-64: Avoid Path.home() in default arguments.

Default-arg evaluation happens at import time; a None default (or module constant) is safer and easier to patch in tests.

♻️ Proposed refactor
 GDINO_NODE_NAME = "grounding_dino"
 GDINO_SERVICE_NAME = "grounding_dino_classify"
+DEFAULT_WEIGHTS_ROOT = Path.home() / Path(".cache/rai")

 class GroundingDinoAgent(BaseAgent):
@@
     def __init__(
         self,
-        weights_root_path: str | Path = Path.home() / Path(".cache/rai"),
+        weights_root_path: str | Path | None = None,
         ros2_name: str = GDINO_NODE_NAME,
     ):
@@
-        self.ros2_connector, self._service = create_service_wrapper(
+        if weights_root_path is None:
+            weights_root_path = DEFAULT_WEIGHTS_ROOT
+        self.ros2_connector, self._service = create_service_wrapper(
             DetectionService,
             ros2_name,
             "grounding_dino",
             GDINO_SERVICE_NAME,
             weights_root_path,
         )
src/rai_extensions/rai_perception/rai_perception/tools/segmentation_tools.py (1)

153-168: Avoid polling get_future_result in a loop.

Each call registers another callback and can spam warnings. A single wait with an explicit timeout and graceful fallback is safer.

♻️ Proposed refactor
-        resolved = None
-        while rclpy.ok():
-            resolved = get_future_result(future)
-            if resolved is not None:
-                break
+        resolved = get_future_result(future, timeout_sec=60.0)
+        if resolved is None:
+            logger.warning("Detection service timed out")
+            return "", {"segmentations": []}
@@
-        ret = []
-        while rclpy.ok():
-            resolved = get_future_result(future)
-            if resolved is not None:
-                for img_msg in resolved.masks:
-                    ret.append(convert_ros_img_to_base64(img_msg))
-                break
+        ret = []
+        resolved = get_future_result(future, timeout_sec=60.0)
+        if resolved is None:
+            logger.warning("Segmentation service timed out")
+            return "", {"segmentations": []}
+        for img_msg in resolved.masks:
+            ret.append(convert_ros_img_to_base64(img_msg))
src/rai_extensions/rai_perception/rai_perception/tools/gripping_points_tools.py (2)

93-109: Mark class-level constants as ClassVar to keep them out of Pydantic fields.

♻️ Proposed refactor
-from typing import Any, Callable, Dict, Optional, Type
+from typing import Any, Callable, ClassVar, Dict, Optional, Type
@@
-    pipeline_stages: list[dict[str, str]] = [
+    pipeline_stages: ClassVar[list[dict[str, str]]] = [
@@
-    required_services: list[dict[str, str]] = [
+    required_services: ClassVar[list[dict[str, str]]] = [

Also applies to: 112-125


167-205: Silence unused stage_time results.

♻️ Proposed refactor
-            pcl, stage_time = self._run_stage(
+            pcl, _stage_time = self._run_stage(
@@
-            pcl_filtered, stage_time = self._run_stage(
+            pcl_filtered, _stage_time = self._run_stage(
@@
-            gripping_points, stage_time = self._run_stage(
+            gripping_points, _stage_time = self._run_stage(

@Juliaj Juliaj requested a review from rachwalk January 22, 2026 01:48
@Juliaj
Copy link
Collaborator Author

Juliaj commented Jan 22, 2026

Regarding rai_sim and rai_bench, I don't think a lot of changes will be required. Mainly naming changes, switching to new tools or changing paths. For example in src/rai_bench/rai_bench/manipulation_o3de/predefined/configs/o3de_config.yaml

@jmatejcz thanks for help look into this. The current code changes are back-compat by default. Once we have this merged, I will look into migrate existing code to the new service names.

Copy link
Contributor

@jmatejcz jmatejcz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look into to code, just left some minor questions.

How can i test this whole new functionality? manipulation demo v2 is enough? And what to look for ?

@Juliaj
Copy link
Collaborator Author

Juliaj commented Jan 23, 2026

How can i test this whole new functionality? manipulation demo v2 is enough? And what to look for ?

@jmatejcz, thanks for looking into this. The testing I have done is listed in the PR description. A few additional things perhaps

  • Rerun the manipulation demo v2 and rai_bench sanity check test to make sure I didn't miss there.
  • Some work of this PR is for usability, you could evaluate it with a potential feature for the overall developer experience. For example, does the new structure make it easier to find the folder to make the change? I did some initial evaluation with this new use case.
  • A check on another internal demo would be nice as well if anyone has the bandwidth for backward compatibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants