Skip to content

Commit 0c321c0

Browse files
authored
Merge pull request #29 from eholum/camera-support
Adding basic RGB-D camera support
2 parents bd2d576 + ae410a5 commit 0c321c0

File tree

15 files changed

+861
-29
lines changed

15 files changed

+861
-29
lines changed

doc/index.rst

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,26 @@ Use ``mujoco_ros2_control/MujocoSystem`` for plugin
3333
</joint>
3434
</ros2_control>
3535
36-
Convert URDF model to xml
36+
Convert URDF model to XML
3737
--------------------------
38-
You need to convert the URDF model to a MJCF XML file.
38+
URDF models must be converted to `MJCF XML <https://mujoco.readthedocs.io/en/latest/modeling.html>`_ files.
3939
Make sure to use the same name for the link and joint, which are mapped to the body and joint in Mujoco.
40-
You need to specify <limit> which is mapped to ``range`` in MJCF. For now, there is no way to specify velocity or acceleration limit.
40+
You need to specify <limit> which is mapped to ``range`` in MJCF.
41+
For now, there is no way to specify velocity or acceleration limit.
4142

42-
For force torque sensor, you need to map the sensor to a force sensor and a torque sensor in MJCF since there is no combined force torque sensor in MuJoCo.
43-
The name of each sensor should be ``sensor_name`` + ``_force`` and ``sensor_name`` + ``_torque``.
44-
For example, if you have a force torque sensor called ``my_sensor``, you need to create ``my_sensor_force`` and ``my_sensor_torque`` in MJCF.
43+
For force torque sensors, there is no combined force torque sensor in MuJoCo.
44+
Therefore in ROS, a single force torque sensor must be mapped to both a force sensor and a torque sensor in MJCF.
45+
The name of each sensor must be ``sensor_name`` + ``_force`` and ``sensor_name`` + ``_torque``.
46+
For example, the mapped names for a ROS force torque sensor ``my_sensor`` would be ``my_sensor_force`` and ``my_sensor_torque`` in MJCF.
4547

46-
Check ``mujoco_ros2_control_demos/mujoco_models`` for examples.
48+
The drivers additionally support simulated RGB-D cameras for publishing simulated color images and depth maps.
49+
Cameras must be given a name and be attached to a joint called ``<name>_optical_frame``.
50+
The camera_info, color, and depth images will be published to topics called ``<name>/camera_info``,
51+
``<name>/color``, and ``<name>/depth``, repectively.
52+
Also note that MuJuCo's conventions for cameras are different than ROS's, and which must be accounted for.
53+
An overview is available through the "camera" demo.
54+
55+
For additional information refer to the ``mujoco_ros2_control_demos/mujoco_models`` for examples.
4756

4857
Specify the location of Mujoco models and the controller configuration file
4958
----------------------------------------------------------------------------

mujoco_ros2_control/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,12 @@ install(TARGETS
7171
)
7272

