Skip to content

Feature: support bayer encoding - #1749

Open
abaeyens wants to merge 10 commits into
ros2:rollingfrom
abaeyens:abaeyens/rolling/support-bayer-encoding
Open

Feature: support bayer encoding#1749
abaeyens wants to merge 10 commits into
ros2:rollingfrom
abaeyens:abaeyens/rolling/support-bayer-encoding

Conversation

@abaeyens

@abaeyens abaeyens commented May 25, 2026

Copy link
Copy Markdown
Contributor

Description

Handle the eight bayer encodings ROS Image messages can have (= 4 sensel layouts for 8- and 16-bit). Given that bayer images tend to be in linear light, also add a "Linear input" toggle, similar to the one recently introduced in rqt_image_view (though here it's currently only active for bayer images).

Fixes #887

Screenshot of a bayer image with these changes:

Screenshot from 2026-05-25 17-11-07

Is this user-facing behavior change?

Yes, with this PR bayer-encoded images are shown in full color instead of in grey with a checkerboard pattern (for 8-bit bayer encodings), screenshot:

Screenshot from 2026-05-25 17-38-18

... or completely unsupported for the case of 16-bit, given "No Image" and the following error:

[ERROR] [1779734902.069680247] [rviz2]: Error loading image: InvalidParametersException: Stream size does not match calculated image size in Image::loadRawData at /home/arend/ros2_rolling/build/rviz_ogre_vendor/ogre_vendor-prefix/src/ogre_vendor/OgreMain/src/OgreImage.cpp (line 318)

Did you use Generative AI?

Claude Opus 4.7, Claude Fable 5

Additional Information

Work in progress, I'm aware of several things to clean up. That said, please feel free to already provide feedback on the architecture and functional aspects. => ready for review

A remaining question for me is how to handle linear vs sRGB color space (see also my question here). Currently, this PR considers all bayer-encoded images to be expressed in linear light. If they instead follow an sRGB-like OETF, then they'll be shown with their shadows too bright. One solution is to add a toggle "Linear input" like we're doing for RQt. => added toggle "Linear input"

abaeyens added 2 commits May 25, 2026 15:24
Resolves ros2#887. Bayer encodings (bayer_rggb8 / bayer_bggr8 / bayer_gbrg8 /
bayer_grbg8 and their 16-bit variants) were previously routed to a stub
that handed the raw mosaic to Ogre as PF_BYTE_L, rendering as a visible
checkerboard. They are now bilinearly demosaiced to sRGB-encoded RGB.

The demosaic is hand-rolled to avoid pulling in OpenCV / cv_bridge as a
dependency, matching the policy established in
ros-visualization/rviz#1009 and the existing YUYV / UYVY / NV12
converter pattern in this file. A single templated implementation
(parameterised on the input pixel type and on the (R_DY, R_DX) position
of R within the 2x2 mosaic cell) covers all four layouts at both 8-bit
and 16-bit. The interior loop is bounds-check-free; four separate border
passes handle the 1-pixel edge with clamp-to-edge and a count-of-valid-
neighbours average. Each demosaiced linear value passes through an
sRGB transfer-function LUT (256 entries at 8-bit, 65536 entries / 64 KiB
at 16-bit, built once on first use). The sRGB step is always-on for
Bayer because the bilinear output is linear-light and the texture is
displayed as sRGB; a user-toggleable linear-input property is left for
a follow-up if a real use case appears.

For 16-bit Bayer, the existing Normalize Range / Min Value / Max Value /
Median window properties are reused. Cameras packing 10/12/14-bit data
into 16-bit messages can be displayed correctly by disabling Normalize
Range and setting Max Value to the actual bit-depth maximum
(1023 / 4095 / 16383). Min/max statistics are computed over the input
mosaic (not the 3x-larger demosaiced output) using a stride-aware
row-by-row scan that ignores padding bytes between rows; this scan and
the bayer_16bit_padded_stride regression test become removable once
PR ros2#1719 lands and repacks padded buffers in update() before dispatch
(see in-file comment for cleanup notes in both directions).

Input validation rejects malformed messages upfront with
UnsupportedImageEncoding: zero or oversize dimensions (> 32768),
inconsistent stride (< width * bpp), truncated data (< height * stride),
odd stride for 16-bit, and misaligned data_ptr for 16-bit (the latter
guards against strict-alignment platforms like ARMv7 where the
uint16_t reinterpret_cast would otherwise be undefined behaviour).
Unknown bayer_* encodings now throw rather than being silently rendered
as luminance; a small deliberate user-visible behaviour change.

