Compare commits
No commits in common. "main" and "main" have entirely different histories.
|
|
@ -118,10 +118,10 @@ install(DIRECTORY launch config models rviz urdf worlds scripts gui
|
||||||
DESTINATION share/${PROJECT_NAME}/
|
DESTINATION share/${PROJECT_NAME}/
|
||||||
)
|
)
|
||||||
|
|
||||||
# Install Python scripts with correct permissions
|
# Install Python scripts with correct permissions
|
||||||
install(PROGRAMS scripts/scan_frame_fix.py scripts/camera_topic_remap.py
|
install(PROGRAMS scripts/scan_frame_fix.py scripts/camera_topic_remap.py scripts/depth_to_pointcloud.py
|
||||||
DESTINATION lib/${PROJECT_NAME}
|
DESTINATION lib/${PROJECT_NAME}
|
||||||
)
|
)
|
||||||
|
|
||||||
install(DIRECTORY include/
|
install(DIRECTORY include/
|
||||||
DESTINATION include/
|
DESTINATION include/
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,9 @@
|
||||||
gz_type_name: gz.msgs.CameraInfo
|
gz_type_name: gz.msgs.CameraInfo
|
||||||
direction: GZ_TO_ROS
|
direction: GZ_TO_ROS
|
||||||
|
|
||||||
- ros_topic_name: /intel_realsense_r200_pointcloud
|
# === Depth image (L16 workaround for Humble) ===
|
||||||
ros_type_name: sensor_msgs/msg/PointCloud2
|
# Gazebo depth camera with <format>L16</format> publishes on /depth_image topic
|
||||||
gz_topic_name: /world/default/model/burger/link/realsense_link/sensor/intel_realsense_r200_pointcloud/points
|
# as gz.msgs.Image type (NOT /image). This bypasses unsupported gz.msgs.DepthImage.
|
||||||
gz_type_name: gz.msgs.PointCloudPacked
|
|
||||||
direction: GZ_TO_ROS
|
|
||||||
|
|
||||||
- ros_topic_name: /intel_realsense_r200_depth/depth/image_raw
|
- ros_topic_name: /intel_realsense_r200_depth/depth/image_raw
|
||||||
ros_type_name: sensor_msgs/msg/Image
|
ros_type_name: sensor_msgs/msg/Image
|
||||||
gz_topic_name: /world/default/model/burger/link/realsense_link/sensor/intel_realsense_r200_depth/depth_image
|
gz_topic_name: /world/default/model/burger/link/realsense_link/sensor/intel_realsense_r200_depth/depth_image
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Launch depth image -> point cloud conversion for TurtleBot3 Gz Sim.
|
||||||
|
# Subscribes to the bridged L16 depth image and camera_info, and publishes
|
||||||
|
# a sensor_msgs/PointCloud2 that can be visualized in RViz.
|
||||||
|
|
||||||
|
from launch import LaunchDescription
|
||||||
|
from launch.actions import DeclareLaunchArgument
|
||||||
|
from launch.substitutions import LaunchConfiguration
|
||||||
|
from launch_ros.actions import Node
|
||||||
|
|
||||||
|
|
||||||
|
def generate_launch_description():
|
||||||
|
use_sim_time = LaunchConfiguration('use_sim_time', default='true')
|
||||||
|
|
||||||
|
declare_use_sim_time = DeclareLaunchArgument(
|
||||||
|
'use_sim_time',
|
||||||
|
default_value='true',
|
||||||
|
description='Use simulation clock if true')
|
||||||
|
|
||||||
|
# Custom Python replacement for depth_image_proc/point_cloud_xyz.
|
||||||
|
# It converts the bridged L16 depth image + camera_info into a PointCloud2.
|
||||||
|
depth_to_cloud_node = Node(
|
||||||
|
package='turtlebot3_gazebo',
|
||||||
|
executable='depth_to_pointcloud.py',
|
||||||
|
name='depth_to_pointcloud',
|
||||||
|
parameters=[{'use_sim_time': use_sim_time}],
|
||||||
|
output='screen',
|
||||||
|
)
|
||||||
|
|
||||||
|
ld = LaunchDescription()
|
||||||
|
ld.add_action(declare_use_sim_time)
|
||||||
|
ld.add_action(depth_to_cloud_node)
|
||||||
|
|
||||||
|
return ld
|
||||||
|
|
@ -19,20 +19,22 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from ament_index_python.packages import get_package_share_directory
|
from ament_index_python.packages import get_package_share_directory
|
||||||
from launch import LaunchDescription
|
from launch import LaunchDescription
|
||||||
from launch.actions import IncludeLaunchDescription, SetEnvironmentVariable
|
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, SetEnvironmentVariable
|
||||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
from launch.conditions import IfCondition
|
||||||
from launch.substitutions import LaunchConfiguration
|
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||||
from launch_ros.actions import Node
|
from launch.substitutions import LaunchConfiguration, PythonExpression
|
||||||
|
from launch_ros.actions import Node
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_description():
|
def generate_launch_description():
|
||||||
launch_file_dir = os.path.join(get_package_share_directory('turtlebot3_gazebo'), 'launch')
|
launch_file_dir = os.path.join(get_package_share_directory('turtlebot3_gazebo'), 'launch')
|
||||||
|
|
||||||
use_sim_time = LaunchConfiguration('use_sim_time', default='true')
|
use_sim_time = LaunchConfiguration('use_sim_time', default='true')
|
||||||
x_pose = LaunchConfiguration('x_pose', default='0.0')
|
x_pose = LaunchConfiguration('x_pose', default='0.0')
|
||||||
y_pose = LaunchConfiguration('y_pose', default='0.0')
|
y_pose = LaunchConfiguration('y_pose', default='0.0')
|
||||||
|
use_depth_cloud = LaunchConfiguration('use_depth_cloud', default='true')
|
||||||
|
|
||||||
pkg_share_dir = get_package_share_directory('turtlebot3_gazebo')
|
pkg_share_dir = get_package_share_directory('turtlebot3_gazebo')
|
||||||
world = os.path.join(pkg_share_dir, 'worlds', 'office_gz_dartsim.sdf')
|
world = os.path.join(pkg_share_dir, 'worlds', 'office_gz_dartsim.sdf')
|
||||||
|
|
@ -83,18 +85,33 @@ def generate_launch_description():
|
||||||
parameters=[{'use_sim_time': True}],
|
parameters=[{'use_sim_time': True}],
|
||||||
)
|
)
|
||||||
|
|
||||||
ld = LaunchDescription()
|
depth_cloud_cmd = IncludeLaunchDescription(
|
||||||
|
PythonLaunchDescriptionSource(
|
||||||
# Set GZ_SIM_RESOURCE_PATH so gz sim can resolve model:// URIs
|
os.path.join(launch_file_dir, 'depth_cloud.launch.py')
|
||||||
ld.add_action(SetEnvironmentVariable(
|
),
|
||||||
name='GZ_SIM_RESOURCE_PATH',
|
launch_arguments={'use_sim_time': use_sim_time}.items(),
|
||||||
value=office_models_path + os.pathsep + models_path
|
condition=IfCondition(PythonExpression(["'", use_depth_cloud, "' == 'true'"])),
|
||||||
|
)
|
||||||
|
|
||||||
|
ld = LaunchDescription()
|
||||||
|
|
||||||
|
declare_use_depth_cloud_cmd = DeclareLaunchArgument(
|
||||||
|
'use_depth_cloud',
|
||||||
|
default_value='true',
|
||||||
|
description='Launch depth image -> point cloud conversion node'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set GZ_SIM_RESOURCE_PATH so gz sim can resolve model:// URIs
|
||||||
|
ld.add_action(SetEnvironmentVariable(
|
||||||
|
name='GZ_SIM_RESOURCE_PATH',
|
||||||
|
value=office_models_path + os.pathsep + models_path
|
||||||
))
|
))
|
||||||
|
|
||||||
# Add the commands to the launch description
|
# Add the commands to the launch description
|
||||||
|
|
||||||
ld.add_action(gz_sim_cmd)
|
ld.add_action(gz_sim_cmd)
|
||||||
ld.add_action(robot_state_publisher_cmd)
|
ld.add_action(robot_state_publisher_cmd)
|
||||||
ld.add_action(spawn_turtlebot_cmd)
|
ld.add_action(spawn_turtlebot_cmd)
|
||||||
ld.add_action(scan_frame_fix_cmd)
|
ld.add_action(scan_frame_fix_cmd)
|
||||||
return ld
|
|
||||||
|
return ld
|
||||||
|
|
|
||||||
|
|
@ -341,11 +341,11 @@
|
||||||
<!-- RGB camera sensor - migrated from libgazebo_ros_camera.so to gz-sim native
|
<!-- RGB camera sensor - migrated from libgazebo_ros_camera.so to gz-sim native
|
||||||
Original plugin had: hack_baseline=0.07 (stereo baseline)
|
Original plugin had: hack_baseline=0.07 (stereo baseline)
|
||||||
Note: Not applicable in Gz Sim native sensor (same reason as depth camera above). -->
|
Note: Not applicable in Gz Sim native sensor (same reason as depth camera above). -->
|
||||||
<sensor name="intel_realsense_r200_rgb" type="camera">
|
<sensor name="intel_realsense_r200_rgb" type="camera">
|
||||||
<gz_frame_id>camera_rgb_frame</gz_frame_id>
|
<gz_frame_id>camera_rgb_frame</gz_frame_id>
|
||||||
<always_on>true</always_on>
|
<always_on>true</always_on>
|
||||||
<visualize>true</visualize>
|
<visualize>true</visualize>
|
||||||
<update_rate>30</update_rate>
|
<update_rate>30</update_rate>
|
||||||
<pose>0.076 0.0 0.093 0 0 0</pose>
|
<pose>0.076 0.0 0.093 0 0 0</pose>
|
||||||
<camera name="realsense_rgb_camera">
|
<camera name="realsense_rgb_camera">
|
||||||
<horizontal_fov>1.02974</horizontal_fov>
|
<horizontal_fov>1.02974</horizontal_fov>
|
||||||
|
|
@ -364,30 +364,10 @@
|
||||||
<type>gaussian</type>
|
<type>gaussian</type>
|
||||||
<mean>0.0</mean>
|
<mean>0.0</mean>
|
||||||
<stddev>0.007</stddev>
|
<stddev>0.007</stddev>
|
||||||
</noise>
|
</noise>
|
||||||
</camera>
|
</camera>
|
||||||
</sensor>
|
</sensor>
|
||||||
|
</link>
|
||||||
<!-- Native RGBD sensor for point cloud output. -->
|
|
||||||
<sensor name="intel_realsense_r200_pointcloud" type="rgbd_camera">
|
|
||||||
<gz_frame_id>realsense_depth_frame</gz_frame_id>
|
|
||||||
<always_on>true</always_on>
|
|
||||||
<visualize>false</visualize>
|
|
||||||
<update_rate>30</update_rate>
|
|
||||||
<topic>intel_realsense_r200_pointcloud</topic>
|
|
||||||
<camera name="realsense_pointcloud_camera">
|
|
||||||
<horizontal_fov>1.02974</horizontal_fov>
|
|
||||||
<image>
|
|
||||||
<width>640</width>
|
|
||||||
<height>480</height>
|
|
||||||
</image>
|
|
||||||
<clip>
|
|
||||||
<near>0.02</near>
|
|
||||||
<far>10</far>
|
|
||||||
</clip>
|
|
||||||
</camera>
|
|
||||||
</sensor>
|
|
||||||
</link>
|
|
||||||
|
|
||||||
<joint name="base_joint" type="fixed">
|
<joint name="base_joint" type="fixed">
|
||||||
<parent>base_footprint</parent>
|
<parent>base_footprint</parent>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Convert a depth image (16UC1 mm or 32FC1 m) + CameraInfo into a sensor_msgs/PointCloud2.
|
||||||
|
# This replaces depth_image_proc/point_cloud_xyz for environments where that package
|
||||||
|
# is not available or its executable is missing.
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import rclpy
|
||||||
|
from rclpy.node import Node
|
||||||
|
from sensor_msgs.msg import CameraInfo, Image, PointCloud2, PointField
|
||||||
|
from std_msgs.msg import Header
|
||||||
|
|
||||||
|
|
||||||
|
class DepthToPointCloud(Node):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__('depth_to_pointcloud')
|
||||||
|
|
||||||
|
self.declare_parameter('depth_topic', '/intel_realsense_r200_depth/depth/image_raw')
|
||||||
|
self.declare_parameter('camera_info_topic', '/intel_realsense_r200_depth/camera_info')
|
||||||
|
self.declare_parameter('points_topic', '/intel_realsense_r200_depth/depth/color/points')
|
||||||
|
self.declare_parameter('max_depth_m', 10.0)
|
||||||
|
|
||||||
|
depth_topic = self.get_parameter('depth_topic').value
|
||||||
|
camera_info_topic = self.get_parameter('camera_info_topic').value
|
||||||
|
points_topic = self.get_parameter('points_topic').value
|
||||||
|
self.max_depth_m = float(self.get_parameter('max_depth_m').value)
|
||||||
|
|
||||||
|
self.camera_info = None
|
||||||
|
|
||||||
|
self.info_sub = self.create_subscription(
|
||||||
|
CameraInfo, camera_info_topic, self.info_callback, qos_profile=1)
|
||||||
|
self.depth_sub = self.create_subscription(
|
||||||
|
Image, depth_topic, self.depth_callback, qos_profile=1)
|
||||||
|
self.cloud_pub = self.create_publisher(PointCloud2, points_topic, qos_profile=1)
|
||||||
|
|
||||||
|
self.get_logger().info(
|
||||||
|
f'Waiting for camera info on {camera_info_topic}; '
|
||||||
|
f'publishing points on {points_topic}'
|
||||||
|
)
|
||||||
|
|
||||||
|
def info_callback(self, msg: CameraInfo):
|
||||||
|
self.camera_info = msg
|
||||||
|
|
||||||
|
def depth_callback(self, msg: Image):
|
||||||
|
if self.camera_info is None:
|
||||||
|
self.get_logger().warn(
|
||||||
|
'CameraInfo not received yet; skipping depth frame',
|
||||||
|
throttle_duration_sec=5)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Decode depth image according to its encoding.
|
||||||
|
# Gz Sim may publish depth as either 16-bit unsigned mm (16UC1/mono16)
|
||||||
|
# or 32-bit float meters (32FC1), depending on sensor/bridge version.
|
||||||
|
if msg.encoding in ('16UC1', 'mono16'):
|
||||||
|
depth_raw = np.frombuffer(msg.data, dtype=np.uint16).reshape(msg.height, msg.width)
|
||||||
|
depth_m = depth_raw.astype(np.float32) * 0.001
|
||||||
|
elif msg.encoding == '32FC1':
|
||||||
|
depth_m = np.frombuffer(msg.data, dtype=np.float32).reshape(msg.height, msg.width)
|
||||||
|
else:
|
||||||
|
self.get_logger().error(
|
||||||
|
f'Unsupported depth encoding: {msg.encoding}; expected 16UC1/mono16 or 32FC1',
|
||||||
|
throttle_duration_sec=5)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Intrinsics from CameraInfo
|
||||||
|
K = self.camera_info.k
|
||||||
|
fx, fy = float(K[0]), float(K[4])
|
||||||
|
cx, cy = float(K[2]), float(K[5])
|
||||||
|
|
||||||
|
# Pixel coordinates
|
||||||
|
u = np.arange(msg.width, dtype=np.float32)
|
||||||
|
v = np.arange(msg.height, dtype=np.float32)
|
||||||
|
u, v = np.meshgrid(u, v)
|
||||||
|
|
||||||
|
# Filter valid depth pixels
|
||||||
|
valid = (depth_m > 0.0) & (depth_m < self.max_depth_m)
|
||||||
|
u = u[valid]
|
||||||
|
v = v[valid]
|
||||||
|
z = depth_m[valid]
|
||||||
|
|
||||||
|
if z.size == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Back-project to 3D (in the camera optical frame)
|
||||||
|
x = (u - cx) * z / fx
|
||||||
|
y = (v - cy) * z / fy
|
||||||
|
|
||||||
|
points = np.stack((x, y, z), axis=-1).astype(np.float32)
|
||||||
|
|
||||||
|
header = Header()
|
||||||
|
header.stamp = msg.header.stamp
|
||||||
|
header.frame_id = self.camera_info.header.frame_id
|
||||||
|
|
||||||
|
cloud_msg = PointCloud2()
|
||||||
|
cloud_msg.header = header
|
||||||
|
cloud_msg.height = 1
|
||||||
|
cloud_msg.width = int(points.shape[0])
|
||||||
|
cloud_msg.fields = [
|
||||||
|
PointField(name='x', offset=0, datatype=PointField.FLOAT32, count=1),
|
||||||
|
PointField(name='y', offset=4, datatype=PointField.FLOAT32, count=1),
|
||||||
|
PointField(name='z', offset=8, datatype=PointField.FLOAT32, count=1),
|
||||||
|
]
|
||||||
|
cloud_msg.is_bigendian = False
|
||||||
|
cloud_msg.point_step = 12
|
||||||
|
cloud_msg.row_step = cloud_msg.point_step * cloud_msg.width
|
||||||
|
cloud_msg.is_dense = True
|
||||||
|
cloud_msg.data = points.tobytes()
|
||||||
|
|
||||||
|
self.cloud_pub.publish(cloud_msg)
|
||||||
|
|
||||||
|
|
||||||
|
def main(args=None):
|
||||||
|
rclpy.init(args=args)
|
||||||
|
node = DepthToPointCloud()
|
||||||
|
try:
|
||||||
|
rclpy.spin(node)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
node.destroy_node()
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue