Skip to content

Commit 23b3e21

Browse files
JArmandoAnayajoel-mbcsonthomisi
authored
feat(ros2): [3/8] unify camera publishers under CarlaCameraPublisher (carla-simulator#9746)
* feat(ros2): unify camera publishers under CarlaCameraPublisher Lands the camera-image slice of upstream ROS2 Refactor cd3d640 on ue5-dev, plus the K-matrix correctness fix from 86ebadc and the <cmath> include from 02a83ef: * New CarlaCameraPublisher.{h,cpp} base inherits BasePublisher (from PR-2) and owns the sensor_msgs/Image + sensor_msgs/CameraInfo DataWriter pair via PublisherImpl<>. CameraIntrinsics.h hosts the pinhole-camera intrinsics math (fx = width / (2 * tan(fov * pi / 360))) in a header-only seam so the K matrix can be exercised by a GTest without linking FastDDS. The PublisherImpl<> forward declarations in the header keep <fastdds/...> includes out of carla-server's ROS2.cpp compile unit, same pattern PR-2 uses for AckermannControlSubscriber. * CarlaRGBCameraPublisher collapses to a 24-LOC header subclass with GetChannels() = 4 and GetEncoding() = "bgra8". CarlaDepth, CarlaSS, CarlaIS, CarlaNormals camera publishers become `using` aliases of CarlaRGBCameraPublisher (their wire format is uniform 4-channel BGRA uint8 on ue5-dev today: encoding "bgra8", step = width * 4). Their per-sensor .cpp files are deleted. * CarlaOpticalFlowCameraPublisher stays as a non-alias subclass that overrides ComputeImage to reinterpret the post-header buffer as const float * and run the HSV colour-wheel encoding. The encoding math is lifted out of the publisher into a header-only seam OpticalFlowEncoding.h so the GTest can exercise it without FastDDS. This intentionally diverges from ue4-dev (which aliased OpticalFlow to RGB and shipped raw float bytes as BGRA); preserving the encoded form keeps the topic visually useful for ROS 2 subscribers consuming ue5-dev today. * ROS2.cpp gains a private GetOrCreateCameraSensor<CameraT> template that replaces the six per-camera switch cases in GetOrCreateSensor. The five BGRA8 camera dispatches in ProcessDataFromCamera collapse to one shared block; OpticalFlow keeps its own dispatch because the buffer cast differs. A parallel _camera_publishers map tracks CarlaCameraPublisher-derived publishers (which do not inherit the legacy CarlaPublisher base); RemoveActorRosName clears it alongside _publishers and _transforms. * CarlaLineInvasionPublisher.{h,cpp} and the four CarlaLineInvasion types files are deleted along with their three references in ROS2.cpp (include, make_shared, dynamic_pointer_cast). The publisher was wired in but had no Unreal-plugin call sites on ue5-dev. * DVS publisher migration is intentionally deferred to PR-4. The unified CarlaDVSPublisher composite needs a CarlaPointCloudPublisher base for its event-cloud half, and that base is introduced in PR-4 alongside the lidar/radar/semantic-lidar migration. Test coverage: * LibCarla/source/test/common/test_camera_intrinsics.cpp exercises ComputeIntrinsics against known FOV/resolution combinations and guards against the pre-86ebadcee operator-precedence regression (the old spelling applied M_PI / 360 to tan(fov) instead of to fov, returning ~0.06 for a 90deg FOV instead of the correct 960). * LibCarla/source/test/common/test_optical_flow_encoding.cpp exercises EncodeFlowPixelToBgra and EncodeFlowImageToBgra with 7 cases covering zero flow, hue hemispheres, brightness clamp, alpha-byte invariant, and buffer length. (adapted from ue4-dev cd3d640) (adapted from ue4-dev 86ebadc) Co-Authored-By: joel-mb <joel.moriana@gmail.com> Co-Authored-By: csonthomisi <48515100+csonthomisi@users.noreply.github.com> * fix(ros2): address Copilot review on PR carla-simulator#9746 Round-1 follow-up on top of 30b5c8e addressing the four inline comments left by GitHub Copilot: * test_optical_flow_encoding.cpp: add an explicit <algorithm> include so std::max({...}) does not rely on a transitive pull from the production header. * publishers/OpticalFlowEncoding.h: rewrite the saturation-curve comment. With shift = 0.999 and a = 1 / log(0.1 + shift) the intensity reaches 1 at magnitude ~= 0.1 (not at unit magnitude as the prior comment claimed). Math is unchanged; ue5-dev's existing wire format is preserved. * publishers/PublisherImpl.h: null-guard Publish(). If Init() failed (or was never called), the previous code dereferenced _datawriter and crashed; now it logs once and returns false. * publishers/CarlaCameraPublisher.cpp: log if either PublisherImpl<>::Init fails inside the constructor instead of silently dropping the return value. Pairs with the PublisherImpl guard above. * Actor/ActorDispatcher.cpp: drop the redundant RemoveActorRosName call after RemoveActorCallback. RemoveActorCallback already routes to UnregisterVehicle which clears the ros_name maps, so the explicit second call was a no-op. Comment updated to reflect the actual cleanup chain. No CHANGELOG entry (consolidated in PR-12). * fix(ros2): address Copilot round-2 review on PR carla-simulator#9746 Round-2 follow-up on top of 0dbc1ef addressing the three new inline comments from the post-round-1 Copilot pass: * publishers/CarlaCameraPublisher.cpp: Publish() now calls the image and camera_info writers unconditionally and combines the results. The previous "_impl_camera_info->Publish() && _impl_image->Publish()" form short-circuited the image publish whenever the camera_info publish failed; the new form preserves the per-sensor publishers' behaviour and ships the image regardless of header-writer state. * ros2/ROS2CallbackData.h: explicit #include <cstdint>. VehicleControl declares an int32_t gear field but the header only pulled <variant> and <functional>. Avoids relying on transitive includes. * ros2/ROS2.h: explicit #include <string>. Many public APIs take std::string arguments; the header previously pulled it transitively through <unordered_map> / <vector>. No CHANGELOG entry (consolidated in PR-12). * fix(ros2): enforce camera publisher type on cache hit (PR carla-simulator#9746 review) GetOrCreateCameraSensor cached the camera publisher by actor only, so a cache hit returned the existing publisher regardless of the requested CameraT. Under the current one-actor-one-camera-type invariant this is safe, but if an actor were ever dispatched as two camera types the first-created type would silently win and, for example, route optical flow float bytes through an RGB publisher. The cache-hit path now dynamic_pointer_cast<CameraT> the stored publisher. RGB/Depth/SS/IS/Normals all alias CarlaRGBCameraPublisher so a hit across them casts cleanly and is expected; only an RGB to OpticalFlow mismatch fails the cast, in which case the sample is logged and skipped (callers already guard on a null publisher) instead of corrupting the published image. Add test_camera_publisher_types.cpp to lock the type partitioning the guard relies on (the BGRA cameras collapse to one type, optical flow stays a distinct polymorphic sibling). The publisher constructors live in the FastDDS-linked native build and are not linked into the test binaries, so the test asserts the relationships at compile time via type traits. Add stack_cameras.json under PythonAPI/examples/ros2 spawning all six unified camera types for manual ROS 2 validation of the publisher path. * fix(ros2): rate-limit publisher Init-failure log to one-shot A failed Init() leaves the publisher cached, so Publish() runs once per frame for the sensor's lifetime. Log the uninitialized-DataWriter guard only once instead of every frame to avoid flooding the server log. --------- Co-authored-by: joel-mb <joel.moriana@gmail.com> Co-authored-by: csonthomisi <48515100+csonthomisi@users.noreply.github.com>
1 parent 0056362 commit 23b3e21

31 files changed

Lines changed: 948 additions & 4058 deletions

LibCarla/source/carla/ros2/ROS2.cpp

Lines changed: 128 additions & 261 deletions
Large diffs are not rendered by default.

LibCarla/source/carla/ros2/ROS2.h

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
#include "carla/ros2/ROS2CallbackData.h"
1313
#include "carla/streaming/detail/Types.h"
1414

15-
#include <unordered_set>
16-
#include <unordered_map>
1715
#include <memory>
16+
#include <string>
17+
#include <unordered_map>
18+
#include <unordered_set>
1819
#include <vector>
1920

2021
// forward declarations
@@ -38,6 +39,7 @@ namespace carla {
3839
namespace ros2 {
3940

4041
class CarlaPublisher;
42+
class CarlaCameraPublisher;
4143
class CarlaTransformPublisher;
4244
class CarlaClockPublisher;
4345
class BaseSubscriber;
@@ -162,6 +164,17 @@ void ProcessDataFromCollisionSensor(
162164
private:
163165
std::pair<std::shared_ptr<CarlaPublisher>, std::shared_ptr<CarlaTransformPublisher>> GetOrCreateSensor(int type, carla::streaming::detail::stream_id_type id, void* actor);
164166

167+
// Camera-side counterpart of GetOrCreateSensor for publishers that inherit
168+
// the new CarlaCameraPublisher base. Used by ProcessDataFromCamera for the
169+
// unified RGB/Depth/SS/IS/Normals/OpticalFlow path. DVS still uses the
170+
// legacy GetOrCreateSensor route until PR-4 introduces CarlaPointCloudPublisher.
171+
template <typename CameraT>
172+
std::pair<std::shared_ptr<CarlaCameraPublisher>, std::shared_ptr<CarlaTransformPublisher>>
173+
GetOrCreateCameraSensor(
174+
carla::streaming::detail::stream_id_type id,
175+
void *actor,
176+
std::string default_prefix);
177+
165178
// sigleton
166179
ROS2() {};
167180

@@ -175,6 +188,7 @@ void ProcessDataFromCollisionSensor(
175188
std::unordered_map<void *, std::vector<void*> > _actor_parent_ros_name;
176189
std::shared_ptr<CarlaClockPublisher> _clock_publisher;
177190
std::unordered_map<void *, std::shared_ptr<CarlaPublisher>> _publishers;
191+
std::unordered_map<void *, std::shared_ptr<CarlaCameraPublisher>> _camera_publishers;
178192
std::unordered_map<void *, std::shared_ptr<CarlaTransformPublisher>> _transforms;
179193
std::unordered_set<carla::streaming::detail::stream_id_type> _publish_stream;
180194
std::unordered_map<void *, ActorCallback> _actor_callbacks;

LibCarla/source/carla/ros2/ROS2CallbackData.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
#pragma once
88

9-
#include <variant>
9+
#include <cstdint>
1010
#include <functional>
11+
#include <variant>
1112

1213
namespace carla {
1314
namespace ros2 {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2+
// de Barcelona (UAB).
3+
//
4+
// This work is licensed under the terms of the MIT license.
5+
// For a copy, see <https://opensource.org/licenses/MIT>.
6+
7+
#pragma once
8+
9+
#include <cmath>
10+
#include <cstdint>
11+
12+
namespace carla {
13+
namespace ros2 {
14+
15+
struct CameraIntrinsics {
16+
double fx;
17+
double fy;
18+
double cx;
19+
double cy;
20+
};
21+
22+
// Builds a pinhole-camera intrinsic matrix from horizontal FOV (degrees) and
23+
// image dimensions (pixels):
24+
// fx = width / (2 * tan(fov_deg * pi / 360))
25+
// fy = fx (square pixels)
26+
// cx = width / 2
27+
// cy = height / 2
28+
//
29+
// This is the corrected form from upstream commit 86ebadcee. The pre-fix
30+
// spelling read `width / (2 * tan(fov) * M_PI / 360)` which applied the
31+
// degree-to-radian conversion to `tan(fov)` instead of to `fov`, producing a
32+
// nonsensical focal length.
33+
//
34+
// M_PI is used here, not std::numbers::pi_v, because this header is consumed
35+
// by the carla-ros2-native ExternalProject in Ros2Native/, which does not
36+
// configure CMAKE_CXX_STANDARD and therefore defaults to C++17 where <numbers>
37+
// is unavailable.
38+
[[nodiscard]] inline CameraIntrinsics ComputeIntrinsics(
39+
uint32_t width, uint32_t height, float fov_degrees) noexcept {
40+
const double w = static_cast<double>(width);
41+
const double h = static_cast<double>(height);
42+
const double fov_rad = static_cast<double>(fov_degrees) * M_PI / 360.0;
43+
const double fx = w / (2.0 * std::tan(fov_rad));
44+
return {fx, fx, w / 2.0, h / 2.0};
45+
}
46+
47+
} // namespace ros2
48+
} // namespace carla
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2+
// de Barcelona (UAB).
3+
//
4+
// This work is licensed under the terms of the MIT license.
5+
// For a copy, see <https://opensource.org/licenses/MIT>.
6+
7+
#include "carla/ros2/publishers/CarlaCameraPublisher.h"
8+
9+
#include "carla/Logging.h"
10+
#include "carla/ros2/publishers/CameraIntrinsics.h"
11+
#include "carla/ros2/publishers/PublisherImpl.h"
12+
#include "carla/ros2/types/CameraInfo.h"
13+
#include "carla/ros2/types/CameraInfoPubSubTypes.h"
14+
#include "carla/ros2/types/Image.h"
15+
#include "carla/ros2/types/ImagePubSubTypes.h"
16+
17+
#include <cmath>
18+
#include <utility>
19+
20+
namespace carla {
21+
namespace ros2 {
22+
23+
struct CarlaCameraImageMsgTraits {
24+
using msg_type = sensor_msgs::msg::Image;
25+
using msg_pubsub_type = sensor_msgs::msg::ImagePubSubType;
26+
};
27+
28+
struct CarlaCameraInfoMsgTraits {
29+
using msg_type = sensor_msgs::msg::CameraInfo;
30+
using msg_pubsub_type = sensor_msgs::msg::CameraInfoPubSubType;
31+
};
32+
33+
CarlaCameraPublisher::CarlaCameraPublisher(
34+
std::string base_topic_name, std::string frame_id)
35+
: BasePublisher(std::move(base_topic_name), std::move(frame_id)),
36+
_impl_image(std::make_shared<PublisherImpl<CarlaCameraImageMsgTraits>>()),
37+
_impl_camera_info(std::make_shared<PublisherImpl<CarlaCameraInfoMsgTraits>>()) {
38+
if (!_impl_image->Init(GetBaseTopicName() + "/image")) {
39+
log_error("CarlaCameraPublisher: failed to initialise image writer for", GetBaseTopicName());
40+
}
41+
if (!_impl_camera_info->Init(GetBaseTopicName() + "/camera_info")) {
42+
log_error("CarlaCameraPublisher: failed to initialise camera_info writer for", GetBaseTopicName());
43+
}
44+
}
45+
46+
CarlaCameraPublisher::~CarlaCameraPublisher() = default;
47+
48+
bool CarlaCameraPublisher::Publish() {
49+
// Publish both writers unconditionally so a failure on one does not silence
50+
// the other. Image goes first to match the previous per-sensor publishers.
51+
const bool image_ok = _impl_image->Publish();
52+
const bool info_ok = _impl_camera_info->Publish();
53+
return image_ok && info_ok;
54+
}
55+
56+
std::vector<uint8_t> CarlaCameraPublisher::ComputeImage(
57+
uint32_t height, uint32_t width, const uint8_t *data) const {
58+
const size_t size =
59+
static_cast<size_t>(height) * static_cast<size_t>(width) * GetChannels();
60+
return std::vector<uint8_t>(data, data + size);
61+
}
62+
63+
bool CarlaCameraPublisher::WriteCameraInfo(
64+
int32_t seconds,
65+
uint32_t nanoseconds,
66+
uint32_t x_offset,
67+
uint32_t y_offset,
68+
uint32_t height,
69+
uint32_t width,
70+
float fov,
71+
bool do_rectify) {
72+
auto *message = _impl_camera_info->GetMessage();
73+
message->header().stamp().sec(seconds);
74+
message->header().stamp().nanosec(nanoseconds);
75+
message->header().frame_id(GetFrameId());
76+
77+
const auto k = ComputeIntrinsics(width, height, fov);
78+
79+
message->height(height);
80+
message->width(width);
81+
message->distortion_model("plumb_bob");
82+
message->D({0.0, 0.0, 0.0, 0.0, 0.0});
83+
message->k({k.fx, 0.0, k.cx, 0.0, k.fy, k.cy, 0.0, 0.0, 1.0});
84+
message->r({1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0});
85+
message->p({k.fx, 0.0, k.cx, 0.0, 0.0, k.fy, k.cy, 0.0, 0.0, 0.0, 1.0, 0.0});
86+
message->binning_x(0);
87+
message->binning_y(0);
88+
89+
message->roi().x_offset(x_offset);
90+
message->roi().y_offset(y_offset);
91+
message->roi().height(height);
92+
message->roi().width(width);
93+
message->roi().do_rectify(do_rectify);
94+
95+
return true;
96+
}
97+
98+
bool CarlaCameraPublisher::WriteImage(
99+
int32_t seconds,
100+
uint32_t nanoseconds,
101+
uint32_t height,
102+
uint32_t width,
103+
const uint8_t *data) {
104+
return WriteImage(seconds, nanoseconds, height, width, ComputeImage(height, width, data));
105+
}
106+
107+
bool CarlaCameraPublisher::WriteImage(
108+
int32_t seconds,
109+
uint32_t nanoseconds,
110+
uint32_t height,
111+
uint32_t width,
112+
std::vector<uint8_t> data) {
113+
auto *message = _impl_image->GetMessage();
114+
message->header().stamp().sec(seconds);
115+
message->header().stamp().nanosec(nanoseconds);
116+
message->header().frame_id(GetFrameId());
117+
118+
message->width(width);
119+
message->height(height);
120+
message->encoding(GetEncoding());
121+
message->is_bigendian(0);
122+
message->step(width * GetChannels() * static_cast<uint32_t>(sizeof(uint8_t)));
123+
// https://github.com/eProsima/Fast-DDS/issues/2330
124+
message->data(std::move(data));
125+
126+
return true;
127+
}
128+
129+
} // namespace ros2
130+
} // namespace carla
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2+
// de Barcelona (UAB).
3+
//
4+
// This work is licensed under the terms of the MIT license.
5+
// For a copy, see <https://opensource.org/licenses/MIT>.
6+
7+
#pragma once
8+
9+
#include "carla/ros2/publishers/BasePublisher.h"
10+
11+
#include <cstdint>
12+
#include <memory>
13+
#include <string>
14+
#include <vector>
15+
16+
namespace carla {
17+
namespace ros2 {
18+
19+
// Forward declarations keep the FastDDS-heavy PublisherImpl<> definition out of
20+
// the main carla-server compile unit. The full template definition + Traits
21+
// instantiations live in CarlaCameraPublisher.cpp, which is built by the
22+
// carla-ros2-native ExternalProject (where FastDDS headers are on the include
23+
// path). Same pattern as AckermannControlSubscriber.{h,cpp} from PR-2.
24+
template <typename Traits> class PublisherImpl;
25+
struct CarlaCameraImageMsgTraits;
26+
struct CarlaCameraInfoMsgTraits;
27+
28+
class CarlaCameraPublisher : public BasePublisher {
29+
public:
30+
CarlaCameraPublisher(std::string base_topic_name, std::string frame_id);
31+
~CarlaCameraPublisher() override;
32+
33+
CarlaCameraPublisher(const CarlaCameraPublisher &) = delete;
34+
CarlaCameraPublisher &operator=(const CarlaCameraPublisher &) = delete;
35+
CarlaCameraPublisher(CarlaCameraPublisher &&) noexcept = default;
36+
CarlaCameraPublisher &operator=(CarlaCameraPublisher &&) noexcept = default;
37+
38+
bool Publish() override;
39+
40+
bool WriteCameraInfo(
41+
int32_t seconds,
42+
uint32_t nanoseconds,
43+
uint32_t x_offset,
44+
uint32_t y_offset,
45+
uint32_t height,
46+
uint32_t width,
47+
float fov,
48+
bool do_rectify);
49+
50+
bool WriteImage(
51+
int32_t seconds,
52+
uint32_t nanoseconds,
53+
uint32_t height,
54+
uint32_t width,
55+
const uint8_t *data);
56+
57+
protected:
58+
[[nodiscard]] virtual uint8_t GetChannels() const = 0;
59+
[[nodiscard]] virtual std::string GetEncoding() const = 0;
60+
[[nodiscard]] virtual std::vector<uint8_t> ComputeImage(
61+
uint32_t height, uint32_t width, const uint8_t *data) const;
62+
63+
private:
64+
bool WriteImage(
65+
int32_t seconds,
66+
uint32_t nanoseconds,
67+
uint32_t height,
68+
uint32_t width,
69+
std::vector<uint8_t> data);
70+
71+
std::shared_ptr<PublisherImpl<CarlaCameraImageMsgTraits>> _impl_image;
72+
std::shared_ptr<PublisherImpl<CarlaCameraInfoMsgTraits>> _impl_camera_info;
73+
};
74+
75+
} // namespace ros2
76+
} // namespace carla

0 commit comments

Comments
 (0)