In ImageDisplay, the private got_float_image_ flag is renamed to
has_normalizable_range_ to reflect that it now also gates the
normalize controls for 16-bit Bayer. Tooltip on Normalize Range is
extended to mention Bayer applicability.

Tests cover: all four 8-bit layouts (pure-red and pure-blue, which are
discriminating across layouts because a layout-swap bug surfaces as
swapped output channels); all four 16-bit layouts with fixed Max Value;
16-bit black-point via Min Value; running-median smoke test for the
auto-normalisation path; padded-stride regression test (verified to
fail under the previous contiguous min/max scan); scale-then-sRGB
ordering; multi-anchor sRGB transfer-function validation across the
linear toe, the knee at 0.0031308, and the upper exponent range;
small-image edge cases (2x2 and 3x3); unknown-encoding rejection; and
oversize / truncated / odd-stride-16-bit rejection.

Signed-off-by: Arne Baeyens <mail@arnebaeyens.com>
The hover-over pixel readout introduced in ros2#1716 has no Bayer branch,
so for bayer_* encodings it falls through to the raw-byte fallback at
image_display.cpp:592, showing one byte for 8-bit Bayer or two raw
bytes for 16-bit Bayer with no channel context. This pre-existed our
demosaic work but is far more visible now that Bayer images actually
render correctly.

Add a Bayer-aware branch to formatPixel that, given a hover position
(px, py), reads the four sensel values from the 2x2 mosaic cell
anchored at (px & ~1, py & ~1) and labels each by its channel based on
the layout (R / G / G / B for RGGB; mirrored for the other three). The
readout is honest about what the sensor recorded — the on-screen pixel
values shown are interpolated, the readout values are not. Output is
HTML-formatted to match the existing colour-coded RGB8 / RGBA8 styles.
Edge pixels clamp neighbour coordinates to the image bounds, matching
the demosaic's edge handling.

No unit test added; the existing image_display.cpp pixel-readout
helpers are in an anonymous namespace and the formatPixelAt accessor
is private. Visual verification with a Bayer publisher is sufficient
for this UI-formatting change, matching the precedent of ros2#1716 itself.

Signed-off-by: Arne Baeyens <mail@arnebaeyens.com>
abaeyens added 5 commits July 5, 2026 18:49
…upport-bayer-encoding

Signed-off-by: Arne Baeyens <mail@arnebaeyens.com>

Conflicts:
	rviz_default_plugins/include/rviz_default_plugins/displays/image/image_display.hpp
	rviz_default_plugins/src/rviz_default_plugins/displays/image/image_display.cpp
	rviz_default_plugins/test/rviz_default_plugins/displays/image/ros_image_texture_test.cpp
The previous "Bayer input is linear-light, apply sRGB gamma" rule was
hardcoded. Expose it as a per-display "Linear input" BoolProperty on
ImageDisplay; hidden unless the current message is Bayer; default off.
Toggling on a latched frame re-runs the converter without needing a new
publish (mirrors setSmoothScaling).

Now that PR ros2#1719 (buffer repack in update()) has landed
upstream:
- Replace the inline stride-aware 16-bit min/max scan with a call to
  the shared getMinimalAndMaximalValueToNormalize<uint16_t>.
- Delete unreachable defensive stride/alignment checks and the
  now-obsolete odd_stride_16bit_rejected test.

Also: parameterise linear-passthrough tests over all four Bayer layouts
(8 and 16-bit); add tests for fixed-max above actual, median-frames
tracking, toggle round-trip on a held frame, and initialize-time
propagation of the default. Fix a -Wreorder in the constructor init
list. Name the sRGB piecewise constants (IEC 61966-2-1). Apply
ament_uncrustify to ros_image_texture.cpp.

Signed-off-by: Arne Baeyens <mail@arnebaeyens.com>
The two header files only gained small declaration additions (a setter
override, a Q_SLOT, a couple of tracking bools). That is below the
threshold for a new copyright attribution; the substantive work lives
in the corresponding .cpp files and the test file, which keep the
2026 line.

Signed-off-by: Arne Baeyens <mail@arnebaeyens.com>
Test-quality fixes:
- srgb_transfer_anchor_points: drop the five tautological EXPECT_* against
  the test-side srgbEncodeByte reference; the remaining test drives four
  anchor input values (0, 1, 128, 255) through ROSImageTexture and checks
  the demosaic output. Comment that the piecewise-linear segment is
  unreachable via 8-bit input.
