Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add symmetries publisher and publish empty messages on no detections #1

Merged
merged 34 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e8d705b
Untested symmetries publisher
Kotochleb Jul 16, 2024
d956f07
Add unittest basis
Kotochleb Jul 17, 2024
f840ba6
Update setup.py
Kotochleb Jul 18, 2024
58a8288
Fix thread cleanup
Kotochleb Jul 18, 2024
f6832ba
Fix typos | Change QOS in tests
Kotochleb Jul 18, 2024
46933c0
Fix typos in setup.py
Kotochleb Jul 18, 2024
d79df1c
Fix typos pointed during review
Kotochleb Jul 23, 2024
0ba5392
Add draft for continuous to discrete symmetries converter
Kotochleb Jul 23, 2024
e9e8e03
Add package dependencies
Kotochleb Jul 23, 2024
278047a
Fix formatting of package.xml
Kotochleb Jul 23, 2024
b4fc4e6
Add option to return ROS messages
Kotochleb Jul 23, 2024
31b280f
Rename function
Kotochleb Jul 23, 2024
1c35c18
Fix comment
Kotochleb Jul 23, 2024
69709fb
Add base for unittests
Kotochleb Jul 23, 2024
604184c
Smarter way to aviod `__pycache__`
Kotochleb Jul 24, 2024
5162234
Update happypose_msgs/msg/ObjectSymmetriesArray.msg
Kotochleb Jul 24, 2024
c15e794
Update happypose_msgs/msg/ObjectSymmetries.msg
Kotochleb Jul 24, 2024
89c0140
Update happypose_ros/.gitignore
Kotochleb Jul 24, 2024
b4eb1e0
Update happypose_ros/.gitignore
Kotochleb Jul 24, 2024
aa004fe
Merge branch 'main' of https://github.com/agimus-project/happypose_ro…
Kotochleb Jul 24, 2024
ee3f145
Cover all test cases | Fix bugs
Kotochleb Jul 24, 2024
adb790f
Merge branch 'main' of https://github.com/agimus-project/happypose_ro…
Kotochleb Jul 24, 2024
df8a165
Update package.xml
Kotochleb Jul 24, 2024
81be232
Extend time for unit tests
Kotochleb Jul 24, 2024
c6eefb5
Update .gitignore
Kotochleb Jul 24, 2024
052c273
FIx gitignore comment
Kotochleb Jul 24, 2024
bd5de32
Reduce workload on actions | Fix test order
Kotochleb Jul 24, 2024
11621b5
Hopefully reduce number of memory allocations
Kotochleb Jul 25, 2024
bf62c59
Update comment
Kotochleb Jul 25, 2024
0be9c5e
Fix pacakge version from 0.0.2 to 0.2.0
Kotochleb Jul 25, 2024
c1964f5
FIx comment typo
Kotochleb Jul 25, 2024
2341260
Fix rotations around symmetry axis
Kotochleb Aug 2, 2024
055cfd6
Use `np.linspace` to generate rotation steps | Add notebook with an e…
Kotochleb Aug 8, 2024
b3e4895
Update documentation
Kotochleb Sep 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ The node provides the following ROS topics:

Information about the used pose estimator (currently only CosyPose is supported) and URL with object database location.

- **happypose/object_symmetries** [happypose_msgs/msg/ObjectSymmetriesArray] (*QOS: TRANSIENT_LOCAL*)

Discrete and continuous symmetries of objects in the dataset.

- **happypose/markers** [visualization_msgs/msg/MarkerArray]

Array of markers used to visualize detections with their meshes in software like RViz 2.
Expand Down
2 changes: 1 addition & 1 deletion happypose_examples/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.5.0)
cmake_minimum_required(VERSION 3.10)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the version of CMake that is the default on Ubuntu LTS 22.04?

Copy link

Choose a reason for hiding this comment

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

project(happypose_examples)

find_package(ament_cmake REQUIRED)
Expand Down
2 changes: 1 addition & 1 deletion happypose_examples/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>happypose_examples</name>
<version>0.0.0</version>
<version>0.0.2</version>
<description>Examples for happypose_ros package</description>
<author email="[email protected]">Krzysztof Wojciechowski</author>
<maintainer email="[email protected]">Guilhem Saurel</maintainer>
Expand Down
21 changes: 21 additions & 0 deletions happypose_msgs/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.10)
Copy link
Contributor

