|
| 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