7373
# TODO: make it simple
74-
add_executable(mujoco_ros2_control src/mujoco_ros2_control_node.cpp src/mujoco_rendering.cpp src/mujoco_ros2_control.cpp)
74+
add_executable(mujoco_ros2_control
75+
src/mujoco_ros2_control_node.cpp
76+
src/mujoco_rendering.cpp
77+
src/mujoco_ros2_control.cpp
78+
src/mujoco_cameras.cpp
79+
)
7580
ament_target_dependencies(mujoco_ros2_control ${THIS_PACKAGE_DEPENDS})
7681
target_link_libraries(mujoco_ros2_control ${MUJOCO_LIB} glfw)
7782
target_include_directories(mujoco_ros2_control
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) 2025 Erik Holum
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16+
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
#pragma once
22+
23+
#include <string>
24+
#include <vector>
25+
26+
#include "GLFW/glfw3.h"
27+
#include "mujoco/mujoco.h"
28+
#include "rclcpp/rclcpp.hpp"
29+
30+
#include "rclcpp/node.hpp"
31+
#include "rclcpp/publisher.hpp"
32+
#include "sensor_msgs/msg/camera_info.hpp"
33+
#include "sensor_msgs/msg/image.hpp"
34+
35+
namespace mujoco_ros2_control
36+
{
37+
38+
struct CameraData
39+
{
40+
mjvCamera mjv_cam;
41+
mjrRect viewport;
42+
43+
std::string name;
44+
std::string frame_name;
45+
uint32_t width;
46+
uint32_t height;
47+
48+
std::vector<uint8_t> image_buffer;
49+
std::vector<float> depth_buffer;
50+
std::vector<float> depth_buffer_flipped;
51+
52+
sensor_msgs::msg::Image image;
53+
sensor_msgs::msg::Image depth_image;
54+
sensor_msgs::msg::CameraInfo camera_info;
55+
56+
rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr image_pub;
57+
rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr depth_image_pub;
58+
rclcpp::Publisher<sensor_msgs::msg::CameraInfo>::SharedPtr camera_info_pub;
59+
};
60+
61+
class MujocoCameras
62+
{
63+
public:
64+
explicit MujocoCameras(rclcpp::Node::SharedPtr &node);
65+
66+
void init(mjModel *mujoco_model);
67+
void update(mjModel *mujoco_model, mjData *mujoco_data);
68+
void close();
69+
70+
private:
71+
void register_cameras(const mjModel *mujoco_model);
72+
73+
rclcpp::Node::SharedPtr node_;
74+
75+
// Rendering options for the cameras, currently hard coded to defaults
76+
mjvOption mjv_opt_;
77+
mjvScene mjv_scn_;
78+
mjrContext mjr_con_;
79+
80+
// Containers for camera data and ROS constructs
81+
std::vector<CameraData> cameras_;
82+
};
83+
84+
} // namespace mujoco_ros2_control

mujoco_ros2_control/include/mujoco_ros2_control/mujoco_rendering.hpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,26 @@
2121
#ifndef MUJOCO_ROS2_CONTROL__MUJOCO_RENDERING_HPP_
2222
#define MUJOCO_ROS2_CONTROL__MUJOCO_RENDERING_HPP_
2323

24+
#include <string>
25+
#include <vector>
26+
2427
#include "GLFW/glfw3.h"
2528
#include "mujoco/mujoco.h"
2629
#include "rclcpp/rclcpp.hpp"
2730