Choose a reason for hiding this comment

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

CMake version on Ubuntu 22.04?
This code targets this version at the minimum so it should ask for this CMake version at the minimum.

project(happypose_msgs)

find_package(ament_cmake REQUIRED)
Copy link
Contributor

Choose a reason for hiding this comment

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

For future reference, concerning pure ament package like this please consider ament_cmake_auto.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Due to an additional package happypose_msgs_py being created on a build time, I decided to resort to default ament

find_package(builtin_interfaces REQUIRED)
find_package(std_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(
${PROJECT_NAME}
msg/ContinuousSymmetry.msg
msg/ObjectSymmetries.msg
msg/ObjectSymmetriesArray.msg
DEPENDENCIES
builtin_interfaces
std_msgs
geometry_msgs)

ament_export_dependencies(rosidl_default_runtime)
ament_package()
6 changes: 6 additions & 0 deletions happypose_msgs/msg/ContinuousSymmetry.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Definition of continuous symmetry.
# Consists of rotation axis and offset.
# Symilarly to HappyPose object ContinuousSymmetry

geometry_msgs/Vector3 offset
geometry_msgs/Vector3 axis
10 changes: 10 additions & 0 deletions happypose_msgs/msg/ObjectSymmetries.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Class id for which symmetries are considered
string class_id
# Lists discrete and continuous symmetries of considered object.
# If no symmetries of a given type, list is left empty.

# In HappyPose discrete symmetries are represented as
# transformation matrices. Those matrices are directyl
# converted to the Transform message.
geometry_msgs/Transform[] symmetries_discrete
ContinuousSymmetry[] symmetries_continuous
5 changes: 5 additions & 0 deletions happypose_msgs/msg/ObjectSymmetriesArray.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Time stamp at which the message was sent
std_msgs/Header header

# List of objects detected be HappyPose node
ObjectSymmetries[] objects
31 changes: 31 additions & 0 deletions happypose_msgs/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0"?>
<package format="3">
<name>happypose_msgs</name>
<version>0.0.2</version>
<description>Custom messages used by happypose_ros package.</description>
<author email="[email protected]">Krzysztof Wojciechowski</author>
<maintainer email="[email protected]">Guilhem Saurel</maintainer>
<license>BSD</license>

<url type="website">https://gitlab.laas.fr/kwojciecho/happypose_ros</url>
<url type="bugtracker">https://gitlab.laas.fr/kwojciecho/happypose_ros/-/issues</url>
<url type="repository">https://gitlab.laas.fr/kwojciecho/happypose_ros</url>

<buildtool_depend>ament_cmake</buildtool_depend>
<buildtool_depend>rosidl_default_generators</buildtool_depend>

<build_depend>builtin_interfaces</build_depend>
<build_depend>std_msgs</build_depend>
<build_depend>geometry_msgs</build_depend>

<exec_depend>builtin_interfaces</exec_depend>
<exec_depend>std_msgs</exec_depend>
<exec_depend>geometry_msgs</exec_depend>
<exec_depend>rosidl_default_runtime</exec_depend>

<member_of_group>rosidl_interface_packages</member_of_group>

<export>
<build_type>ament_cmake</build_type>
</export>
</package>
172 changes: 119 additions & 53 deletions happypose_ros/happypose_ros/happypose_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
import torch.multiprocessing as mp
import queue


import rclpy
from rclpy.duration import Duration
from rclpy.exceptions import ParameterException
import rclpy.logging
from rclpy.node import Node
from rclpy.time import Time
from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile

from tf2_ros import TransformBroadcaster

Expand All @@ -34,8 +34,11 @@
get_camera_transform,
get_detection_array_msg,
get_marker_array_msg,
get_object_symmetries_msg,
)

from happypose_msgs.msg import ObjectSymmetriesArray # noqa: E402

# Automatically generated file
from happypose_ros.happypose_ros_parameters import happypose_ros # noqa: E402

Expand All @@ -44,6 +47,7 @@ def happypose_worker_proc(
worker_free: mp.Value,
observation_tensor_queue: mp.Queue,
results_queue: mp.Queue,
symmetries_queue: mp.Queue,
params_queue: mp.Queue,
) -> None:
"""Function used to trigger worker process.
Expand All @@ -59,6 +63,8 @@ def happypose_worker_proc(
"""
# Initialize the pipeline
pipeline = HappyPosePipeline(params_queue.get())
# Inform ROS node about the dataset
symmetries_queue.put(pipeline.get_dataset())

