Skip to content

Commit 2ad0423

Browse files
Merge pull request #18 from Tang-JingWei/ROS2
Add ros2 publish uw image function.
2 parents 7b46374 + 089f2dd commit 2ad0423

File tree

5 files changed

+191
-3
lines changed

5 files changed

+191
-3
lines changed

docs/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,10 @@
55
### Added
66

77
- Initial version of OceanSim Extension
8+
9+
## [0.1.0] - 2025-08-05
10+
11+
### Added
12+
13+
- Add ros2 control function
14+
- Add ros2 publish uw image

docs/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,25 @@ We suggest that make sure the extension isaacsim.ros2.bridge is being setup to "
1111

1212

1313
## Usage:
14+
### ros2 control:
1415
We provided an exmaple util located at `isaacsim/oceansim/utils/ros2_control.py` for user to consult and develop on.
1516

1617
This util extends the control mode to ros control in the **sensor_example** extension.
1718

19+
### ros2 publish uw image:
20+
We add ros2 publish uw image function in the UW_Camera class, located at `isaacsim/oceansim/sensors/UW_Camera.py`.
21+
22+
For testing, we provide a ros2 subscriber example located at `isaacsim/oceansim/utils/ros2_image_subscriber.py`.
23+
24+
Test steps:
25+
1. check the Underwater Camera checkbox in the **sensor_example** extension.
26+
2. run the simulation.
27+
3. open a terminal and run the ros2_image_subscriber.py.
28+
```
29+
cd /path/to/oceansim/utils
30+
python3 ros2_image_subscriber.py
31+
```
32+
1833
## Acknowledgement:
1934
Great appreciation to [Tang-JingWei](https://github.com/Tang-JingWei) for contributng the ros bridge example for OceanSim.
2035

isaacsim/oceansim/sensors/UW_Camera.py

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@
1313
# Custom import
1414
from isaacsim.oceansim.utils.UWrenderer_utils import UW_render
1515

16+
'''
17+
Attention:
18+
19+
Before OceanSim extension being activated, the extension isaacsim.ros2.bridge should be activated, otherwise rclpy will
20+
fail to be loaded.
21+
22+
so, we suggest that make sure the extension isaacsim.ros2.bridge is being setup to "AUTOLOADED" in Window->Extension.
23+
'''
24+
import rclpy
25+
from sensor_msgs.msg import CompressedImage
26+
import time
27+
import cv2
1628

1729
class UW_Camera(Camera):
1830

@@ -64,7 +76,8 @@ def initialize(self,
6476
viewport: bool = True,
6577
writing_dir: str = None,
6678
UW_yaml_path: str = None,
67-
physics_sim_view=None):
79+
physics_sim_view=None,
80+
enable_ros2_pub=True, uw_img_topic="/oceansim/robot/uw_img", ros2_pub_frequency=5, ros2_pub_jpeg_quality=50):
6881