31+
#include "rclcpp/node.hpp"
32+
2833
namespace mujoco_ros2_control
2934
{
35+
3036
class MujocoRendering
3137
{
3238
public:
3339
MujocoRendering(const MujocoRendering &obj) = delete;
3440
void operator=(const MujocoRendering &) = delete;
3541

3642
static MujocoRendering *get_instance();
37-
void init(rclcpp::Node::SharedPtr &node, mjModel *mujoco_model, mjData *mujoco_data);
43+
void init(mjModel *mujoco_model, mjData *mujoco_data);
3844
bool is_close_flag_raised();
3945
void update();
4046
void close();
@@ -45,22 +51,26 @@ class MujocoRendering
4551
static void mouse_button_callback(GLFWwindow *window, int button, int act, int mods);
4652
static void mouse_move_callback(GLFWwindow *window, double xpos, double ypos);
4753
static void scroll_callback(GLFWwindow *window, double xoffset, double yoffset);
54+
4855
void keyboard_callback_impl(GLFWwindow *window, int key, int scancode, int act, int mods);
4956
void mouse_button_callback_impl(GLFWwindow *window, int button, int act, int mods);
5057
void mouse_move_callback_impl(GLFWwindow *window, double xpos, double ypos);
5158
void scroll_callback_impl(GLFWwindow *window, double xoffset, double yoffset);
5259

5360
static MujocoRendering *instance_;
54-
rclcpp::Node::SharedPtr node_; // TODO(sangtaek): delete node and add logger
61+
5562
mjModel *mj_model_;
5663
mjData *mj_data_;
64+
65+
// Window and primary camera for the simulation's viewer
66+
GLFWwindow *window_;
5767
mjvCamera mjv_cam_;
68+
69+
// Options for the rendering context and scene, all of these are hard coded to defaults.
5870
mjvOption mjv_opt_;
5971
mjvScene mjv_scn_;
6072
mjrContext mjr_con_;
6173

62-
GLFWwindow *window_;
63-
6474
bool button_left_;
6575
bool button_middle_;
6676
bool button_right_;
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright (c) 2025 Erik Holum
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16+
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
#include "mujoco_ros2_control/mujoco_cameras.hpp"
22+
23+
#include "sensor_msgs/image_encodings.hpp"
24+
25+
namespace mujoco_ros2_control
26+
{
27+
28+
MujocoCameras::MujocoCameras(rclcpp::Node::SharedPtr &node) : node_(node) {}
29+
30+
void MujocoCameras::init(mjModel *mujoco_model)
31+
{
32+
// initialize visualization data structures
33+
mjv_defaultOption(&mjv_opt_);
34+
mjv_defaultScene(&mjv_scn_);
35+
mjr_defaultContext(&mjr_con_);
36+
37+
// create scene and context
38+
mjv_makeScene(mujoco_model, &mjv_scn_, 2000);
39+
mjr_makeContext(mujoco_model, &mjr_con_, mjFONTSCALE_150);
40+
41+
// Add user cameras
42+
register_cameras(mujoco_model);
43+
}
44+
45+
void MujocoCameras::update(mjModel *mujoco_model, mjData *mujoco_data)
46+
{
47+
// Rendering is done offscreen
48+
mjr_setBuffer(mjFB_OFFSCREEN, &mjr_con_);
49+
50+
for (auto &camera : cameras_)
51+
{
52+
// Render simple RGB data for all cameras
53+
mjv_updateScene(
54+
mujoco_model, mujoco_data, &mjv_opt_, NULL, &camera.mjv_cam, mjCAT_ALL, &mjv_scn_);
55+
mjr_render(camera.viewport, &mjv_scn_, &mjr_con_);
56+
57+
// Copy image into relevant buffers
58+
mjr_readPixels(
59+
camera.image_buffer.data(), camera.depth_buffer.data(), camera.viewport, &mjr_con_);
60+
61+
// Fix non-linear projections in the depth image and flip the data.
62+
// https://github.com/google-deepmind/mujoco/blob/3.2.7/python/mujoco/renderer.py#L190
63+
float near = static_cast<float>(mujoco_model->vis.map.znear * mujoco_model->stat.extent);
64+
float far = static_cast<float>(mujoco_model->vis.map.zfar * mujoco_model->stat.extent);
65+
for (uint32_t h = 0; h < camera.height; h++)
66+
{
67+
for (uint32_t w = 0; w < camera.width; w++)
68+
{
69+
auto idx = h * camera.width + w;
70+
auto idx_flipped = (camera.height - 1 - h) * camera.width + w;
71+
camera.depth_buffer[idx] = near / (1.0f - camera.depth_buffer[idx] * (1.0f - near / far));
72+
camera.depth_buffer_flipped[idx_flipped] = camera.depth_buffer[idx];
73+
}
74+
}
75+
// Copy flipped data into the depth image message, floats -> unsigned chars
76+
std::memcpy(
77+
&camera.depth_image.data[0], camera.depth_buffer_flipped.data(),
78+
camera.depth_image.data.size());
79+
80+
// OpenGL's coordinate system's origin is in the bottom left, so we invert the images row-by-row
81+
auto row_size = camera.width * 3;
82+
for (uint32_t h = 0; h < camera.height; h++)
83+
{
84+
auto src_idx = h * row_size;
85+
auto dest_idx = (camera.height - 1 - h) * row_size;
86+
std::memcpy(&camera.image.data[dest_idx], &camera.image_buffer[src_idx], row_size);
87+
}
88+
89+
// Publish images and camera info
90+
auto time = node_->now();
91+
camera.image.header.stamp = time;
92+
camera.depth_image.header.stamp = time;
93+
camera.camera_info.header.stamp = time;
94+
95+
camera.image_pub->publish(camera.image);
96+
camera.depth_image_pub->publish(camera.depth_image);
97+
camera.camera_info_pub->publish(camera.camera_info);
98+
}
99+
}
100+
101+
void MujocoCameras::close()
102+
{
103+
mjv_freeScene(&mjv_scn_);
104+
mjr_freeContext(&mjr_con_);
105+
}
106+
107+
void MujocoCameras::register_cameras(const mjModel *mujoco_model)
108+
{
109+
cameras_.resize(0);
110+
for (auto i = 0; i < mujoco_model->ncam; ++i)
111+
{
112+
const char *cam_name = mujoco_model->names + mujoco_model->name_camadr[i];
113+
const int *cam_resolution = mujoco_model->cam_resolution + 2 * i;
114+
const mjtNum cam_fovy = mujoco_model->cam_fovy[i];
115+
116+
// Construct CameraData wrapper and set defaults
117+
CameraData camera;
118+
camera.name = cam_name;
119+
camera.mjv_cam.type = mjCAMERA_FIXED;
120+
camera.mjv_cam.fixedcamid = i;
121+
camera.width = static_cast<uint32_t>(cam_resolution[0]);
122+
camera.height = static_cast<uint32_t>(cam_resolution[1]);
123+
camera.viewport = {0, 0, cam_resolution[0], cam_resolution[1]};
124+
125+
// TODO(eholum): Ensure that the camera is attached to the expected pose.
126+
// For now assume that's the case.
127+
camera.frame_name = camera.name + "_optical_frame";
128+
129+
// Configure publishers
130+
camera.image_pub = node_->create_publisher<sensor_msgs::msg::Image>(camera.name + "/color", 1);
131+
camera.depth_image_pub =
132+
node_->create_publisher<sensor_msgs::msg::Image>(camera.name + "/depth", 1);
133+
camera.camera_info_pub =
134+
node_->create_publisher<sensor_msgs::msg::CameraInfo>(camera.name + "/camera_info", 1);
135+
136+
// Setup contaienrs for color image data
137+
camera.image.header.frame_id = camera.frame_name;
138+
139+
const auto image_size = camera.width * camera.height * 3;
140+
camera.image_buffer.resize(image_size);
141+
camera.image.data.resize(image_size);
142+
camera.image.width = camera.width;
143+
camera.image.height = camera.height;
144+
camera.image.step = camera.width * 3;
145+
camera.image.encoding = sensor_msgs::image_encodings::RGB8;
146+
147+
// Depth image data
148+
camera.depth_image.header.frame_id = camera.frame_name;
149+
camera.depth_buffer.resize(camera.width * camera.height);
150+
camera.depth_buffer_flipped.resize(camera.width * camera.height);
151+
camera.depth_image.data.resize(camera.width * camera.height * sizeof(float));
152+
camera.depth_image.width = camera.width;
153+
camera.depth_image.height = camera.height;
154+
camera.depth_image.step = camera.width * sizeof(float);
155+
camera.depth_image.encoding = sensor_msgs::image_encodings::TYPE_32FC1;
156+
157+
// Camera info
158+
camera.camera_info.header.frame_id = camera.frame_name;
159+
camera.camera_info.width = camera.width;
160+
camera.camera_info.height = camera.height;
161+
camera.camera_info.distortion_model = "plumb_bob";
162+
camera.camera_info.k.fill(0.0);
163+
camera.camera_info.r.fill(0.0);
164+
camera.camera_info.p.fill(0.0);
165+
camera.camera_info.d.resize(5, 0.0);
166+
167+
double focal_scaling = (1.0 / std::tan((cam_fovy * M_PI / 180.0) / 2.0)) * camera.height / 2.0;
168+
camera.camera_info.k[0] = camera.camera_info.p[0] = focal_scaling;
169+
camera.camera_info.k[2] = camera.camera_info.p[2] = static_cast<double>(camera.width) / 2.0;
170+
camera.camera_info.k[4] = camera.camera_info.p[5] = focal_scaling;
171+
camera.camera_info.k[5] = camera.camera_info.p[6] = static_cast<double>(camera.height) / 2.0;
172+
camera.camera_info.k[8] = camera.camera_info.p[10] = 1.0;
173+
174+
// Add to list of cameras
175+
cameras_.push_back(camera);
176+
}
177+
}
178+
179+
} // namespace mujoco_ros2_control

0 commit comments

Comments
 (0)