Skip to content

Commit 1c48116

Browse files
authored
Implement republishing to address RealSense issue (#49)
* Implemented republisher * Remove non-aligned depth * Added default parameter value * Finalized republisher node * Updated launchfile to include republisher * Updated readme and CameraInfo topic
1 parent 5b44be6 commit 1c48116

File tree

7 files changed

+339
-24
lines changed

7 files changed

+339
-24
lines changed

ada_feeding_perception/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ For testing, be sure to unzip `test/food_img.zip`.
1818

1919
1. Build your workspace: `colcon build`
2020
2. Source your workspace: `source install/setup.bash`
21-
3. Run the perception nodes: `ros2 launch ada_feeding_perception ada_feeding_perception_launch.xml`
21+
3. Run the perception nodes: `ros2 launch ada_feeding_perception ada_feeding_perception.launch.py`
2222
4. Launch the motion nodes:
2323
1. Dummy nodes: `ros2 launch feeding_web_app_ros2_test feeding_web_app_dummy_nodes_launch.xml run_real_sense:=false run_face_detection:=false run_food_detection:=false`
2424
2. Real nodes: `ros2 launch ada_feeding ada_feeding_launch.xml`
@@ -60,6 +60,8 @@ See `config/test_segment_from_point.yaml` for other sample images and points. No
6060

6161
#### Option B: Interactively Testing the ROS Action Server
6262

63+
**NOTE**: On some machines Option B does not work (more generally, matplotlib interactive graphics don't work).
64+
6365
We have provided a ROS node that displays the live image stream from a topic, let's users click on it, and sends that point click to the SegmentFromPoint action server.
6466

6567
Run this script with: `ros2 launch ada_feeding_perception test_food_segmentation_launch.xml`
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#!/usr/bin/env python3
2+
"""
3+
This module defines the Republisher node, which takes in parameters for `from_topics`
4+
and `republished_namespace` and republishes the messages from the `from_topics` within
5+
the specified namespace.
6+
7+
This node is intended to address the issue where, when subscribing to images on a
8+
different machine to the one you are publishing them on, the rate slows down a lot if
9+
you have >3 subscriptions.
10+
"""
11+
12+
# Standard imports
13+
from typing import Any, Callable, List, Tuple
14+
15+
# Third-party imports
16+
from rcl_interfaces.msg import ParameterDescriptor, ParameterType
17+
import rclpy
18+
from rclpy.executors import MultiThreadedExecutor
19+
from rclpy.node import Node
20+
21+
# Local imports
22+
from ada_feeding.helpers import import_from_string
23+
24+
25+
class Republisher(Node):
26+
"""
27+
A node that takes in parameters for `from_topics` and `republished_namespace` and
28+
republishes the messages from the `from_topics` within the specified namespace.
29+
"""
30+
31+
def __init__(self) -> None:
32+
"""
33+
Initialize the node.
34+
"""
35+
super().__init__("republisher")
36+
37+
# Load the parameters
38+
(
39+
self.from_topics,
40+
self.topic_type_strs,
41+
self.republished_namespace,
42+
) = self.load_parameters()
43+
44+
# Import the topic types
45+
self.topic_types = []
46+
for topic_type_str in self.topic_type_strs:
47+
self.topic_types.append(import_from_string(topic_type_str))
48+
49+
# For each topic, create a callback, publisher, and subscriber
50+
num_topics = min(len(self.from_topics), len(self.topic_types))
51+
self.callbacks = []
52+
self.pubs = []
53+
self.subs = []
54+
for i in range(num_topics):
55+
# Create the callback
56+
callback = self.create_callback(i)
57+
self.callbacks.append(callback)
58+
59+
# Create the publisher
60+
to_topic = "/".join(
61+
[
62+
"",
63+
self.republished_namespace.lstrip("/"),
64+
self.from_topics[i].lstrip("/"),
65+
]
66+
)
67+
publisher = self.create_publisher(
68+
msg_type=self.topic_types[i],
69+
topic=to_topic,
70+
qos_profile=1, # TODO: we should get and mirror the QOS profile of the from_topic
71+
)
72+
self.pubs.append(publisher)
73+
74+
# Create the subscriber
75+
subscriber = self.create_subscription(
76+
msg_type=self.topic_types[i],
77+
topic=self.from_topics[i],
78+
callback=callback,
79+
qos_profile=1, # TODO: we should get and mirror the QOS profile of the from_topic
80+
)
81+
self.subs.append(subscriber)
82+
83+
def load_parameters(self) -> Tuple[List[str], List[str], List[str]]:
84+
"""
85+
Load the parameters for the republisher.
86+
87+
Returns
88+
-------
89+
from_topics : List[str]
90+
The topics to subscribe to.
91+
topic_types : List[str]
92+
The types of the topics to subscribe to in format, e.g., `std_msgs.msg.String`.
93+
republished_namespace : str
94+
The namespace to republish topics under.
95+
"""
96+
# Read the from topics
97+
from_topics = self.declare_parameter(
98+
"from_topics",
99+
descriptor=ParameterDescriptor(
100+
name="from_topics",
101+
type=ParameterType.PARAMETER_STRING_ARRAY,
102+
description="List of the topics to subscribe to.",
103+
read_only=True,
104+
),
105+
)
106+
107+
# Read the topic types
108+
topic_types = self.declare_parameter(
109+
"topic_types",
110+
descriptor=ParameterDescriptor(
111+
name="topic_types",
112+
type=ParameterType.PARAMETER_STRING_ARRAY,
113+
description=(
114+
"List of the types of the topics to subscribe to in format, "
115+
"e.g., `std_msgs.msg.String`."
116+
),
117+
read_only=True,
118+
),
119+
)
120+
121+
# Read the to topics
122+
republished_namespace = self.declare_parameter(
123+
"republished_namespace",
124+
"/local",
125+
descriptor=ParameterDescriptor(
126+
name="republished_namespace",
127+
type=ParameterType.PARAMETER_STRING,
128+
description="The namespace to republish topics under.",
129+
read_only=True,
130+
),
131+
)
132+
133+
# Replace unset parameters with empty list
134+
from_topics_retval = from_topics.value
135+
if from_topics_retval is None:
136+
from_topics_retval = []
137+
topic_types_retval = topic_types.value
138+
if topic_types_retval is None:
139+
topic_types_retval = []
140+
141+
return from_topics_retval, topic_types_retval, republished_namespace.value
142+
143+
def create_callback(self, i: int) -> Callable:
144+
"""
145+
Create the callback for the subscriber.
146+
147+
Parameters
148+
----------
149+
i : int
150+
The index of the callback.
151+
152+
Returns
153+
-------
154+
callback : Callable
155+
The callback for the subscriber.
156+
"""
157+
158+
def callback(msg: Any):
159+
"""
160+
The callback for the subscriber.
161+
162+
Parameters
163+
----------
164+
msg : Any
165+
The message from the subscriber.
166+
"""
167+
# self.get_logger().info(
168+
# f"Received message on topic {i} {self.from_topics[i]}"
169+
# )
170+
self.pubs[i].publish(msg)
171+
172+
return callback
173+
174+
175+
def main(args=None):
176+
"""
177+
Launch the ROS node and spin.
178+
"""
179+
rclpy.init(args=args)
180+
181+
republisher = Republisher()
182+
183+
# Use a MultiThreadedExecutor to enable processing goals concurrently
184+
executor = MultiThreadedExecutor()
185+
186+
rclpy.spin(republisher, executor=executor)
187+
188+
189+
if __name__ == "__main__":
190+
main()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# NOTE: You have to change this node name if you change the node name in the launchfile.
2+
republisher:
3+
ros__parameters:
4+
# The name of the topics to republish from
5+
from_topics:
6+
- /camera/color/image_raw/compressed
7+
- /camera/color/camera_info
8+
- /camera/aligned_depth_to_color/image_raw
9+
10+
# The types of topics to republish from
11+
topic_types:
12+
- sensor_msgs.msg.CompressedImage
13+
- sensor_msgs.msg.CameraInfo
14+
- sensor_msgs.msg.Image
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
"""
4+
Launch file for ada_feeding_perception
5+
"""
6+
7+
import os
8+
from ament_index_python.packages import get_package_share_directory
9+
from launch_ros.actions import Node
10+
from launch_ros.parameter_descriptions import ParameterValue
11+
from launch import LaunchDescription
12+
from launch.actions import DeclareLaunchArgument
13+
from launch.conditions import IfCondition
14+
from launch.launch_context import LaunchContext
15+
from launch.substitutions import LaunchConfiguration, PythonExpression
16+
17+
18+
# pylint: disable=too-many-locals
19+
# This is a launch file, so it's okay to have a lot of local variables.
20+
def generate_launch_description():
21+
"""Creates launch description to run all perception nodes"""
22+
launch_description = LaunchDescription()
23+
24+
# Get the ada_feeding_perception share directory
25+
ada_feeding_perception_share_dir = get_package_share_directory(
26+
"ada_feeding_perception"
27+
)
28+
29+
# Declare launch arguments
30+
use_republisher_da = DeclareLaunchArgument(
31+
"use_republisher",
32+
default_value="true",
33+
description="Whether to use the republisher node",
34+
)
35+
use_republisher = LaunchConfiguration("use_republisher")
36+
launch_description.add_action(use_republisher_da)
37+
republished_namespace_da = DeclareLaunchArgument(
38+
"republished_namespace",
39+
default_value="/local",
40+
description="The namespace to republish topics under.",
41+
)
42+
republished_namespace = LaunchConfiguration("republished_namespace")
43+
launch_description.add_action(republished_namespace_da)
44+
45+
# If we are using the republisher, add the republisher node
46+
republisher_config = os.path.join(
47+
ada_feeding_perception_share_dir, "config", "republisher.yaml"
48+
)
49+
republisher_params = {}
50+
republisher_params["republished_namespace"] = ParameterValue(
51+
republished_namespace, value_type=str
52+
)
53+
republisher = Node(
54+
package="ada_feeding_perception",
55+
name="republisher",
56+
executable="republisher",
57+
parameters=[republisher_config, republisher_params],
58+
condition=IfCondition(use_republisher),
59+
)
60+
launch_description.add_action(republisher)
61+
62+
# Remap from the perception nodes to the realsense topics
63+
prefix = PythonExpression(
64+
expression=[
65+
"'",
66+
republished_namespace,
67+
"' if '",
68+
use_republisher,
69+
"'=='true' else ''",
70+
]
71+
)
72+
realsense_remappings = [
73+
(
74+
"~/image",
75+
PythonExpression(
76+
expression=["'", prefix, "/camera/color/image_raw/compressed'"]
77+
),
78+
),
79+
(
80+
"~/camera_info",
81+
PythonExpression(expression=["'", prefix, "/camera/color/camera_info'"]),
82+
),
83+
(
84+
"~/aligned_depth",
85+
PythonExpression(
86+
expression=["'", prefix, "/camera/aligned_depth_to_color/image_raw'"]
87+
),
88+
),
89+
]
90+
91+
# Load the segment from point node
92+
segment_from_point_config = os.path.join(
93+
ada_feeding_perception_share_dir, "config", "segment_from_point.yaml"
94+
)
95+
segment_from_point_params = {}
96+
segment_from_point_params["model_dir"] = ParameterValue(
97+
os.path.join(ada_feeding_perception_share_dir, "model"), value_type=str
98+
)
99+
segment_from_point = Node(
100+
package="ada_feeding_perception",
101+
name="segment_from_point",
102+
executable="segment_from_point",
103+
parameters=[segment_from_point_config, segment_from_point_params],
104+
remappings=realsense_remappings,
105+
)
106+
launch_description.add_action(segment_from_point)
107+
108+
# Load the face detection node
109+
face_detection_config = os.path.join(
110+
ada_feeding_perception_share_dir, "config", "face_detection.yaml"
111+
)
112+
face_detection_params = {}
113+
face_detection_params["model_dir"] = ParameterValue(
114+
os.path.join(ada_feeding_perception_share_dir, "model"), value_type=str
115+
)
116+
face_detection_remappings = [
117+
("~/face_detection", "/face_detection"),
118+
("~/face_detection_img", "/face_detection_img"),
119+
("~/toggle_face_detection", "/toggle_face_detection"),
120+
]
121+
face_detection = Node(
122+
package="ada_feeding_perception",
123+
name="face_detection",
124+
executable="face_detection",
125+
parameters=[face_detection_config, face_detection_params],
126+
remappings=realsense_remappings + face_detection_remappings,
127+
)
128+
launch_description.add_action(face_detection)
129+
130+
return launch_description

ada_feeding_perception/launch/ada_feeding_perception_launch.xml

Lines changed: 0 additions & 22 deletions
This file was deleted.

ada_feeding_perception/launch/test_food_segmentation_launch.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<param name="base_dir" value="$(find-pkg-share ada_feeding_perception)"/>
55

66
<!-- The image topic must be the same topic that the SegmentFromPoint action server is listening to -->
7-
<remap from="~/image" to="/camera/color/image_raw/compressed"/>
7+
<remap from="~/image" to="/local/camera/color/image_raw/compressed"/>
88
</node>
99

1010
</launch>

ada_feeding_perception/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
tests_require=["pytest"],
4242
entry_points={
4343
"console_scripts": [
44+
"republisher = ada_feeding_perception.republisher:main",
4445
"segment_from_point = ada_feeding_perception.segment_from_point:main",
4546
"test_segment_from_point = ada_feeding_perception.test_segment_from_point:main",
4647
"face_detection = ada_feeding_perception.face_detection:main",

0 commit comments

Comments
 (0)