Skip to content

Commit bbbc0fe

Browse files
Alvvalenciaclaude
andcommitted
feat(pj_base)!: carry frame_id through the canonical Image schema
Image was the only canonical builtin lacking frame_id: a producer could not tell a consumer which CameraInfo (calibration / native resolution / rectification) an image belongs to. Every sibling codec already serializes frame_id; Image silently dropped it on any serialized path. - Image.proto: append field 10 (string frame_id). - image_codec: serialize/deserialize frame_id. - image_codec_test: populated round-trip + empty default. The wire change alone is forward/backward compatible (an old reader skips the unknown field). The break is the struct: sdk::Image gains a member, so its layout changes, and Image crosses the plugin/host boundary by value inside BuiltinObject (std::any) -- an old-built plugin and a new host then disagree on its layout. That is an ABI break => MAJOR per the SDK versioning policy; the first break since pre-1.0, so bump 0.6.0 -> 1.0.0. baseline.abi is unchanged: a header struct field exports no ELF symbols. BREAKING CHANGE: plotjuggler_sdk 1.0.0; consumers pin [>=1.0.0 <2.0.0]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 90133ad commit bbbc0fe

7 files changed

Lines changed: 22 additions & 3 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ endif()
111111
if(PJ_INSTALL_SDK)
112112
include(CMakePackageConfigHelpers)
113113

114-
set(PJ_PACKAGE_VERSION "0.6.0")
114+
set(PJ_PACKAGE_VERSION "1.0.0")
115115
set(PJ_PACKAGE_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/plotjuggler_sdk)
116116

117117
install(EXPORT plotjuggler_sdkTargets

conanfile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
plugin_sdk — umbrella for plugin authors (base + dialog SDK + parser SDK)
77
plugin_host — umbrella for host loaders (data_source/parser/toolbox/dialog)
88
9-
A consuming Conan recipe declares e.g. `plotjuggler_sdk/0.6.0` and then:
9+
A consuming Conan recipe declares e.g. `plotjuggler_sdk/1.0.0` and then:
1010
1111
find_package(plotjuggler_sdk REQUIRED COMPONENTS plugin_sdk)
1212
target_link_libraries(my_plugin PRIVATE plotjuggler_sdk::plugin_sdk)
@@ -30,7 +30,7 @@
3030

3131
class PlotjugglerSdkConan(ConanFile):
3232
name = "plotjuggler_sdk"
33-
version = "0.6.0"
33+
version = "1.0.0"
3434
# Apache-2.0 covers the whole SDK (pj_base + pj_plugins). See LICENSE.
3535
license = "Apache-2.0"
3636
url = "https://github.com/PlotJuggler/plotjuggler_sdk"

docs/builtin_type.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ messages.
150150
| `anchor` | `BufferAnchor` | Keeps `data` alive when it references shared storage. |
151151
| `compressed_depth_min` | `std::optional<float>` | ROS compressed-depth quantization metadata, when present. |
152152
| `compressed_depth_max` | `std::optional<float>` | ROS compressed-depth quantization metadata, when present. |
153+
| `frame_id` | `std::string` | Source coordinate frame (ROS `sensor_msgs/Image` and `foxglove.CompressedImage` both carry it). Lets a consumer match the image to the `CameraInfo` of the same `frame_id` (calibration / native resolution), e.g. to rectify lens distortion so 2D annotations align with the image. Empty when the producer has no frame information. |
153154

154155
Common raw encodings are `rgb8`, `rgba8`, `bgr8`, `bgra8`, `mono8`, and
155156
`mono16`. Common compressed encodings are `jpeg`, `png`, and `qoi`.

pj_base/include/pj_base/builtin/image.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,13 @@ struct Image {
150150
std::optional<float> compressed_depth_max;
151151

152152
Timestamp timestamp_ns = 0;
153+
154+
/// Source coordinate frame (ROS sensor_msgs/Image and foxglove.CompressedImage
155+
/// both carry it). Lets a consumer match the image to the CameraInfo of the same
156+
/// frame_id (calibration / native resolution), e.g. to rectify lens distortion so
157+
/// 2D annotations align with the image. Empty when the producer has no frame
158+
/// information.
159+
std::string frame_id;
153160
};
154161

155162
} // namespace sdk

pj_base/proto/pj/Image.proto

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,9 @@ message Image {
5757
// ROS compressedDepth quantization maximum; set together with `compressed_depth_min` when `encoding ==
5858
// "compressedDepth"`.
5959
optional float compressed_depth_max = 9;
60+
61+
// Source coordinate frame (ROS sensor_msgs/Image, foxglove.CompressedImage). Lets a consumer match the image to the
62+
// CameraInfo of the same frame_id (calibration / native resolution), e.g. to rectify lens distortion so 2D
63+
// annotations align with the image. Empty when the producer has no frame information.
64+
string frame_id = 10;
6065
}

pj_base/src/builtin/image_codec.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ std::vector<uint8_t> serializeImage(const Image& image) {
5353
if (image.compressed_depth_max.has_value()) {
5454
writer.floatField(9, *image.compressed_depth_max);
5555
}
56+
writer.string(10, image.frame_id);
5657

5758
return out;
5859
}
@@ -139,6 +140,8 @@ Expected<sdk::Image> deserializeImage(const uint8_t* data, size_t size) {
139140
image.compressed_depth_max = v;
140141
return true;
141142
}
143+
case 10:
144+
return tag.type == WireType::kLengthDelimited && r.readString(image.frame_id);
142145
default:
143146
return false;
144147
}

pj_base/tests/image_codec_test.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ TEST(ImageCodecTest, RoundTripRawRGB8) {
3333
in.encoding = "rgb8";
3434
in.row_step = 6; // 2 px * 3 bytes
3535
in.is_bigendian = false;
36+
in.frame_id = "camera_front";
3637
const std::vector<uint8_t> pixels = {255, 0, 0, 0, 255, 0, 0, 0, 255, 128, 128, 128};
3738
in.data = Span<const uint8_t>(pixels.data(), pixels.size());
3839

@@ -45,6 +46,7 @@ TEST(ImageCodecTest, RoundTripRawRGB8) {
4546
EXPECT_EQ(out->encoding, in.encoding);
4647
EXPECT_EQ(out->row_step, in.row_step);
4748
EXPECT_FALSE(out->is_bigendian);
49+
EXPECT_EQ(out->frame_id, "camera_front");
4850
EXPECT_FALSE(out->compressed_depth_min.has_value());
4951
EXPECT_FALSE(out->compressed_depth_max.has_value());
5052
ASSERT_EQ(out->data.size(), pixels.size());
@@ -70,6 +72,7 @@ TEST(ImageCodecTest, RoundTripCompressedDepthWithRange) {
7072
ASSERT_TRUE(out->compressed_depth_max.has_value());
7173
EXPECT_FLOAT_EQ(*out->compressed_depth_min, 0.5f);
7274
EXPECT_FLOAT_EQ(*out->compressed_depth_max, 10.0f);
75+
EXPECT_TRUE(out->frame_id.empty()); // unset frame_id round-trips as empty
7376
}
7477

7578
} // namespace

0 commit comments

Comments
 (0)