- bayer_16bit_padded_stride: fix a misleading docstring (update() repacks
  the buffer, so the min/max scan never sees padding) and drop the
  incorrect "padding biased min_value to 0" reasoning on the G/B
  assertions.
- small_{2,3}x{2,3}_image tests: pure-red mosaic + per-pixel R >= 100 and
  G = B = 0 assertions instead of asserting only rgb.size().
- bayer_16bit_normalize_range_smoke: deleted; the running-median path is
  now covered by bayer_16bit_median_frames_tracks_median_not_last with
  proper output assertions.
- bayer_16bit_srgb_after_scaling / linear-mode fixed-max helpers: move the
  ASSERT_GT/LT sentries from the test-side reference onto the actual
  output. A wrong-ordering or auto-detect bug now surfaces as a pixel
  mismatch, not a test-reference sanity failure.

Consolidation:
- buildUniformMosaic8 / buildUniformMosaic16 folded into one templated
  buildUniformMosaic<T>; 14 call sites updated.
- checkPureRedDemosaicSrgb / checkPureBlueDemosaicSrgb folded into one
  checkPureChannelDemosaicSrgb(encoding, channel).
- Twenty hand-copied TEST_F entries collapsed into four TEST_P suites
  (Bayer8bitSrgbPurChannelTest, Bayer16bitSrgbFixedMaxTest,
  Bayer8bitLinearTest, Bayer16bitLinearFixedMaxTest) parameterised over
  the four Bayer layouts. RosImageTextureTestFixture::SetUpTestCase is
  guarded against re-entry so parameterised subclasses can share the
  singleton Ogre environment.

Display test:
- initialize_propagates_linear_input_default_to_texture: tighten
  Times(AtLeast(1)) -> Times(1) so a redundant propagation would surface.

Deferred for now: display-side visibility gating (needs a test shim for
protected processMessage), interior-vs-border assertions on the 8x8 pure-
red tests, uppercase encoding fallthrough test, readTextureRGB format
assertion.

Signed-off-by: Arne Baeyens <mail@arnebaeyens.com>
- Move the encoding -> layout / bit-depth table into bayer_format.hpp as
  the single source of truth, shared by the demosaic, the property
  gating, and the pixel read-out; gate the Bayer-dependent UI on this
  whitelist rather than image_encodings::isBayer()
- Report malformed frames (zero/oversize dimensions, bad step, truncated
  data) via a new MalformedImageMessage exception naming the actual
  fault, instead of blaming the encoding
- Reset the Image status to OK once a subsequent frame converts cleanly,
  so a transient bad frame no longer latches a permanent error; broaden
  the display catch to std::exception so allocation failure degrades to
  an error status instead of aborting
- Bound the total pixel count (2^28) in addition to the per-dimension
  cap, and require exactly packed rows, making the precondition of the
  contiguous 16-bit min/max scan explicit
- Fold the 16-bit rescale (and optional sRGB transfer) into a per-frame
  lookup table so the demosaic inner loop does one table lookup per
  channel instead of floating-point math
- Match convertTo8bit's isfinite() guard; avoid a std::clamp
  precondition violation in the pixel read-out for absurd dimensions
- Tests: drop a stale comment, fix a fixture typo, unify fixture
  suffixes, drop assertions implied by adjacent EXPECT_NEARs, split a
  two-behaviour test
- Mention Bayer support in docs/FEATURES.md

Signed-off-by: Arne Baeyens <mail@arnebaeyens.com>
@abaeyens
abaeyens force-pushed the abaeyens/rolling/support-bayer-encoding branch from 0fd6136 to 477749f Compare July 6, 2026 21:01
@abaeyens
abaeyens marked this pull request as ready for review July 6, 2026 21:02
@mergify

mergify Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

abaeyens added 2 commits July 6, 2026 23:14
Signed-off-by: Arne Baeyens <mail@arnebaeyens.com>
no code changes, only reordening

Signed-off-by: Arne Baeyens <mail@arnebaeyens.com>
Comment on lines +76 to +78
// When enabled, apply sRGB gamma to converter output instead of passthrough.
// Default no-op for out-of-tree implementors.
virtual void setLinearInput(bool /*enabled*/) {}

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

#include "rviz_rendering/render_window.hpp"
#include "sensor_msgs/image_encodings.hpp"

#include "./bayer_format.hpp"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

don't use relative paths

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.

Sure, fixed in e6f9b9d.

Signed-off-by: Arne Baeyens <mail@arnebaeyens.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error displaying bayer_rggb8 encoded images.

2 participants