# Notify parent that initialization has finished
with worker_free.get_lock():
Expand All @@ -68,11 +74,14 @@ def happypose_worker_proc(
while True:
# Await any data on all the input queues
observation = observation_tensor_queue.get(block=True, timeout=None)
if observation is None:
break

# Update inference args if available
try:
params = params_queue.get_nowait()
pipeline.update_params(params)
symmetries_queue.put(pipeline.get_dataset())
except queue.Empty:
pass

Expand Down Expand Up @@ -121,6 +130,7 @@ def __init__(self, node_name: str = "happypose_node", **kwargs) -> None:
self._worker_free = ctx.Value(c_bool, False)
self._observation_tensor_queue = ctx.Queue(1)
self._results_queue = ctx.Queue(1)
self._symmetries_queue = ctx.Queue(1)
self._params_queue = ctx.Queue(1)

self._update_dynamic_params(True)
Expand All @@ -133,12 +143,13 @@ def __init__(self, node_name: str = "happypose_node", **kwargs) -> None:
self._worker_free,
self._observation_tensor_queue,
self._results_queue,
self._symmetries_queue,
self._params_queue,
),
)
self._await_results_task = None

# TODO once Megapose is available initialization
# TODO once MegaPose is available initialization
# should be handled better
self._vision_info_msg = VisionInfo(
method=self._params.pose_estimator_type,
Expand All @@ -164,6 +175,16 @@ def __init__(self, node_name: str = "happypose_node", **kwargs) -> None:
VisionInfo, "happypose/vision_info", 10
)

# Set the message to be "latched"
qos = QoSProfile(
depth=1,
durability=DurabilityPolicy.TRANSIENT_LOCAL,
history=HistoryPolicy.KEEP_LAST,
)
self._symmetries_publisher = self.create_publisher(
ObjectSymmetriesArray, "happypose/object_symmetries", qos
)

