From 582f2fdf4e48b4e954e1872c12ee1d4f400189ee Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Wed, 18 Oct 2023 11:27:22 +0200 Subject: [PATCH] C++ examples & roundtrip tests for image/segmentation-image/depth-image/tensor (#3899) ### What * Fixes #3380 * Part of #2919 commit history is a bit messy because this was based on the annotation context example pr. Speaking off, I made those a bit nicer on the way ### Checklist * [x] I have read and agree to [Contributor Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and the [Code of Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md) * [x] I've included a screenshot or gif (if applicable) * [x] I have tested [demo.rerun.io](https://demo.rerun.io/pr/3899) (if applicable) * [x] The PR title and labels are set such as to maximize their usefulness for the next release's CHANGELOG - [PR Build Summary](https://build.rerun.io/pr/3899) - [Docs preview](https://rerun.io/preview/c324163602dfdde33684cae0f9365fd72b177ad3/docs) - [Examples preview](https://rerun.io/preview/c324163602dfdde33684cae0f9365fd72b177ad3/examples) - [Recent benchmark results](https://ref.rerun.io/dev/bench/) - [Wasm size tracking](https://ref.rerun.io/dev/sizes/) --- .../rerun/archetypes/depth_image.fbs | 3 +- .../definitions/rerun/archetypes/image.fbs | 3 +- crates/re_types/src/archetypes/image_ext.rs | 2 +- crates/re_types/src/archetypes/tensor.rs | 3 +- crates/re_types/src/archetypes/tensor_ext.rs | 2 +- .../annotation_context_segmentation.cpp | 17 +-- docs/code-examples/depth_image_3d.cpp | 35 +++++ docs/code-examples/depth_image_simple.cpp | 23 ++++ docs/code-examples/image_simple.cpp | 25 ++++ docs/code-examples/roundtrips.py | 5 - .../segmentation_image_simple.cpp | 32 +++++ docs/code-examples/tensor_simple.cpp | 21 +++ docs/code-examples/tensor_simple.rs | 3 +- .../rerun/archetypes/annotation_context.hpp | 17 +-- .../src/rerun/archetypes/depth_image.hpp | 60 ++++++++- .../src/rerun/archetypes/depth_image_ext.cpp | 45 +++++++ rerun_cpp/src/rerun/archetypes/image.hpp | 48 ++++++- rerun_cpp/src/rerun/archetypes/image_ext.cpp | 60 +++++++++ .../rerun/archetypes/segmentation_image.hpp | 51 ++++++- .../archetypes/segmentation_image_ext.cpp | 18 ++- rerun_cpp/src/rerun/archetypes/tensor.hpp | 46 +++++++ rerun_cpp/src/rerun/archetypes/tensor_ext.cpp | 54 ++++++++ .../src/rerun/datatypes/tensor_buffer.hpp | 31 +++-- .../src/rerun/datatypes/tensor_buffer_ext.cpp | 31 +++-- rerun_cpp/tests/archetypes/image.cpp | 127 ++++++++++++++++++ .../segmentation_and_depth_image.cpp | 107 +++++++++++++++ .../tests/archetypes/segmentation_image.cpp | 111 --------------- tests/cpp/roundtrips/depth_image/main.cpp | 13 ++ tests/cpp/roundtrips/image/main.cpp | 53 ++++++++ .../roundtrips/segmentation_image/main.cpp | 14 ++ tests/cpp/roundtrips/tensor/main.cpp | 16 +-- tests/python/roundtrips/image/main.py | 4 +- tests/roundtrips.py | 4 - tests/rust/roundtrips/image/src/main.rs | 4 +- 34 files changed, 893 insertions(+), 195 deletions(-) create mode 100644 docs/code-examples/depth_image_3d.cpp create mode 100644 docs/code-examples/depth_image_simple.cpp create mode 100644 docs/code-examples/image_simple.cpp create mode 100644 docs/code-examples/segmentation_image_simple.cpp create mode 100644 docs/code-examples/tensor_simple.cpp create mode 100644 rerun_cpp/src/rerun/archetypes/depth_image_ext.cpp create mode 100644 rerun_cpp/src/rerun/archetypes/image_ext.cpp create mode 100644 rerun_cpp/src/rerun/archetypes/tensor_ext.cpp create mode 100644 rerun_cpp/tests/archetypes/image.cpp create mode 100644 rerun_cpp/tests/archetypes/segmentation_and_depth_image.cpp delete mode 100644 rerun_cpp/tests/archetypes/segmentation_image.cpp create mode 100644 tests/cpp/roundtrips/depth_image/main.cpp create mode 100644 tests/cpp/roundtrips/image/main.cpp create mode 100644 tests/cpp/roundtrips/segmentation_image/main.cpp diff --git a/crates/re_types/definitions/rerun/archetypes/depth_image.fbs b/crates/re_types/definitions/rerun/archetypes/depth_image.fbs index 1a6519ec0f7e..45b07cd14ca4 100644 --- a/crates/re_types/definitions/rerun/archetypes/depth_image.fbs +++ b/crates/re_types/definitions/rerun/archetypes/depth_image.fbs @@ -14,7 +14,8 @@ namespace rerun.archetypes; /// \example depth_image_simple !api title="Simple example" image="https://static.rerun.io/depth_image_simple/9598554977873ace2577bddd79184ac120ceb0b0/1200w.png" /// \example depth_image_3d title="Depth to 3D example" image="https://static.rerun.io/depth_image_3d/f78674bdae0eb25786c6173307693c5338f38b87/1200w.png" table DepthImage ( - "attr.rust.derive": "PartialEq" + "attr.rust.derive": "PartialEq", + "attr.cpp.no_field_ctors" ) { // --- Required --- diff --git a/crates/re_types/definitions/rerun/archetypes/image.fbs b/crates/re_types/definitions/rerun/archetypes/image.fbs index 6ea18ea89a55..ed811bda9333 100644 --- a/crates/re_types/definitions/rerun/archetypes/image.fbs +++ b/crates/re_types/definitions/rerun/archetypes/image.fbs @@ -18,7 +18,8 @@ namespace rerun.archetypes; /// /// \example image_simple image="https://static.rerun.io/image_simple/06ba7f8582acc1ffb42a7fd0006fad7816f3e4e4/1200w.png" table Image ( - "attr.rust.derive": "PartialEq" + "attr.rust.derive": "PartialEq", + "attr.cpp.no_field_ctors" ) { // --- Required --- diff --git a/crates/re_types/src/archetypes/image_ext.rs b/crates/re_types/src/archetypes/image_ext.rs index 45f2bbd65ee2..3813191a2182 100644 --- a/crates/re_types/src/archetypes/image_ext.rs +++ b/crates/re_types/src/archetypes/image_ext.rs @@ -28,7 +28,7 @@ impl Image { assign_if_none(&mut data.shape[non_empty_dim_inds[1]].name, "width"); } 3 => match data.shape[non_empty_dim_inds[2]].size { - 3 | 4 => { + 1 | 3 | 4 => { assign_if_none(&mut data.shape[non_empty_dim_inds[0]].name, "height"); assign_if_none(&mut data.shape[non_empty_dim_inds[1]].name, "width"); assign_if_none(&mut data.shape[non_empty_dim_inds[2]].name, "depth"); diff --git a/crates/re_types/src/archetypes/tensor.rs b/crates/re_types/src/archetypes/tensor.rs index 9d1953425cb2..4ec45fa797e8 100644 --- a/crates/re_types/src/archetypes/tensor.rs +++ b/crates/re_types/src/archetypes/tensor.rs @@ -29,7 +29,8 @@ /// let mut data = Array::::default((8, 6, 3, 5).f()); /// data.map_inplace(|x| *x = rand::random()); /// -/// let tensor = rerun::Tensor::try_from(data)?.with_names(["batch", "channel", "height", "width"]); +/// let tensor = +/// rerun::Tensor::try_from(data)?.with_dim_names(["batch", "channel", "height", "width"]); /// rec.log("tensor", &tensor)?; /// /// rerun::native_viewer::show(storage.take())?; diff --git a/crates/re_types/src/archetypes/tensor_ext.rs b/crates/re_types/src/archetypes/tensor_ext.rs index 885182ae278b..7671c9f902e1 100644 --- a/crates/re_types/src/archetypes/tensor_ext.rs +++ b/crates/re_types/src/archetypes/tensor_ext.rs @@ -24,7 +24,7 @@ impl Tensor { /// /// If too many, or too few names are provided, this function will warn and only /// update the subset of names that it can. - pub fn with_names(self, names: impl IntoIterator>) -> Self { + pub fn with_dim_names(self, names: impl IntoIterator>) -> Self { let names: Vec<_> = names.into_iter().map(|x| Some(x.into())).collect(); if names.len() != self.data.0.shape.len() { re_log::warn_once!( diff --git a/docs/code-examples/annotation_context_segmentation.cpp b/docs/code-examples/annotation_context_segmentation.cpp index c8e0332587e1..e3e75e6f3bd7 100644 --- a/docs/code-examples/annotation_context_segmentation.cpp +++ b/docs/code-examples/annotation_context_segmentation.cpp @@ -2,6 +2,8 @@ #include +#include + int main() { auto rec = rerun::RecordingStream("rerun_example_annotation_context_connections"); rec.connect("127.0.0.1:9876").throw_on_failure(); @@ -19,17 +21,12 @@ int main() { const int HEIGHT = 8; const int WIDTH = 12; std::vector data(WIDTH * HEIGHT, 0); - for (auto y = 0; y < 4; ++y) { // top half - auto row = data.begin() + y * WIDTH; - std::fill(row, row + 6, 1); // left half + for (auto y = 0; y < 4; ++y) { // top half + std::fill_n(data.begin() + y * WIDTH, 6, 1); // left half } - for (auto y = 4; y < 8; ++y) { // bottom half - auto row = data.begin() + y * WIDTH; - std::fill(row + 6, row + 12, 2); // right half + for (auto y = 4; y < 8; ++y) { // bottom half + std::fill_n(data.begin() + y * WIDTH + 6, 6, 2); // right half } - rec.log( - "segmentation/image", - rerun::SegmentationImage(rerun::TensorData({HEIGHT, WIDTH}, std::move(data))) - ); + rec.log("segmentation/image", rerun::SegmentationImage({HEIGHT, WIDTH}, std::move(data))); } diff --git a/docs/code-examples/depth_image_3d.cpp b/docs/code-examples/depth_image_3d.cpp new file mode 100644 index 000000000000..028ad3a976f4 --- /dev/null +++ b/docs/code-examples/depth_image_3d.cpp @@ -0,0 +1,35 @@ +// Create and log a depth image. + +#include + +#include + +int main() { + auto rec = rerun::RecordingStream("rerun_example_depth_image"); + rec.connect("127.0.0.1:9876").throw_on_failure(); + + // Create a synthetic depth image. + const int HEIGHT = 8; + const int WIDTH = 12; + std::vector data(WIDTH * HEIGHT, 65535); + for (auto y = 0; y < 4; ++y) { // top half + std::fill_n(data.begin() + y * WIDTH, 6, 20000); // left half + } + for (auto y = 4; y < 8; ++y) { // bottom half + std::fill_n(data.begin() + y * WIDTH + 6, 6, 45000); // right half + } + + // If we log a pinhole camera model, the depth gets automatically back-projected to 3D + rec.log( + "world/camera", + rerun::Pinhole::focal_length_and_resolution( + {20.0f, 20.0f}, + {static_cast(WIDTH), static_cast(HEIGHT)} + ) + ); + + rec.log( + "world/camera/depth", + rerun::DepthImage({HEIGHT, WIDTH}, std::move(data)).with_meter(10000.0) + ); +} diff --git a/docs/code-examples/depth_image_simple.cpp b/docs/code-examples/depth_image_simple.cpp new file mode 100644 index 000000000000..f6a8abe27d96 --- /dev/null +++ b/docs/code-examples/depth_image_simple.cpp @@ -0,0 +1,23 @@ +// Create and log a depth image. + +#include + +#include + +int main() { + auto rec = rerun::RecordingStream("rerun_example_depth_image"); + rec.connect("127.0.0.1:9876").throw_on_failure(); + + // create a synthetic depth image. + const int HEIGHT = 8; + const int WIDTH = 12; + std::vector data(WIDTH * HEIGHT, 65535); + for (auto y = 0; y < 4; ++y) { // top half + std::fill_n(data.begin() + y * WIDTH, 6, 20000); // left half + } + for (auto y = 4; y < 8; ++y) { // bottom half + std::fill_n(data.begin() + y * WIDTH + 6, 6, 45000); // right half + } + + rec.log("depth", rerun::DepthImage({HEIGHT, WIDTH}, std::move(data)).with_meter(10000.0)); +} diff --git a/docs/code-examples/image_simple.cpp b/docs/code-examples/image_simple.cpp new file mode 100644 index 000000000000..093f010c0310 --- /dev/null +++ b/docs/code-examples/image_simple.cpp @@ -0,0 +1,25 @@ +// Create and log a image. + +#include + +int main() { + auto rec = rerun::RecordingStream("rerun_example_image_simple"); + rec.connect("127.0.0.1:9876").throw_on_failure(); + + // Create a synthetic image. + const int HEIGHT = 8; + const int WIDTH = 12; + std::vector data(WIDTH * HEIGHT * 3, 0); + for (size_t i = 0; i < data.size(); i += 3) { + data[i] = 255; + } + for (auto y = 0; y < 4; ++y) { // top half + auto row = data.begin() + y * WIDTH * 3; + for (auto i = 0; i < 6 * 3; i += 3) { // left half + row[i] = 0; + row[i + 1] = 255; + } + } + + rec.log("image", rerun::Image({HEIGHT, WIDTH, 3}, std::move(data))); +} diff --git a/docs/code-examples/roundtrips.py b/docs/code-examples/roundtrips.py index cfd38bf74fbd..fe8688fb2083 100755 --- a/docs/code-examples/roundtrips.py +++ b/docs/code-examples/roundtrips.py @@ -23,16 +23,11 @@ "any_values": ["cpp", "rust"], # Not yet implemented "asset3d_out_of_tree": ["cpp"], # TODO(#2919): Not yet implemented in C++ "custom_data": ["cpp"], # TODO(#2919): Not yet implemented in C++ - "depth_image_3d": ["cpp"], # TODO(#2919): Not yet implemented in C++ - "depth_image_simple": ["cpp"], # TODO(#2919): Not yet implemented in C++ "extra_values": ["cpp", "rust"], # Missing examples "image_advanced": ["cpp", "rust"], # Missing examples - "image_simple": ["cpp"], # TODO(#2919): Not yet implemented in C++ "log_line": ["cpp", "rust", "py"], # Not a complete example -- just a single log line "quick_start_connect": ["cpp"], # TODO(#3870): Not yet implemented in C++ "scalar_multiple_plots": ["cpp"], # TODO(#2919): Not yet implemented in C++ - "segmentation_image_simple": ["cpp"], # TODO(#2919): Not yet implemented in C++ - "tensor_simple": ["cpp"], # TODO(#2919): Not yet implemented in C++ "text_log_integration": ["cpp"], # TODO(#2919): Not yet implemented in C++ # This is this script, it's not an example. diff --git a/docs/code-examples/segmentation_image_simple.cpp b/docs/code-examples/segmentation_image_simple.cpp new file mode 100644 index 000000000000..1b10309e1eeb --- /dev/null +++ b/docs/code-examples/segmentation_image_simple.cpp @@ -0,0 +1,32 @@ +// Create and log a segmentation image. + +#include + +#include + +int main() { + auto rec = rerun::RecordingStream("rerun_example_annotation_context_connections"); + rec.connect("127.0.0.1:9876").throw_on_failure(); + + // Create a segmentation image + const int HEIGHT = 8; + const int WIDTH = 12; + std::vector data(WIDTH * HEIGHT, 0); + for (auto y = 0; y < 4; ++y) { // top half + std::fill_n(data.begin() + y * WIDTH, 6, 1); // left half + } + for (auto y = 4; y < 8; ++y) { // bottom half + std::fill_n(data.begin() + y * WIDTH + 6, 6, 2); // right half + } + + // create an annotation context to describe the classes + rec.log_timeless( + "/", + rerun::AnnotationContext({ + rerun::AnnotationInfo(1, "red", rerun::Rgba32(255, 0, 0)), + rerun::AnnotationInfo(2, "green", rerun::Rgba32(0, 255, 0)), + }) + ); + + rec.log("image", rerun::SegmentationImage({HEIGHT, WIDTH}, std::move(data))); +} diff --git a/docs/code-examples/tensor_simple.cpp b/docs/code-examples/tensor_simple.cpp new file mode 100644 index 000000000000..00738620b3d3 --- /dev/null +++ b/docs/code-examples/tensor_simple.cpp @@ -0,0 +1,21 @@ +// Create and log a tensor. + +#include + +#include + +int main() { + auto rec = rerun::RecordingStream("rerun_example_tensor_simple"); + rec.connect("127.0.0.1:9876").throw_on_failure(); + + std::default_random_engine gen; + std::uniform_int_distribution dist(0, 255); + + std::vector data(8 * 6 * 3 * 5); + std::generate(data.begin(), data.end(), [&] { return dist(gen); }); + + rec.log( + "tensor", + rerun::Tensor({8, 6, 3, 5}, data).with_dim_names({"batch", "channel", "height", "width"}) + ); +} diff --git a/docs/code-examples/tensor_simple.rs b/docs/code-examples/tensor_simple.rs index 94b6a980d6dc..3874f5a4167e 100644 --- a/docs/code-examples/tensor_simple.rs +++ b/docs/code-examples/tensor_simple.rs @@ -9,7 +9,8 @@ fn main() -> Result<(), Box> { let mut data = Array::::default((8, 6, 3, 5).f()); data.map_inplace(|x| *x = rand::random()); - let tensor = rerun::Tensor::try_from(data)?.with_names(["batch", "channel", "height", "width"]); + let tensor = + rerun::Tensor::try_from(data)?.with_dim_names(["batch", "channel", "height", "width"]); rec.log("tensor", &tensor)?; rerun::native_viewer::show(storage.take())?; diff --git a/rerun_cpp/src/rerun/archetypes/annotation_context.hpp b/rerun_cpp/src/rerun/archetypes/annotation_context.hpp index a6fd52d78681..350b9c98ff62 100644 --- a/rerun_cpp/src/rerun/archetypes/annotation_context.hpp +++ b/rerun_cpp/src/rerun/archetypes/annotation_context.hpp @@ -29,6 +29,8 @@ namespace rerun { /// ```cpp,ignore /// #include /// + /// #include + /// /// int main() { /// auto rec = rerun::RecordingStream("rerun_example_annotation_context_connections"); /// rec.connect("127.0.0.1:9876").throw_on_failure(); @@ -46,19 +48,14 @@ namespace rerun { /// const int HEIGHT = 8; /// const int WIDTH = 12; /// std::vector data(WIDTH * HEIGHT, 0); - /// for (auto y = 0; y <4; ++y) { // top half - /// auto row = data.begin() + y * WIDTH; - /// std::fill(row, row + 6, 1); // left half + /// for (auto y = 0; y <4; ++y) { // top half + /// std::fill_n(data.begin() + y * WIDTH, 6, 1); // left half /// } - /// for (auto y = 4; y <8; ++y) { // bottom half - /// auto row = data.begin() + y * WIDTH; - /// std::fill(row + 6, row + 12, 2); // right half + /// for (auto y = 4; y <8; ++y) { // bottom half + /// std::fill_n(data.begin() + y * WIDTH + 6, 6, 2); // right half /// } /// - /// rec.log( - /// "segmentation/image", - /// rerun::SegmentationImage(rerun::TensorData({HEIGHT, WIDTH}, std::move(data))) - /// ); + /// rec.log("segmentation/image", rerun::SegmentationImage({HEIGHT, WIDTH}, std::move(data))); /// } /// ``` struct AnnotationContext { diff --git a/rerun_cpp/src/rerun/archetypes/depth_image.hpp b/rerun_cpp/src/rerun/archetypes/depth_image.hpp index a2f818944d93..196da0307b9b 100644 --- a/rerun_cpp/src/rerun/archetypes/depth_image.hpp +++ b/rerun_cpp/src/rerun/archetypes/depth_image.hpp @@ -8,6 +8,7 @@ #include "../components/draw_order.hpp" #include "../components/tensor_data.hpp" #include "../data_cell.hpp" +#include "../error.hpp" #include "../indicator_component.hpp" #include "../result.hpp" @@ -22,6 +23,45 @@ namespace rerun { /// /// The shape of the `TensorData` must be mappable to an `HxW` tensor. /// Each pixel corresponds to a depth value in units specified by `meter`. + /// + /// ## Example + /// + /// ### Depth to 3D example + /// ```cpp,ignore + /// #include + /// + /// #include + /// + /// int main() { + /// auto rec = rerun::RecordingStream("rerun_example_depth_image"); + /// rec.connect("127.0.0.1:9876").throw_on_failure(); + /// + /// // Create a synthetic depth image. + /// const int HEIGHT = 8; + /// const int WIDTH = 12; + /// std::vector data(WIDTH * HEIGHT, 65535); + /// for (auto y = 0; y <4; ++y) { // top half + /// std::fill_n(data.begin() + y * WIDTH, 6, 20000); // left half + /// } + /// for (auto y = 4; y <8; ++y) { // bottom half + /// std::fill_n(data.begin() + y * WIDTH + 6, 6, 45000); // right half + /// } + /// + /// // If we log a pinhole camera model, the depth gets automatically back-projected to 3D + /// rec.log( + /// "world/camera", + /// rerun::Pinhole::focal_length_and_resolution( + /// {20.0f, 20.0f}, + /// {static_cast(WIDTH), static_cast(HEIGHT)} + /// ) + /// ); + /// + /// rec.log( + /// "world/camera/depth", + /// rerun::DepthImage({HEIGHT, WIDTH}, std::move(data)).with_meter(10000.0) + /// ); + /// } + /// ``` struct DepthImage { /// The depth-image data. Should always be a rank-2 tensor. rerun::components::TensorData data; @@ -42,12 +82,28 @@ namespace rerun { /// Indicator component, used to identify the archetype when converting to a list of components. using IndicatorComponent = components::IndicatorComponent; + public: + // Extensions to generated type defined in 'depth_image_ext.cpp' + + /// New depth image from height/width and tensor buffer. + /// + /// Sets the dimension names to "height" and "width" if they are not specified. + /// Calls `Error::handle()` if the shape is not rank 2. + DepthImage( + std::vector shape, datatypes::TensorBuffer buffer + ) + : DepthImage(datatypes::TensorData(std::move(shape), std::move(buffer))) {} + + /// New depth image from tensor data. + /// + /// Sets the dimension names to "height" and "width" if they are not specified. + /// Calls `Error::handle()` if the shape is not rank 2. + explicit DepthImage(components::TensorData _data); + public: DepthImage() = default; DepthImage(DepthImage&& other) = default; - explicit DepthImage(rerun::components::TensorData _data) : data(std::move(_data)) {} - /// An optional floating point value that specifies how long a meter is in the native depth units. /// /// For instance: with uint16, perhaps meter=1000 which would mean you have millimeter precision diff --git a/rerun_cpp/src/rerun/archetypes/depth_image_ext.cpp b/rerun_cpp/src/rerun/archetypes/depth_image_ext.cpp new file mode 100644 index 000000000000..d6683c43baa0 --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/depth_image_ext.cpp @@ -0,0 +1,45 @@ +#include "../error.hpp" +#include "depth_image.hpp" + +// Uncomment for better auto-complete while editing the extension. +// #define EDIT_EXTENSION + +namespace rerun { + namespace archetypes { + +#ifdef EDIT_EXTENSION + // [CODEGEN COPY TO HEADER START] + + /// New depth image from height/width and tensor buffer. + /// + /// Sets the dimension names to "height" and "width" if they are not specified. + /// Calls `Error::handle()` if the shape is not rank 2. + DepthImage(std::vector shape, datatypes::TensorBuffer buffer) + : DepthImage(datatypes::TensorData(std::move(shape), std::move(buffer))) {} + + /// New depth image from tensor data. + /// + /// Sets the dimension names to "height" and "width" if they are not specified. + /// Calls `Error::handle()` if the shape is not rank 2. + explicit DepthImage(components::TensorData _data); + + // [CODEGEN COPY TO HEADER END] +#endif + + DepthImage::DepthImage(components::TensorData _data) : data(std::move(_data)) { + auto& shape = data.data.shape; + if (shape.size() != 2) { + Error(ErrorCode::InvalidTensorDimension, "Shape must be rank 2.").handle(); + return; + } + + if (!shape[0].name.has_value()) { + shape[0].name = "height"; + } + if (!shape[1].name.has_value()) { + shape[1].name = "width"; + } + } + + } // namespace archetypes +} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/image.hpp b/rerun_cpp/src/rerun/archetypes/image.hpp index 92272102e1ba..89dde6851070 100644 --- a/rerun_cpp/src/rerun/archetypes/image.hpp +++ b/rerun_cpp/src/rerun/archetypes/image.hpp @@ -7,6 +7,7 @@ #include "../components/draw_order.hpp" #include "../components/tensor_data.hpp" #include "../data_cell.hpp" +#include "../error.hpp" #include "../indicator_component.hpp" #include "../result.hpp" @@ -26,6 +27,35 @@ namespace rerun { /// /// Leading and trailing unit-dimensions are ignored, so that /// `1x640x480x3x1` is treated as a `640x480x3` RGB image. + /// + /// ## Example + /// + /// ### `image_simple`: + /// ```cpp,ignore + /// #include + /// + /// int main() { + /// auto rec = rerun::RecordingStream("rerun_example_image_simple"); + /// rec.connect("127.0.0.1:9876").throw_on_failure(); + /// + /// // Create a synthetic image. + /// const int HEIGHT = 8; + /// const int WIDTH = 12; + /// std::vector data(WIDTH * HEIGHT * 3, 0); + /// for (size_t i = 0; i ; + public: + // Extensions to generated type defined in 'image_ext.cpp' + + /// New Image from height/width/channel and tensor buffer. + /// + /// Sets the dimension names to "height", "width" and "channel" if they are not specified. + /// Calls `Error::handle()` if the shape is not rank 2 or 3. + Image(std::vector shape, datatypes::TensorBuffer buffer) + : Image(datatypes::TensorData(std::move(shape), std::move(buffer))) {} + + /// New depth image from tensor data. + /// + /// Sets the dimension names to "height", "width" and "channel" if they are not specified. + /// Calls `Error::handle()` if the shape is not rank 2 or 3. + explicit Image(rerun::components::TensorData _data); + public: Image() = default; Image(Image&& other) = default; - explicit Image(rerun::components::TensorData _data) : data(std::move(_data)) {} - /// An optional floating point value that specifies the 2D drawing order. /// /// Objects with higher values are drawn on top of those with lower values. diff --git a/rerun_cpp/src/rerun/archetypes/image_ext.cpp b/rerun_cpp/src/rerun/archetypes/image_ext.cpp new file mode 100644 index 000000000000..bcb65011a8b6 --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/image_ext.cpp @@ -0,0 +1,60 @@ +#include "../error.hpp" +#include "image.hpp" + +// Uncomment for better auto-complete while editing the extension. +// #define EDIT_EXTENSION + +namespace rerun { + namespace archetypes { + +#ifdef EDIT_EXTENSION + // [CODEGEN COPY TO HEADER START] + + /// New Image from height/width/channel and tensor buffer. + /// + /// Sets the dimension names to "height", "width" and "channel" if they are not specified. + /// Calls `Error::handle()` if the shape is not rank 2 or 3. + Image(std::vector shape, datatypes::TensorBuffer buffer) + : Image(datatypes::TensorData(std::move(shape), std::move(buffer))) {} + + /// New depth image from tensor data. + /// + /// Sets the dimension names to "height", "width" and "channel" if they are not specified. + /// Calls `Error::handle()` if the shape is not rank 2 or 3. + explicit Image(rerun::components::TensorData _data); + // [CODEGEN COPY TO HEADER END] +#endif + + Image::Image(rerun::components::TensorData _data) : data(std::move(_data)) { + auto& shape = data.data.shape; + if (shape.size() != 2 && shape.size() != 3) { + Error( + ErrorCode::InvalidTensorDimension, + "Image shape is expected to be either rank 2 or 3." + ) + .handle(); + return; + } + if (shape.size() == 3 && shape[2].size != 1 && shape[2].size != 3 && + shape[2].size != 4) { + Error( + ErrorCode::InvalidTensorDimension, + "Only images with 1, 3 and 4 channels are supported." + ) + .handle(); + return; + } + + if (!shape[0].name.has_value()) { + shape[0].name = "height"; + } + if (!shape[1].name.has_value()) { + shape[1].name = "width"; + } + if (shape.size() > 2 && !shape[2].name.has_value()) { + shape[2].name = "depth"; + } + } + + } // namespace archetypes +} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/segmentation_image.hpp b/rerun_cpp/src/rerun/archetypes/segmentation_image.hpp index 35b820bba391..293a4064d706 100644 --- a/rerun_cpp/src/rerun/archetypes/segmentation_image.hpp +++ b/rerun_cpp/src/rerun/archetypes/segmentation_image.hpp @@ -25,6 +25,42 @@ namespace rerun { /// /// Leading and trailing unit-dimensions are ignored, so that /// `1x640x480x1` is treated as a `640x480` image. + /// + /// ## Example + /// + /// ### Simple segmentation image + /// ```cpp,ignore + /// #include + /// + /// #include + /// + /// int main() { + /// auto rec = rerun::RecordingStream("rerun_example_annotation_context_connections"); + /// rec.connect("127.0.0.1:9876").throw_on_failure(); + /// + /// // Create a segmentation image + /// const int HEIGHT = 8; + /// const int WIDTH = 12; + /// std::vector data(WIDTH * HEIGHT, 0); + /// for (auto y = 0; y <4; ++y) { // top half + /// std::fill_n(data.begin() + y * WIDTH, 6, 1); // left half + /// } + /// for (auto y = 4; y <8; ++y) { // bottom half + /// std::fill_n(data.begin() + y * WIDTH + 6, 6, 2); // right half + /// } + /// + /// // create an annotation context to describe the classes + /// rec.log_timeless( + /// "/", + /// rerun::AnnotationContext({ + /// rerun::AnnotationInfo(1, "red", rerun::Rgba32(255, 0, 0)), + /// rerun::AnnotationInfo(2, "green", rerun::Rgba32(0, 255, 0)), + /// }) + /// ); + /// + /// rec.log("image", rerun::SegmentationImage({HEIGHT, WIDTH}, std::move(data))); + /// } + /// ``` struct SegmentationImage { /// The image data. Should always be a rank-2 tensor. rerun::components::TensorData data; @@ -42,11 +78,20 @@ namespace rerun { public: // Extensions to generated type defined in 'segmentation_image_ext.cpp' + /// New segmentation image from height/width and tensor buffer. + /// + /// Sets the dimension names to "height" and "width" if they are not specified. + /// Calls `Error::handle()` if the shape is not rank 2. + SegmentationImage( + std::vector shape, datatypes::TensorBuffer buffer + ) + : SegmentationImage(datatypes::TensorData(std::move(shape), std::move(buffer))) {} + /// New segmentation image from tensor data. /// - /// Sets dimensions to width/height if they are not specified. - /// Calls Error::handle() if the shape is not rank 2. - explicit SegmentationImage(rerun::components::TensorData _data); + /// Sets the dimension names to "height" and "width" if they are not specified. + /// Calls `Error::handle()` if the shape is not rank 2. + explicit SegmentationImage(components::TensorData _data); public: SegmentationImage() = default; diff --git a/rerun_cpp/src/rerun/archetypes/segmentation_image_ext.cpp b/rerun_cpp/src/rerun/archetypes/segmentation_image_ext.cpp index 2626d6954c2f..d1a8fa9da235 100644 --- a/rerun_cpp/src/rerun/archetypes/segmentation_image_ext.cpp +++ b/rerun_cpp/src/rerun/archetypes/segmentation_image_ext.cpp @@ -10,15 +10,25 @@ namespace rerun { #ifdef EDIT_EXTENSION // [CODEGEN COPY TO HEADER START] + /// New segmentation image from height/width and tensor buffer. + /// + /// Sets the dimension names to "height" and "width" if they are not specified. + /// Calls `Error::handle()` if the shape is not rank 2. + SegmentationImage( + std::vector shape, datatypes::TensorBuffer buffer + ) + : SegmentationImage(datatypes::TensorData(std::move(shape), std::move(buffer))) {} + /// New segmentation image from tensor data. /// - /// Sets dimensions to width/height if they are not specified. - /// Calls Error::handle() if the shape is not rank 2. - explicit SegmentationImage(rerun::components::TensorData _data); + /// Sets the dimension names to "height" and "width" if they are not specified. + /// Calls `Error::handle()` if the shape is not rank 2. + explicit SegmentationImage(components::TensorData _data); + // [CODEGEN COPY TO HEADER END] #endif - SegmentationImage::SegmentationImage(rerun::components::TensorData _data) + SegmentationImage::SegmentationImage(components::TensorData _data) : data(std::move(_data)) { auto& shape = data.data.shape; if (shape.size() != 2) { diff --git a/rerun_cpp/src/rerun/archetypes/tensor.hpp b/rerun_cpp/src/rerun/archetypes/tensor.hpp index 1dcc2957fc9f..0600a4ffc63e 100644 --- a/rerun_cpp/src/rerun/archetypes/tensor.hpp +++ b/rerun_cpp/src/rerun/archetypes/tensor.hpp @@ -6,6 +6,7 @@ #include "../component_batch.hpp" #include "../components/tensor_data.hpp" #include "../data_cell.hpp" +#include "../error.hpp" #include "../indicator_component.hpp" #include "../result.hpp" @@ -16,6 +17,31 @@ namespace rerun { namespace archetypes { /// **Archetype**: A generic n-dimensional Tensor. + /// + /// ## Example + /// + /// ### Simple Tensor + /// ```cpp,ignore + /// #include + /// + /// #include + /// + /// int main() { + /// auto rec = rerun::RecordingStream("rerun_example_tensor_simple"); + /// rec.connect("127.0.0.1:9876").throw_on_failure(); + /// + /// std::default_random_engine gen; + /// std::uniform_int_distribution dist(0, 255); + /// + /// std::vector data(8 * 6 * 3 * 5); + /// std::generate(data.begin(), data.end(), [&] { return dist(gen); }); + /// + /// rec.log( + /// "tensor", + /// rerun::Tensor({8, 6, 3, 5}, data).with_dim_names({"batch", "channel", "height", "width"}) + /// ); + /// } + /// ``` struct Tensor { /// The tensor data rerun::components::TensorData data; @@ -25,6 +51,26 @@ namespace rerun { /// Indicator component, used to identify the archetype when converting to a list of components. using IndicatorComponent = components::IndicatorComponent; + public: + // Extensions to generated type defined in 'tensor_ext.cpp' + + /// New Tensor from dimensions and tensor buffer. + Tensor( + std::vector shape, + rerun::datatypes::TensorBuffer buffer + ) + : Tensor(rerun::datatypes::TensorData(std::move(shape), std::move(buffer))) {} + + /// Update the `names` of the contained [`TensorData`] dimensions. + /// + /// Any existing Dimension names will be be overwritten. + /// + /// If too many, or too few names are provided, this function will call + /// Error::handle and then proceed to only update the subset of names that it can. + /// + /// TODO(#3794): don't use std::vector here. + Tensor with_dim_names(std::vector names) &&; + public: Tensor() = default; Tensor(Tensor&& other) = default; diff --git a/rerun_cpp/src/rerun/archetypes/tensor_ext.cpp b/rerun_cpp/src/rerun/archetypes/tensor_ext.cpp new file mode 100644 index 000000000000..8eca6aef43ac --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/tensor_ext.cpp @@ -0,0 +1,54 @@ +#include "../error.hpp" +#include "tensor.hpp" + +// Uncomment for better auto-complete while editing the extension. +// #define EDIT_EXTENSION + +namespace rerun { + namespace archetypes { + +#ifdef EDIT_EXTENSION + // [CODEGEN COPY TO HEADER START] + + /// New Tensor from dimensions and tensor buffer. + Tensor( + std::vector shape, + rerun::datatypes::TensorBuffer buffer + ) + : Tensor(rerun::datatypes::TensorData(std::move(shape), std::move(buffer))) {} + + /// Update the `names` of the contained [`TensorData`] dimensions. + /// + /// Any existing Dimension names will be be overwritten. + /// + /// If too many, or too few names are provided, this function will call + /// Error::handle and then proceed to only update the subset of names that it can. + /// + /// TODO(#3794): don't use std::vector here. + Tensor with_dim_names(std::vector names) &&; + + // [CODEGEN COPY TO HEADER END] +#endif + + Tensor Tensor::with_dim_names(std::vector names) && { + auto& shape = data.data.shape; + + if (names.size() != shape.size()) { + Error( + ErrorCode::InvalidTensorDimension, + "Wrong number of names provided for tensor dimension. " + + std::to_string(names.size()) + " provided but " + + std::to_string(shape.size()) + " expected." + ) + .handle(); + } + + for (size_t i = 0; i < std::min(shape.size(), names.size()); ++i) { + shape[i].name = std::move(names[i]); + } + + return std::move(*this); + } + + } // namespace archetypes +} // namespace rerun diff --git a/rerun_cpp/src/rerun/datatypes/tensor_buffer.hpp b/rerun_cpp/src/rerun/datatypes/tensor_buffer.hpp index 5edad3b9968d..f45b9bd5e9da 100644 --- a/rerun_cpp/src/rerun/datatypes/tensor_buffer.hpp +++ b/rerun_cpp/src/rerun/datatypes/tensor_buffer.hpp @@ -231,37 +231,46 @@ namespace rerun { // TODO(#3794): don't use std::vector here /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector u8) : TensorBuffer(TensorBuffer::u8(u8)) {} + TensorBuffer(std::vector u8) : TensorBuffer(TensorBuffer::u8(std::move(u8))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector u16) : TensorBuffer(TensorBuffer::u16(u16)) {} + TensorBuffer(std::vector u16) + : TensorBuffer(TensorBuffer::u16(std::move(u16))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector u32) : TensorBuffer(TensorBuffer::u32(u32)) {} + TensorBuffer(std::vector u32) + : TensorBuffer(TensorBuffer::u32(std::move(u32))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector u64) : TensorBuffer(TensorBuffer::u64(u64)) {} + TensorBuffer(std::vector u64) + : TensorBuffer(TensorBuffer::u64(std::move(u64))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector i8) : TensorBuffer(TensorBuffer::i8(i8)) {} + TensorBuffer(std::vector i8) : TensorBuffer(TensorBuffer::i8(std::move(i8))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector i16) : TensorBuffer(TensorBuffer::i16(i16)) {} + TensorBuffer(std::vector i16) + : TensorBuffer(TensorBuffer::i16(std::move(i16))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector i32) : TensorBuffer(TensorBuffer::i32(i32)) {} + TensorBuffer(std::vector i32) + : TensorBuffer(TensorBuffer::i32(std::move(i32))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector i64) : TensorBuffer(TensorBuffer::i64(i64)) {} + TensorBuffer(std::vector i64) + : TensorBuffer(TensorBuffer::i64(std::move(i64))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector f16) : TensorBuffer(TensorBuffer::f16(f16)) {} + TensorBuffer(std::vector f16) + : TensorBuffer(TensorBuffer::f16(std::move(f16))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector f32) : TensorBuffer(TensorBuffer::f32(f32)) {} + TensorBuffer(std::vector f32) + : TensorBuffer(TensorBuffer::f32(std::move(f32))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector f64) : TensorBuffer(TensorBuffer::f64(f64)) {} + TensorBuffer(std::vector f64) + : TensorBuffer(TensorBuffer::f64(std::move(f64))) {} /// Number of elements in the buffer. /// diff --git a/rerun_cpp/src/rerun/datatypes/tensor_buffer_ext.cpp b/rerun_cpp/src/rerun/datatypes/tensor_buffer_ext.cpp index e6b70d799afa..f135e66ffdbe 100644 --- a/rerun_cpp/src/rerun/datatypes/tensor_buffer_ext.cpp +++ b/rerun_cpp/src/rerun/datatypes/tensor_buffer_ext.cpp @@ -18,37 +18,46 @@ namespace rerun { // TODO(#3794): don't use std::vector here /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector u8) : TensorBuffer(TensorBuffer::u8(u8)) {} + TensorBuffer(std::vector u8) : TensorBuffer(TensorBuffer::u8(std::move(u8))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector u16) : TensorBuffer(TensorBuffer::u16(u16)) {} + TensorBuffer(std::vector u16) + : TensorBuffer(TensorBuffer::u16(std::move(u16))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector u32) : TensorBuffer(TensorBuffer::u32(u32)) {} + TensorBuffer(std::vector u32) + : TensorBuffer(TensorBuffer::u32(std::move(u32))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector u64) : TensorBuffer(TensorBuffer::u64(u64)) {} + TensorBuffer(std::vector u64) + : TensorBuffer(TensorBuffer::u64(std::move(u64))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector i8) : TensorBuffer(TensorBuffer::i8(i8)) {} + TensorBuffer(std::vector i8) : TensorBuffer(TensorBuffer::i8(std::move(i8))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector i16) : TensorBuffer(TensorBuffer::i16(i16)) {} + TensorBuffer(std::vector i16) + : TensorBuffer(TensorBuffer::i16(std::move(i16))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector i32) : TensorBuffer(TensorBuffer::i32(i32)) {} + TensorBuffer(std::vector i32) + : TensorBuffer(TensorBuffer::i32(std::move(i32))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector i64) : TensorBuffer(TensorBuffer::i64(i64)) {} + TensorBuffer(std::vector i64) + : TensorBuffer(TensorBuffer::i64(std::move(i64))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector f16) : TensorBuffer(TensorBuffer::f16(f16)) {} + TensorBuffer(std::vector f16) + : TensorBuffer(TensorBuffer::f16(std::move(f16))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector f32) : TensorBuffer(TensorBuffer::f32(f32)) {} + TensorBuffer(std::vector f32) + : TensorBuffer(TensorBuffer::f32(std::move(f32))) {} /// Construct a `TensorBuffer` from a `std::vector`. - TensorBuffer(std::vector f64) : TensorBuffer(TensorBuffer::f64(f64)) {} + TensorBuffer(std::vector f64) + : TensorBuffer(TensorBuffer::f64(std::move(f64))) {} /// Number of elements in the buffer. /// diff --git a/rerun_cpp/tests/archetypes/image.cpp b/rerun_cpp/tests/archetypes/image.cpp new file mode 100644 index 000000000000..9c18fe2fc15b --- /dev/null +++ b/rerun_cpp/tests/archetypes/image.cpp @@ -0,0 +1,127 @@ +#include "../error_check.hpp" + +#include +using namespace rerun::archetypes; + +#define TEST_TAG "[image][archetypes]" + +SCENARIO("Image archetype can be created from tensor data." TEST_TAG) { + GIVEN("tensor data with correct rank 2 shape") { + rerun::datatypes::TensorData data({3, 7}, std::vector(3 * 7, 0)); + THEN("no error occurs on image construction") { + auto image = check_logged_error([&] { return Image(std::move(data)); }); + + AND_THEN("width and height got set") { + CHECK(image.data.data.shape[0].name == "height"); + CHECK(image.data.data.shape[1].name == "width"); + } + + AND_THEN("serialization succeeds") { + CHECK(rerun::AsComponents().serialize(image).is_ok()); + } + } + } + GIVEN("tensor data with correct rank 3 shape") { + rerun::datatypes::TensorData data({3, 7, 3}, std::vector(3 * 7 * 3, 0)); + THEN("no error occurs on image construction") { + auto image = check_logged_error([&] { return Image(std::move(data)); }); + + AND_THEN("width, height and depth got set") { + CHECK(image.data.data.shape[0].name == "height"); + CHECK(image.data.data.shape[1].name == "width"); + CHECK(image.data.data.shape[2].name == "depth"); + } + + AND_THEN("serialization succeeds") { + CHECK(rerun::AsComponents().serialize(image).is_ok()); + } + } + } + GIVEN("tensor data with incorrect rank 3 shape") { + rerun::datatypes::TensorData data({3, 7, 2}, std::vector(3 * 7 * 2, 0)); + THEN("a warning occurs on image construction") { + auto image = check_logged_error( + [&] { return Image(std::move(data)); }, + rerun::ErrorCode::InvalidTensorDimension + ); + + AND_THEN("serialization succeeds") { + CHECK(rerun::AsComponents().serialize(image).is_ok()); + } + } + } + + GIVEN("tensor data with correct shape and named dimensions") { + rerun::datatypes::TensorData data( + {rerun::datatypes::TensorDimension(3, "rick"), + rerun::datatypes::TensorDimension(7, "morty")}, + std::vector(3 * 7, 0) + ); + THEN("no error occurs on image construction") { + auto image = check_logged_error([&] { return Image(std::move(data)); }); + + AND_THEN("tensor dimensions are unchanged") { + CHECK(image.data.data.shape[0].name == "rick"); + CHECK(image.data.data.shape[1].name == "morty"); + } + + AND_THEN("serialization succeeds") { + CHECK(rerun::AsComponents().serialize(image).is_ok()); + } + } + } + + GIVEN("tensor data with too high rank") { + rerun::datatypes::TensorData data( + { + { + rerun::datatypes::TensorDimension(1, "tick"), + rerun::datatypes::TensorDimension(2, "trick"), + rerun::datatypes::TensorDimension(3, "track"), + rerun::datatypes::TensorDimension(4, "dagobert"), + }, + }, + std::vector(1 * 2 * 3 * 4, 0) + ); + THEN("a warning occurs on image construction") { + auto image = check_logged_error( + [&] { return Image(std::move(data)); }, + rerun::ErrorCode::InvalidTensorDimension + ); + + AND_THEN("tensor dimension names are unchanged") { + CHECK(image.data.data.shape[0].name == "tick"); + CHECK(image.data.data.shape[1].name == "trick"); + CHECK(image.data.data.shape[2].name == "track"); + CHECK(image.data.data.shape[3].name == "dagobert"); + } + + AND_THEN("serialization succeeds") { + CHECK(rerun::AsComponents().serialize(image).is_ok()); + } + } + } + + GIVEN("tensor data with too low rank") { + rerun::datatypes::TensorData data( + {{ + rerun::datatypes::TensorDimension(1, "dr robotnik"), + }}, + std::vector(1, 0) + ); + THEN("a warning occurs on image construction") { + auto image = check_logged_error( + [&] { return Image(std::move(data)); }, + rerun::ErrorCode::InvalidTensorDimension + ); + + AND_THEN("tensor dimension names are unchanged") { + CHECK(image.data.data.shape[0].name == "dr robotnik"); + } + + AND_THEN("serialization succeeds") { + CHECK(rerun::AsComponents().serialize(image).is_ok()); + } + } + } +} diff --git a/rerun_cpp/tests/archetypes/segmentation_and_depth_image.cpp b/rerun_cpp/tests/archetypes/segmentation_and_depth_image.cpp new file mode 100644 index 000000000000..4145c478b002 --- /dev/null +++ b/rerun_cpp/tests/archetypes/segmentation_and_depth_image.cpp @@ -0,0 +1,107 @@ +#include "../error_check.hpp" + +#include +#include + +using namespace rerun::archetypes; + +#define TEST_TAG "[image][archetypes]" + +template +void run_image_tests() { + GIVEN("tensor data with correct shape") { + rerun::datatypes::TensorData data({3, 7}, std::vector(3 * 7, 0)); + THEN("no error occurs on image construction") { + auto image = check_logged_error([&] { return ImageType(std::move(data)); }); + + AND_THEN("width and height got set") { + CHECK(image.data.data.shape[0].name == "height"); + CHECK(image.data.data.shape[1].name == "width"); + } + + AND_THEN("serialization succeeds") { + CHECK(rerun::AsComponents().serialize(image).is_ok()); + } + } + } + + GIVEN("tensor data with correct shape and named dimensions") { + rerun::datatypes::TensorData data( + {rerun::datatypes::TensorDimension(3, "rick"), + rerun::datatypes::TensorDimension(7, "morty")}, + std::vector(3 * 7, 0) + ); + THEN("no error occurs on image construction") { + auto image = check_logged_error([&] { return ImageType(std::move(data)); }); + + AND_THEN("tensor dimensions are unchanged") { + CHECK(image.data.data.shape[0].name == "rick"); + CHECK(image.data.data.shape[1].name == "morty"); + } + + AND_THEN("serialization succeeds") { + CHECK(rerun::AsComponents().serialize(image).is_ok()); + } + } + } + + GIVEN("tensor data with too high rank") { + rerun::datatypes::TensorData data( + { + { + rerun::datatypes::TensorDimension(1, "tick"), + rerun::datatypes::TensorDimension(2, "trick"), + rerun::datatypes::TensorDimension(3, "track"), + }, + }, + std::vector(1 * 2 * 3, 0) + ); + THEN("a warning occurs on image construction") { + auto image = check_logged_error( + [&] { return ImageType(std::move(data)); }, + rerun::ErrorCode::InvalidTensorDimension + ); + + AND_THEN("tensor dimension names are unchanged") { + CHECK(image.data.data.shape[0].name == "tick"); + CHECK(image.data.data.shape[1].name == "trick"); + CHECK(image.data.data.shape[2].name == "track"); + } + + AND_THEN("serialization succeeds") { + CHECK(rerun::AsComponents().serialize(image).is_ok()); + } + } + } + + GIVEN("tensor data with too low rank") { + rerun::datatypes::TensorData data( + {{ + rerun::datatypes::TensorDimension(1, "dr robotnik"), + }}, + std::vector(1, 0) + ); + THEN("a warning occurs on image construction") { + auto image = check_logged_error( + [&] { return ImageType(std::move(data)); }, + rerun::ErrorCode::InvalidTensorDimension + ); + + AND_THEN("tensor dimension names are unchanged") { + CHECK(image.data.data.shape[0].name == "dr robotnik"); + } + + AND_THEN("serialization succeeds") { + CHECK(rerun::AsComponents().serialize(image).is_ok()); + } + } + } +} + +SCENARIO("Segmentation archetype image can be created from tensor data." TEST_TAG) { + run_image_tests(); +} + +SCENARIO("Depth archetype image can be created from tensor data." TEST_TAG) { + run_image_tests(); +} diff --git a/rerun_cpp/tests/archetypes/segmentation_image.cpp b/rerun_cpp/tests/archetypes/segmentation_image.cpp deleted file mode 100644 index f50ae4c0ac1d..000000000000 --- a/rerun_cpp/tests/archetypes/segmentation_image.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "../error_check.hpp" - -#include - -using namespace rerun::archetypes; - -#define TEST_TAG "[segmentation_image][archetypes]" - -SCENARIO("segmentation image can be created from tensor data" TEST_TAG) { - GIVEN("tensor data with correct shape") { - rerun::datatypes::TensorData data({3, 7}, std::vector(3 * 7, 0)); - - THEN("no error occurs on segmentation image construction") { - auto segmentation_image = - check_logged_error([&] { return SegmentationImage(std::move(data)); }); - - AND_THEN("width and height got set") { - CHECK(segmentation_image.data.data.shape[0].name == "height"); - CHECK(segmentation_image.data.data.shape[1].name == "width"); - } - - AND_THEN("serialization succeeds") { - CHECK(rerun::AsComponents() - .serialize(segmentation_image) - .is_ok()); - } - } - } - - GIVEN("tensor data with correct shape and named dimensions") { - rerun::datatypes::TensorData data( - {rerun::datatypes::TensorDimension(3, "rick"), - rerun::datatypes::TensorDimension(7, "morty")}, - std::vector(3 * 7, 0) - ); - - THEN("no error occurs on segmentation image construction") { - auto segmentation_image = - check_logged_error([&] { return SegmentationImage(std::move(data)); }); - - AND_THEN("tensor dimensions are unchanged") { - CHECK(segmentation_image.data.data.shape[0].name == "rick"); - CHECK(segmentation_image.data.data.shape[1].name == "morty"); - } - - AND_THEN("serialization succeeds") { - CHECK(rerun::AsComponents() - .serialize(segmentation_image) - .is_ok()); - } - } - } - - GIVEN("tensor data with too high rank") { - rerun::datatypes::TensorData data( - { - { - rerun::datatypes::TensorDimension(1, "tick"), - rerun::datatypes::TensorDimension(2, "trick"), - rerun::datatypes::TensorDimension(3, "track"), - }, - }, - std::vector(1 * 2 * 3, 0) - ); - - THEN("a warning occurs on segmentation image construction") { - auto segmentation_image = check_logged_error( - [&] { return SegmentationImage(std::move(data)); }, - rerun::ErrorCode::InvalidTensorDimension - ); - - AND_THEN("tensor dimension names are unchanged") { - CHECK(segmentation_image.data.data.shape[0].name == "tick"); - CHECK(segmentation_image.data.data.shape[1].name == "trick"); - CHECK(segmentation_image.data.data.shape[2].name == "track"); - } - - AND_THEN("serialization succeeds") { - CHECK(rerun::AsComponents() - .serialize(segmentation_image) - .is_ok()); - } - } - } - - GIVEN("tensor data with too low rank") { - rerun::datatypes::TensorData data( - {{ - rerun::datatypes::TensorDimension(1, "dr robotnik"), - }}, - std::vector(1, 0) - ); - - THEN("a warning occurs on segmentation image construction") { - auto segmentation_image = check_logged_error( - [&] { return SegmentationImage(std::move(data)); }, - rerun::ErrorCode::InvalidTensorDimension - ); - - AND_THEN("tensor dimension names are unchanged") { - CHECK(segmentation_image.data.data.shape[0].name == "dr robotnik"); - } - - AND_THEN("serialization succeeds") { - CHECK(rerun::AsComponents() - .serialize(segmentation_image) - .is_ok()); - } - } - } -} diff --git a/tests/cpp/roundtrips/depth_image/main.cpp b/tests/cpp/roundtrips/depth_image/main.cpp new file mode 100644 index 000000000000..da161188816d --- /dev/null +++ b/tests/cpp/roundtrips/depth_image/main.cpp @@ -0,0 +1,13 @@ +// Logs a `DepthImage` archetype for roundtrip checks. + +#include +#include + +int main(int argc, char** argv) { + auto rec = rerun::RecordingStream("rerun_example_roundtrip_depth_image"); + rec.save(argv[1]).throw_on_failure(); + + auto img = rerun::datatypes::TensorData({2, 3}, std::vector{0, 1, 2, 3, 4, 5}); + + rec.log("depth_image", rerun::archetypes::DepthImage(img).with_meter(1000.0)); +} diff --git a/tests/cpp/roundtrips/image/main.cpp b/tests/cpp/roundtrips/image/main.cpp new file mode 100644 index 000000000000..9dd9aae8931e --- /dev/null +++ b/tests/cpp/roundtrips/image/main.cpp @@ -0,0 +1,53 @@ +// Logs an `Image` archetype for roundtrip checks. + +#include + +#include + +uint32_t as_uint(float f) { + // Dont do `*reinterpret_cast(&x)` since it breaks strict aliasing rules. + uint32_t n; + memcpy(&n, &f, sizeof(float)); + return n; +} + +// Adopted from https://stackoverflow.com/a/60047308 +// IEEE-754 16-bit floating-point format (without infinity): 1-5-10, exp-15, +-131008.0, +-6.1035156E-5, +-5.9604645E-8, 3.311 digits +rerun::half half_from_float(const float x) { + // round-to-nearest-even: add last bit after truncated mantissa1 + const uint32_t b = as_uint(x) + 0x00001000; + const uint32_t e = (b & 0x7F800000) >> 23; // exponent + // mantissa; in line below: 0x007FF000 = 0x00800000-0x00001000 = decimal indicator flag - initial rounding + const uint32_t m = b & 0x007FFFFF; + const uint16_t f16 = (b & 0x80000000) >> 16 | + (e > 112) * ((((e - 112) << 10) & 0x7C00) | m >> 13) | + ((e < 113) & (e > 101)) * ((((0x007FF000 + m) >> (125 - e)) + 1) >> 1) | + (e > 143) * 0x7FFF; // sign : normalized : denormalized : saturate + return rerun::half{f16}; +} + +int main(int argc, char** argv) { + auto rec = rerun::RecordingStream("rerun_example_roundtrip_image"); + rec.save(argv[1]).throw_on_failure(); + + // h=2 w=3 c=3 image. Red channel = x. Green channel = y. Blue channel = 128. + { + auto img = rerun::datatypes::TensorData( + {2, 3, 3}, + std::vector{0, 0, 128, 1, 0, 128, 2, 0, 128, 0, 1, 128, 1, 1, 128, 2, 1, 128} + ); + rec.log("image", rerun::archetypes::Image(img)); + } + + // h=4, w=5 mono image. Pixel = x * y * 123.4 + { + std::vector data; + for (auto y = 0; y < 4; ++y) { + for (auto x = 0; x < 5; ++x) { + data.push_back(half_from_float(x * y * 123.4f)); + } + } + auto img = rerun::datatypes::TensorData({4, 5}, std::move(data)); + rec.log("image_f16", rerun::archetypes::Image(img)); + } +} diff --git a/tests/cpp/roundtrips/segmentation_image/main.cpp b/tests/cpp/roundtrips/segmentation_image/main.cpp new file mode 100644 index 000000000000..5ef6576f9199 --- /dev/null +++ b/tests/cpp/roundtrips/segmentation_image/main.cpp @@ -0,0 +1,14 @@ +// Logs a `SegmentationImage` archetype for roundtrip checks. + +#include +#include + +int main(int argc, char** argv) { + auto rec = rerun::RecordingStream("rerun_example_roundtrip_segmentation_image"); + rec.save(argv[1]).throw_on_failure(); + + // 3x2 image. Each pixel is incremented down each row + auto img = rerun::datatypes::TensorData({2, 3}, std::vector{0, 1, 2, 3, 4, 5}); + + rec.log("segmentation_image", rerun::archetypes::SegmentationImage(img)); +} diff --git a/tests/cpp/roundtrips/tensor/main.cpp b/tests/cpp/roundtrips/tensor/main.cpp index 2864e387295d..0ff5eb67e29c 100644 --- a/tests/cpp/roundtrips/tensor/main.cpp +++ b/tests/cpp/roundtrips/tensor/main.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -7,20 +8,11 @@ int main(int argc, char** argv) { auto rec = rerun::RecordingStream("rerun_example_roundtrip_tensor"); rec.save(argv[1]).throw_on_failure(); - std::vector dimensions{ - rerun::datatypes::TensorDimension{3}, - rerun::datatypes::TensorDimension{4}, - rerun::datatypes::TensorDimension{5}, - rerun::datatypes::TensorDimension{6}, - }; + std::vector dimensions{{3, 4, 5, 6}}; - std::vector data; - for (auto i = 0; i < 360; ++i) { - data.push_back(i); - } + std::vector data(360); + std::generate(data.begin(), data.end(), [n = 0]() mutable { return n++; }); - // TODO(jleibs) Tensor data can't actually be logged yet because C++ Unions - // don't supported nested list-types. rec.log( "tensor", rerun::archetypes::Tensor( diff --git a/tests/python/roundtrips/image/main.py b/tests/python/roundtrips/image/main.py index d5ad52d9b189..3ab32c798973 100755 --- a/tests/python/roundtrips/image/main.py +++ b/tests/python/roundtrips/image/main.py @@ -18,7 +18,7 @@ def main() -> None: rr.script_setup(args, "rerun_example_roundtrip_image") - # 2x3x3 image. Red channel = x. Green channel = y. Blue channel = 128. + # h=2 w=3 c=3 image. Red channel = x. Green channel = y. Blue channel = 128. image = np.zeros((2, 3, 3), dtype=np.uint8) for i in range(3): image[:, i, 0] = i @@ -30,7 +30,7 @@ def main() -> None: rr.log("image", rr.Image(image)) - # 4x5 mono image. Pixel = x * y * 123.4 + # h=4, w=5 mono image. Pixel = x * y * 123.4 image = np.zeros((4, 5), dtype=np.float16) for i in range(4): for j in range(5): diff --git a/tests/roundtrips.py b/tests/roundtrips.py index 817ae4bec17f..cf87190af977 100755 --- a/tests/roundtrips.py +++ b/tests/roundtrips.py @@ -25,11 +25,7 @@ "asset3d": ["cpp", "py", "rust"], # Don't need it, API example roundtrips cover it all "bar_chart": ["cpp", "py", "rust"], # Don't need it, API example roundtrips cover it all "clear": ["cpp", "py", "rust"], # Don't need it, API example roundtrips cover it all - "depth_image": ["cpp"], # TODO(#3380) - "image": ["cpp"], # TODO(#3380) "mesh3d": ["cpp", "py", "rust"], # Don't need it, API example roundtrips cover it all - "segmentation_image": ["cpp"], # TODO(#3380) - "tensor": ["cpp"], # TODO(#3380) "time_series_scalar": ["cpp", "py", "rust"], # Don't need it, API example roundtrips cover it all "transform3d": [ "cpp" diff --git a/tests/rust/roundtrips/image/src/main.rs b/tests/rust/roundtrips/image/src/main.rs index 3dc4d34725b2..6b0dd9c88e0f 100644 --- a/tests/rust/roundtrips/image/src/main.rs +++ b/tests/rust/roundtrips/image/src/main.rs @@ -15,7 +15,7 @@ struct Args { fn run(rec: &RecordingStream, _args: &Args) -> anyhow::Result<()> { let mut img = RgbImage::new(3, 2); - // 2x3x3 image. Red channel = x. Green channel = y. Blue channel = 128. + // h=2 w=3 c=3 image. Red channel = x. Green channel = y. Blue channel = 128. for x in 0..3 { for y in 0..2 { img.put_pixel(x, y, Rgb([x as u8, y as u8, 128])); @@ -26,7 +26,7 @@ fn run(rec: &RecordingStream, _args: &Args) -> anyhow::Result<()> { let mut array_image = Array::::default((4, 5).f()); - // 4x5 mono image. Pixel = x * y * 123.4 + // h=4, w=5 mono image. Pixel = x * y * 123.4 for y in 0..4 { for x in 0..5 { *array_image.get_mut((y, x)).unwrap() = f16::from_f32(x as f32 * y as f32 * 123.4);