Skip to content
5 changes: 5 additions & 0 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ In addition and as before, it needs information about the camera via a `sensor_m
*Changed behaviour:* The CameraInfo topic is assumed to be relative to the Image topic:
If the image topic is "/image", the CameraInfo topic is assumed to be "/image/camera_info".

## Image Display

Bayer-encoded images (the eight `bayer_*` encodings, 8- and 16-bit) are demosaiced and shown in color, in both the Image and Camera displays.
The *Linear input* property applies sRGB encoding for cameras that publish linear-response Bayer images.

## Map Display

#### Topics
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) 2026, Arne Baeyens.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// * Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.


#ifndef RVIZ_DEFAULT_PLUGINS__DISPLAYS__IMAGE__BAYER_FORMAT_HPP_
#define RVIZ_DEFAULT_PLUGINS__DISPLAYS__IMAGE__BAYER_FORMAT_HPP_

#include <optional>
#include <string>

#include "sensor_msgs/image_encodings.hpp"

namespace rviz_default_plugins
{
namespace displays
{

// The position of R within the 2x2 Bayer cell uniquely identifies the layout.
// B sits at the opposite corner; G occupies the other two cells.
// RGGB -> R at (0, 0)
// BGGR -> R at (1, 1)
// GBRG -> R at (1, 0)
// GRBG -> R at (0, 1)
enum class BayerLayout
{
RGGB,
BGGR,
GBRG,
GRBG
};

constexpr int bayerRedRow(BayerLayout layout)
{
return (layout == BayerLayout::BGGR || layout == BayerLayout::GBRG) ? 1 : 0;
}

constexpr int bayerRedCol(BayerLayout layout)
{
return (layout == BayerLayout::BGGR || layout == BayerLayout::GRBG) ? 1 : 0;
}

struct BayerFormat
{
BayerLayout layout;
bool is_16bit;
};

// Single source of truth for the Bayer encodings the image displays can
// demosaic. Returns std::nullopt for anything else — including Bayer variants
// sensor_msgs may grow later: gating UI and conversion on this whitelist
// (rather than on image_encodings::isBayer()) keeps both in lockstep with
// what the converter actually implements.
inline std::optional<BayerFormat> bayerFormatFromEncoding(const std::string & encoding)
{
if (encoding == sensor_msgs::image_encodings::BAYER_RGGB8) {
return BayerFormat{BayerLayout::RGGB, false};
}
if (encoding == sensor_msgs::image_encodings::BAYER_BGGR8) {
return BayerFormat{BayerLayout::BGGR, false};
}
if (encoding == sensor_msgs::image_encodings::BAYER_GBRG8) {
return BayerFormat{BayerLayout::GBRG, false};
}
if (encoding == sensor_msgs::image_encodings::BAYER_GRBG8) {
return BayerFormat{BayerLayout::GRBG, false};
}
if (encoding == sensor_msgs::image_encodings::BAYER_RGGB16) {
return BayerFormat{BayerLayout::RGGB, true};
}
if (encoding == sensor_msgs::image_encodings::BAYER_BGGR16) {
return BayerFormat{BayerLayout::BGGR, true};
}
if (encoding == sensor_msgs::image_encodings::BAYER_GBRG16) {
return BayerFormat{BayerLayout::GBRG, true};
}
if (encoding == sensor_msgs::image_encodings::BAYER_GRBG16) {
return BayerFormat{BayerLayout::GRBG, true};
}
return std::nullopt;
}

} // namespace displays
} // namespace rviz_default_plugins

#endif // RVIZ_DEFAULT_PLUGINS__DISPLAYS__IMAGE__BAYER_FORMAT_HPP_
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,14 @@ class RVIZ_DEFAULT_PLUGINS_PUBLIC ImageDisplay : public rviz_common::_RosTopicDi
public Q_SLOTS:
virtual void updateNormalizeOptions();
virtual void updateSmoothScaling();
virtual void updateLinearInput();

protected Q_SLOTS:
virtual void subscribe();

protected:
void refreshLinearInputVisibility();

void onEnable() override;
void onDisable() override;
virtual void unsubscribe();
Expand Down Expand Up @@ -154,7 +157,15 @@ protected Q_SLOTS:
rviz_common::properties::FloatProperty * max_property_;
rviz_common::properties::IntProperty * median_buffer_size_property_;
rviz_common::properties::BoolProperty * smooth_scaling_property_;
bool got_float_image_;
rviz_common::properties::BoolProperty * linear_input_property_;
// True for encodings whose pixel values need a min/max range mapping before
// they can be shown as 8-bit RGB: float, single-channel 16-bit, and 16-bit
// Bayer. Controls the visibility of the Normalize Range / Min Value /
// Max Value / Median window properties.
bool has_normalizable_range_;
// True when the last received message was Bayer;
// gates visibility of the Linear Input property.
bool has_bayer_encoding_;
};

} // namespace displays
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ class UnsupportedImageEncoding : public std::runtime_error
{}
};

// A message whose encoding is supported but whose fields are inconsistent
// with each other or with the data buffer
// (zero / oversize dimensions, bad step, truncated data).
class MalformedImageMessage : public std::runtime_error
{
public:
explicit MalformedImageMessage(const std::string & description)
: std::runtime_error("Malformed image message: " + description)
{}
};

struct ImageData final
{
ImageData(
Expand Down Expand Up @@ -125,6 +136,9 @@ class ROSImageTexture : public ROSImageTextureIface
RVIZ_DEFAULT_PLUGINS_PUBLIC
void setSmoothScaling(bool enabled) override;

RVIZ_DEFAULT_PLUGINS_PUBLIC
void setLinearInput(bool enabled) override;

private:
// Ensures the underlying Ogre texture matches the requested dimensions,
// pixel format and smooth-scaling state, recreating it if any of those
Expand All @@ -143,6 +157,8 @@ class ROSImageTexture : public ROSImageTextureIface
ImageData convertUYVYToRGBData(const uint8_t * data_ptr, size_t data_size_in_bytes);
ImageData convertYUYVToRGBData(const uint8_t * data_ptr, size_t data_size_in_bytes);
ImageData convertNV12ToRGBData(const uint8_t * data_ptr, size_t data_size_in_bytes);
ImageData convertBayerToRGBData(
const std::string & encoding, const uint8_t * data_ptr, size_t data_size_in_bytes);

ImageData setFormatAndNormalizeDataIfNecessary(
const std::string & encoding, const uint8_t * data_ptr, size_t data_size_in_bytes);
Expand Down Expand Up @@ -173,6 +189,10 @@ class ROSImageTexture : public ROSImageTextureIface
// only briefly between setSmoothScaling and the next ensureTexture call.
bool smooth_scaling_;
bool tex_smooth_;
// When true, apply sRGB gamma encoding before emitting 8-bit output.
// When false, values pass through the linear rescale straight to 8-bit.
// Currently only applied by the bayer demosaicing path.
bool linear_input_;
uint32_t tex_width_;
uint32_t tex_height_;
Ogre::PixelFormat tex_format_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ class RVIZ_DEFAULT_PLUGINS_PUBLIC ROSImageTextureIface
// no-op so that out-of-tree implementors of this interface keep building
// and silently ignore the setting until they opt in.
virtual void setSmoothScaling(bool /*enabled*/) {}

// When enabled, apply sRGB gamma to converter output instead of passthrough.
// Default no-op for out-of-tree implementors.
virtual void setLinearInput(bool /*enabled*/) {}
Comment on lines +76 to +78

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, together with a few other changes, breaks ABI, therefore to ease backporting this, I suggest I split this PR up in two - one that adds pure bayer support, without the "Linear input" toggle that breaks ABI, and a second PR which adds that toggle (and the associated logic)? We can then rather easily backport the former, and just not backport the latter (as that toggle is only of secondary importance).

};

} // namespace displays
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,11 +444,20 @@ void CameraDisplay::update(std::chrono::nanoseconds wall_dt, std::chrono::nanose
(void) wall_dt;
(void) ros_dt;
try {
if (texture_->update() || force_render_) {
const bool new_image = texture_->update();
if (new_image) {
// A new frame was converted and uploaded; clear any error a previous
// (e.g. malformed) frame may have latched.
setStatus(StatusLevel::Ok, "Image", "OK");
}
if (new_image || force_render_) {
caminfo_ok_ = updateCamera();
force_render_ = false;
}
} catch (UnsupportedImageEncoding & e) {
} catch (std::exception & e) {
// UnsupportedImageEncoding, MalformedImageMessage, and also allocation
// failure on absurdly large frames - degrade to an error status rather
// than letting the exception take down the application.
setStatus(StatusLevel::Error, "Image", e.what());
}
}
Expand Down
Loading