if self._params.cameras.n_min_cameras > len(self._cameras):
e = ParameterException(
"Minimum number of cameras to trigger the pipeline is"
Expand Down Expand Up @@ -242,21 +263,40 @@ def __init__(self, node_name: str = "happypose_node", **kwargs) -> None:
# Start the worker when all possible errors are handled
self._happypose_worker.start()

# Start spinner waiting for updates in symmetries
self._symmetries_queue_task = Thread(target=self._symmetries_queue_spinner)
self._symmetries_queue_task.start()

self.get_logger().info(
"Node initialized. Waiting for HappyPose worker to initialize...",
)

def destroy_node(self) -> None:
"""Destroys the node and closes all queues."""
# Signal closing of queues
if self._symmetries_queue is not None:
self._symmetries_queue.put(None)
if self._observation_tensor_queue is not None:
self._observation_tensor_queue.close()
self._observation_tensor_queue.put(None)

# Close receiving queue
if self._results_queue is not None:
self._results_queue.close()

# Stop threads
if self._await_results_task is not None:
self._await_results_task.join()
if self._symmetries_queue_task is not None:
self._symmetries_queue_task.join()
if self._symmetries_queue is not None:
self._symmetries_queue.close()

# Stop worker process
if self._happypose_worker is not None:
self._happypose_worker.join()
self._happypose_worker.terminate()
if self._observation_tensor_queue is not None:
self._observation_tensor_queue.close()
super().destroy_node()

def _update_dynamic_params(self, on_init: bool = False) -> None:
Expand All @@ -278,6 +318,24 @@ def _update_dynamic_params(self, on_init: bool = False) -> None:
if self._params.verbose_info_logs and not on_init:
self.get_logger().info("Parameter change occurred.")

def _symmetries_queue_spinner(self) -> None:
"""Awaits new data in a queue with symmetries and publishes them on a ROS topic"""
try:
while True:
symmetries = self._symmetries_queue.get(block=True, timeout=None)
if symmetries is None:
break

header = Header(
frame_id="",
stamp=self.get_clock().now().to_msg(),
)
symmetries_msg = get_object_symmetries_msg(symmetries, header)
self._symmetries_publisher.publish(symmetries_msg)
# Queue is closed
except ValueError:
pass

def _on_image_cb(self) -> None:
"""Callback function used to synchronize incoming images from different cameras.
Performs basic checks if worker process is available and can start HappyPose pipeline.
Expand Down Expand Up @@ -327,7 +385,7 @@ def _trigger_pipeline(self) -> None:

if len(processed_cameras) < self._params.cameras.n_min_cameras:
# Throttle logs to either 5 times the timeout or 10 seconds
timeout = max(5 * self._params.cameras.timeout, 10.0)
timeout = max(5.0 * self._params.cameras.timeout, 10.0)
if (now - self._last_pipeline_trigger) > (Duration(seconds=timeout)):
self.get_logger().warn(
"Unable to start pipeline! Not enough camera"
Expand Down Expand Up @@ -401,51 +459,6 @@ def _await_results(self, cam_data: dict) -> None:

results = self._results_queue.get(block=True, timeout=None)

if results is None:
if self._params.verbose_info_logs:
self.get_logger().info("No objects detected.")
return

if self._params.verbose_info_logs:
self.get_logger().info(f"Detected {len(results['infos'])} objects.")

if self._multiview:
missing_cameras = len(cam_data) - len(results["camera_infos"])
if missing_cameras > 0:
# Keep only the cameras that were not discarded in multiview
cam_data = {
name: cam
for i, (name, cam) in enumerate(cam_data.items())
if i in results["camera_infos"].view_id.values
}

if self._leading_camera not in cam_data.keys():
self.get_logger().error(
f"Leading camera '{self._leading_camera}'"
" was discarded when performing multi-view!"
)
return

if self._params.verbose_info_logs:
self.get_logger().warn(
f"{missing_cameras} camera views were discarded in multi-view."
)

lead_cam_idx = list(cam_data.keys()).index(self._leading_camera)
# Transform objects into leading camera's reference frame
leading_cam_pose_inv = results["camera_poses"][lead_cam_idx].inverse()
# Transform detected objects' poses
results["poses"] = leading_cam_pose_inv @ results["poses"]
# Create mask of cameras that expect to have TF published
cams_to_tf = [
self._params.cameras.get_entry(name).publish_tf
for name in cam_data.keys()
]
# Transform cameras' poses
results["camera_poses"][cams_to_tf] = (
leading_cam_pose_inv @ results["camera_poses"][cams_to_tf]
)

header = Header(
frame_id=cam_data[self._leading_camera]["frame_id"],
# Choose sorted time stamp
Expand All @@ -456,10 +469,63 @@ def _await_results(self, cam_data: dict) -> None:
).to_msg(),
)

# In case of multi-view, do not use bounding boxes
detections = get_detection_array_msg(
results, header, has_bbox=not self._multiview
)
if results is not None:
if self._params.verbose_info_logs:
self.get_logger().info(f"Detected {len(results['infos'])} objects.")

if self._multiview:
missing_cameras = len(cam_data) - len(results["camera_infos"])
if missing_cameras > 0:
# Keep only the cameras that were not discarded in multiview
cam_data = {
name: cam
for i, (name, cam) in enumerate(cam_data.items())
if i in results["camera_infos"].view_id.values
}

if self._leading_camera not in cam_data.keys():
self.get_logger().error(
f"Leading camera '{self._leading_camera}'"
" was discarded when performing multi-view!"
)
return

if self._params.verbose_info_logs:
self.get_logger().warn(
f"{missing_cameras} camera views were discarded in multi-view."
)

lead_cam_idx = list(cam_data.keys()).index(self._leading_camera)
# Transform objects into leading camera's reference frame
leading_cam_pose_inv = results["camera_poses"][
lead_cam_idx
].inverse()
# Transform detected objects' poses
results["poses"] = leading_cam_pose_inv @ results["poses"]
# Create mask of cameras that expect to have TF published
cams_to_tf = [
self._params.cameras.get_entry(name).publish_tf
for name in cam_data.keys()
]
# Transform cameras' poses
results["camera_poses"][cams_to_tf] = (
leading_cam_pose_inv @ results["camera_poses"][cams_to_tf]
)

# In case of multi-view, do not use bounding boxes
detections = get_detection_array_msg(
results, header, has_bbox=not self._multiview
)

else:
if self._params.verbose_info_logs:
self.get_logger().info("No objects detected.")

detections = Detection2DArray(
header=header,
detections=[],
)

self._detections_publisher.publish(detections)

self._vision_info_msg.header = header
Expand Down
Loading