6982
"""Configure underwater rendering properties and initialize pipelines.
7083
@@ -77,7 +90,11 @@ def initialize(self,
7790
viewport (bool, optional): Enable viewport visualization. Defaults to True.
7891
writing_dir (str, optional): Directory to save rendered images. Defaults to None.
7992
UW_yaml_path (str, optional): Path to YAML file with water properties. Defaults to None.
80-
physics_sim_view (_type_, optional): _description_. Defaults to None.
93+
physics_sim_view (_type_, optional): _description_. Defaults to None.
94+
enable_ros2_pub (bool, optional): Enable ROS2 communication. Defaults to True.
95+
uw_img_topic (str, optional): ROS2 topic name for UW image. Defaults to "/oceansim/robot/uw_img".
96+
ros2_pub_frequency (int, optional): ROS2 publish frequency. Defaults to 5.
97+
ros2_pub_jpeg_quality (int, optional): ROS2 publish jpeg quality. Defaults to 50.
8198
8299
"""
83100
self._id = 0
@@ -116,9 +133,90 @@ def initialize(self,
116133
if writing_dir is not None:
117134
self._writing = True
118135
self._writing_backend = rep.BackendDispatch({"paths": {"out_dir": writing_dir}})
136+
137+
# ROS2 configuration
138+
self._enable_ros2_pub = enable_ros2_pub
139+
self._uw_img_topic = uw_img_topic
140+
self._last_publish_time = 0.0
141+
self._ros2_pub_frequency = ros2_pub_frequency # publish frequency, hz
142+
self._ros2_pub_jpeg_quality = ros2_pub_jpeg_quality
143+
self._setup_ros2_publisher()
119144

120145
print(f'[{self._name}] Initialized successfully. Data writing: {self._writing}')
121146

147+
def _setup_ros2_publisher(self):
148+
'''
149+
setup the publisher for uw image
150+
'''
151+
try:
152+
if not self._enable_ros2_pub:
153+
return
154+
155+
# Initialize ROS2 context if not already done
156+
if not rclpy.ok():
157+
rclpy.init()
158+
print(f'[{self._name}] ROS2 context initialized')
159+
160+
# Create uw image publisher node
161+
node_name = f'oceansim_rob_uw_img_pub_{self._name.lower()}'.replace(' ', '_')
162+
self._ros2_uw_img_node = rclpy.create_node(node_name)
163+
self._uw_img_pub = self._ros2_uw_img_node.create_publisher(
164+
CompressedImage,
165+
self._uw_img_topic,
166+
10
167+
)
168+
169+
except Exception as e:
170+
print(f'[{self._name}] ROS2 uw image publisher setup failed: {e}')
171+
172+
def _ros2_publish_uw_img(self, uw_img):
173+
"""
174+
publish the uw image
175+
"""
176+
try:
177+
if self._uw_img_pub is None:
178+
return
179+
180+
# fps control
181+
current_time = time.time()
182+
if current_time - self._last_publish_time < (1.0 / self._frequency):
183+
return
184+
185+
# Convert the image
186+
uw_image_cpu = uw_img.numpy()
187+
if uw_image_cpu.dtype != np.uint8:
188+
uw_image_cpu = uw_image_cpu.astype(np.uint8) # UW_render return 'rgba'
189+
uw_image_bgr = cv2.cvtColor(uw_image_cpu, cv2.COLOR_RGBA2BGR)
190+
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), self._ros2_pub_jpeg_quality] # JPEG quality, default 90, can be 0-100
191+
result, compressed_img = cv2.imencode('.jpg', uw_image_bgr, encode_param)
192+
if not result:
193+
print(f'[{self._name}] Failed to compress image to JPEG')
194+
return
195+
196+
# Create a ROS2 Image message
197+
msg = CompressedImage()
198+
msg.header.stamp = self._ros2_uw_img_node.get_clock().now().to_msg()
199+
msg.header.frame_id = 'uw_image'
200+
msg.format = 'jpeg'
201+
msg.data = compressed_img.tobytes()
202+
203+
# Publish the message
204+
self._uw_img_pub.publish(msg)
205+
206+
rclpy.spin_once(self._ros2_uw_img_node, timeout_sec=0.0)
207+
208+
self._last_publish_time = current_time
209+
210+
# debug
211+
# self._ros2_uw_img_node.get_logger().info(
212+
# f'Published image: encoding={msg.encoding}, '
213+
# f'width={msg.width}, height={msg.height}, step={msg.step}, '
214+
# f'data_size={len(msg.data)}'
215+
# )
216+
217+
except Exception as e:
218+
print(f'[{self._name}] ROS2 uw image publish failed: {e}')
219+
122220
def render(self):
123221
"""Process and display a single frame with underwater effects.
124222
@@ -150,6 +248,9 @@ def render(self):
150248
if self._writing:
151249
self._writing_backend.schedule(write_image, path=f'UW_image_{self._id}.png', data=uw_image)
152250
print(f'[{self._name}] [{self._id}] Rendered image saved to {self._writing_backend.output_dir}')
251+
if self._enable_ros2_pub:
252+
self._ros2_publish_uw_img(uw_image)
253+
pass
153254

154255
self._id += 1
155256

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"asset_path": "/home/jingwei/jingwei_ssd/OceanSim_assets"
2+
"asset_path": "/home/jingwei/OceanSim_assets"
33
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import rclpy
2+
from rclpy.node import Node
3+
from sensor_msgs.msg import CompressedImage
4+
import cv2
5+
import numpy as np
6+
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
7+
8+
class ImageSubscriber(Node):
9+
def __init__(self):
10+
super().__init__('image_subscriber')
11+
12+
qos = QoSProfile(
13+
reliability=ReliabilityPolicy.RELIABLE,
14+
history=HistoryPolicy.KEEP_LAST,
15+
depth=1
16+
)
17+
18+
self.subscription = self.create_subscription(
19+
CompressedImage,
20+
'/oceansim/robot/uw_img',
21+
self.image_callback,
22+
qos)
23+
24+
self.get_logger().info('Compressed Image Subscriber Node has been started')
25+
26+
def image_callback(self, msg):
27+
try:
28+
# debug
29+
self.get_logger().info(f'Received compressed image: format={msg.format}, '
30+
f'data_size={len(msg.data)}')
31+
32+
# convert
33+
np_arr = np.frombuffer(msg.data, np.uint8)
34+
# decode jpeg img
35+
cv_image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR) # return BGR format
36+
37+
if cv_image is None:
38+
self.get_logger().error('Failed to decode compressed image')
39+
return
40+
41+
# debug
42+
self.get_logger().info(f'Image shape: {cv_image.shape}, dtype: {cv_image.dtype}, '
43+
f'min: {cv_image.min()}, max: {cv_image.max()}')
44+
45+
# imshow
46+
cv2.imshow('Underwater Image', cv_image)
47+
cv2.waitKey(1)
48+
49+
except Exception as e:
50+
self.get_logger().error(f'Error processing compressed image: {str(e)}')
51+
52+
def main(args=None):
53+
rclpy.init(args=args)
54+
image_subscriber = ImageSubscriber()
55+
try:
56+
rclpy.spin(image_subscriber)
57+
except KeyboardInterrupt:
58+
pass
59+
finally:
60+
cv2.destroyAllWindows()
61+
image_subscriber.destroy_node()
62+
rclpy.shutdown()
63+
64+
if __name__ == '__main__':
65+
main()

0 commit comments

Comments
 (0)