@@ -7,6 +7,8 @@ shader_files = files([
'bayer_unpacked.frag',
'bayer_unpacked.vert',
'identity.vert',
+ 'raw_mono_1x_packed.frag',
+ 'raw_mono_unpacked.frag',
])
# Generate header from shaders
new file mode 100644
@@ -0,0 +1,75 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2026, Alain CousiniƩ
+ * raw_mono_1x_packed.frag - Fragment shader for packed monochrome formats (R10_CSI2P & R12_CSI2P)
+ */
+
+#ifdef GL_ES
+precision highp float;
+#endif
+
+varying vec2 textureOut;
+
+uniform vec2 tex_size;
+uniform sampler2D tex_y;
+uniform float gamma;
+uniform float contrastExp;
+
+float apply_contrast(float value)
+{
+ if (value < 0.5)
+ return 0.5 * pow(value / 0.5, contrastExp);
+ else
+ return 1.0 - 0.5 * pow((1.0 - value) / 0.5, contrastExp);
+}
+
+float read_byte(float pixel_x, float pixel_y)
+{
+ vec2 uv = vec2(pixel_x + 0.5, pixel_y + 0.5) / tex_size;
+ return floor(texture2D(tex_y, uv).r * 255.0 + 0.5);
+}
+
+void main(void)
+{
+ vec2 pixel_coords = floor(textureOut * tex_size);
+ float px = pixel_coords.x;
+ float py = pixel_coords.y;
+
+ float gray_val = 0.0;
+
+#ifdef MONO_RAW12
+ int index = int(mod(px, 2.0));
+ float block_start = floor(px / 2.0) * 3.0;
+
+ float g = read_byte(block_start + float(index), py);
+ float lsb = read_byte(block_start + 2.0, py);
+
+ int lsb_shift = (index == 0) ? 2 : 6;
+ float lsb_bits = mod(floor(lsb / pow(2.0, float(lsb_shift))), 4.0);
+
+ gray_val = (g * 4.0) + lsb_bits;
+#else
+ int index = int(mod(px, 4.0));
+ float block_start = floor(px / 4.0) * 5.0;
+
+ float g = read_byte(block_start + float(index), py);
+ float lsb = read_byte(block_start + 4.0, py);
+
+ int lsb_shift = index * 2;
+ float lsb_bits = mod(floor(lsb / pow(2.0, float(lsb_shift))), 4.0); // & 0x03
+
+ gray_val = (g * 4.0) + lsb_bits;
+#endif
+
+ float C = gray_val / 1023.0;
+ vec3 rgb = vec3(C, C, C);
+
+ /* Generic rendering processing */
+ rgb = clamp(rgb, 0.0, 1.0);
+ rgb.r = apply_contrast(rgb.r);
+ rgb.g = apply_contrast(rgb.g);
+ rgb.b = apply_contrast(rgb.b);
+ rgb = pow(rgb, vec3(gamma));
+
+ gl_FragColor = vec4(rgb, 1.0);
+}
new file mode 100644
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2026, Alain CousiniƩ
+ * raw_mono_unpacked.frag - Fragment shader for raw unpacked monochrome formats (R8, R10, R12)
+ */
+
+#extension GL_EXT_texture_rg : enable
+
+#ifdef GL_ES
+precision highp float;
+#endif
+
+varying vec4 center;
+varying vec4 xCoord;
+varying vec4 yCoord;
+
+uniform sampler2D tex_y;
+uniform float gamma;
+uniform float contrastExp;
+
+float apply_contrast(float value)
+{
+ if (value < 0.5)
+ return 0.5 * pow(value / 0.5, contrastExp);
+ else
+ return 1.0 - 0.5 * pow((1.0 - value) / 0.5, contrastExp);
+}
+
+void main(void)
+{
+ vec4 texel = texture2D(tex_y, center.xy);
+
+ float byte_low = floor(texel.r * 255.0 + 0.5);
+ float byte_high = floor(texel.g * 255.0 + 0.5);
+
+ float native_val = (byte_high * 256.0) + byte_low;
+
+ float gray_bits = 0.0;
+
+#ifdef MONO_RAW12
+ gray_bits = floor(native_val / 4.0);
+#else
+ gray_bits = native_val;
+#endif
+
+ float C = gray_bits / 1023.0;
+
+ vec3 rgb = vec3(C, C, C);
+
+ /* Generic rendering processing */
+ rgb = clamp(rgb, 0.0, 1.0);
+ rgb.r = apply_contrast(rgb.r);
+ rgb.g = apply_contrast(rgb.g);
+ rgb.b = apply_contrast(rgb.b);
+ rgb = pow(rgb, vec3(gamma));
+
+ gl_FragColor = vec4(rgb, 1.0);
+}
@@ -424,6 +424,72 @@ void DebayerCpu::debayer12P_RGRG_BGR888(uint8_t *dst, const uint8_t *src[])
}
}
+template<bool addAlphaByte, bool ccmEnabled>
+void DebayerCpu::debayer_Mono(uint8_t *dst, const uint8_t *src[])
+{
+ if (inputConfig_.bpp == 8) {
+ const uint8_t *cur = src[1] + xShift_;
+
+ for (int x = 0; x < (int)window_.width;) {
+ uint16_t pixel_val = *cur++;
+
+ STORE_PIXEL(pixel_val, pixel_val, pixel_val)
+ }
+ return;
+ }
+
+ const uint8_t *cur = src[1] + (xShift_ * 2);
+ const int shift = (inputConfig_.bpp == 10) ? 6 : 2;
+ for (int x = 0; x < (int)window_.width;) {
+ uint8_t low_byte = *cur++;
+ uint8_t high_byte = *cur++;
+
+ uint16_t native_val = (static_cast<uint16_t>(high_byte) << 8) | low_byte;
+ uint16_t pixel_val = native_val >> shift;
+
+ STORE_PIXEL(pixel_val, pixel_val, pixel_val)
+ }
+}
+
+template<bool addAlphaByte, bool ccmEnabled>
+void DebayerCpu::debayerP_Mono(uint8_t *dst, const uint8_t *src[])
+{
+ const uint8_t *cur = src[1];
+
+ if (inputConfig_.bpp == 10) {
+ for (int x = 0; x < (int)window_.width;) {
+ uint16_t g0 = *cur++;
+ uint16_t g1 = *cur++;
+ uint16_t g2 = *cur++;
+ uint16_t g3 = *cur++;
+ uint8_t lsb = *cur++;
+
+ uint16_t gray0 = (g0 << 2) | (lsb & 0x03);
+ uint16_t gray1 = (g1 << 2) | ((lsb >> 2) & 0x03);
+ uint16_t gray2 = (g2 << 2) | ((lsb >> 4) & 0x03);
+ uint16_t gray3 = (g3 << 2) | ((lsb >> 6) & 0x03);
+
+ STORE_PIXEL(gray0, gray0, gray0)
+ STORE_PIXEL(gray1, gray1, gray1)
+ STORE_PIXEL(gray2, gray2, gray2)
+ STORE_PIXEL(gray3, gray3, gray3)
+ }
+ return;
+ }
+ /* 12 bits Packed */
+ for (int x = 0; x < (int)window_.width;) {
+ uint16_t g0 = *cur++;
+ uint16_t g1 = *cur++;
+ uint8_t lsb = *cur++;
+
+ uint16_t gray0 = (g0 << 2) | ((lsb >> 2) & 0x03);
+ uint16_t gray1 = (g1 << 2) | ((lsb >> 6) & 0x03);
+
+ STORE_PIXEL(gray0, gray0, gray0)
+ STORE_PIXEL(gray1, gray1, gray1)
+ }
+}
+
/*
* Setup the Debayer object according to the passed in parameters.
* Return 0 on success, a negative errno value on failure
@@ -439,6 +505,21 @@ int DebayerCpu::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
formats::BGR888,
formats::XBGR8888,
formats::ABGR8888 };
+ if (bayerFormat.order == BayerFormat::Order::MONO) {
+ if (bayerFormat.packing == BayerFormat::Packing::None) {
+ config.bpp = (bayerFormat.bitDepth + 7) & ~7;
+ config.patternSize.width = 2;
+ config.patternSize.height = 2;
+ }
+ else if (bayerFormat.packing == BayerFormat::Packing::CSI2) {
+ config.bpp = bayerFormat.bitDepth;
+ config.patternSize.width = (bayerFormat.bitDepth == 10) ? 4 : 2;
+ config.patternSize.height = 2;
+ }
+
+ config.outputFormats = outputFormats;
+ return 0;
+ }
if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10 || bayerFormat.bitDepth == 12) &&
bayerFormat.packing == BayerFormat::Packing::None &&
@@ -525,6 +606,21 @@ int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat,
return -EINVAL;
};
+ if (bayerFormat.order == BayerFormat::Order::MONO) {
+ if (outputFormat == formats::XRGB8888 || outputFormat == formats::ARGB8888 ||
+ outputFormat == formats::XBGR8888 || outputFormat == formats::ABGR8888) {
+ addAlphaByte = true;
+ }
+ if (bayerFormat.packing == BayerFormat::Packing::None) {
+ SET_DEBAYER_METHODS(debayer_Mono, debayer_Mono)
+ return 0;
+ } else if (bayerFormat.packing == BayerFormat::Packing::CSI2) {
+ SET_DEBAYER_METHODS(debayerP_Mono, debayerP_Mono)
+ return 0;
+ }
+ return invalidFmt();
+ }
+
switch (outputFormat) {
case formats::XRGB8888:
case formats::ARGB8888:
@@ -119,6 +119,11 @@ private:
void debayer12P_GBGB_BGR888(uint8_t *dst, const uint8_t *src[]);
template<bool addAlphaByte, bool ccmEnabled>
void debayer12P_RGRG_BGR888(uint8_t *dst, const uint8_t *src[]);
+ /* CSI-2 packed and unpacked raw mono */
+ template<bool addAlphaByte, bool ccmEnabled>
+ void debayer_Mono(uint8_t *dst, const uint8_t *src[]);
+ template<bool addAlphaByte, bool ccmEnabled>
+ void debayerP_Mono(uint8_t *dst, const uint8_t *src[]);
static int getInputConfig(PixelFormat inputFormat, DebayerInputConfig &config);
int setupStandardBayerOrder(BayerFormat::Order order);
@@ -61,6 +61,17 @@ int DebayerEGL::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
formats::XBGR8888,
formats::ABGR8888 };
+ /* Handle Monochrome case using bayerFormat */
+ if (bayerFormat.order == BayerFormat::Order::MONO) {
+ bool isPacked = (bayerFormat.packing == BayerFormat::Packing::CSI2);
+
+ config.bpp = isPacked ? bayerFormat.bitDepth : ((bayerFormat.bitDepth + 7) & ~7);
+ config.patternSize.width = isPacked ? 4 : 2;
+ config.patternSize.height = 2;
+ config.outputFormats = outputFormats;
+ return 0;
+ }
+
if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10) &&
bayerFormat.packing == BayerFormat::Packing::None &&
isStandardBayerOrder(bayerFormat.order)) {
@@ -166,6 +177,13 @@ int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
shaderStridePixels_ = inputConfig_.stride;
switch (inputFormat) {
+ /* Monochrome cases: No color: phase, proper initialization to 0 for Rx and Rx_CSI2P */
+ case libcamera::formats::R8:
+ case libcamera::formats::R10:
+ case libcamera::formats::R12:
+ firstRed_x_ = 0.0;
+ firstRed_y_ = 0.0;
+ break;
case libcamera::formats::SBGGR8:
case libcamera::formats::SBGGR10_CSI2P:
case libcamera::formats::SBGGR12_CSI2P:
@@ -197,6 +215,33 @@ int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
/* Shader selection */
switch (inputFormat) {
+ case libcamera::formats::R8: {
+ fragmentShaderData = raw_mono_unpacked_frag;
+ vertexShaderData = bayer_unpacked_vert;
+ glFormat_ = GL_LUMINANCE;
+ bytesPerPixel_ = 1;
+ break;
+ }
+ case libcamera::formats::R12:
+ egl_.pushEnv(shaderEnv, "#define MONO_RAW12");
+ [[fallthrough]];
+ case libcamera::formats::R10: {
+ BayerFormat bayerFormat = BayerFormat::fromPixelFormat(inputFormat);
+
+ if (bayerFormat.packing == BayerFormat::Packing::None) {
+ fragmentShaderData = raw_mono_unpacked_frag;
+ vertexShaderData = bayer_unpacked_vert;
+ glFormat_ = GL_RG;
+ bytesPerPixel_ = 2;
+ } else {
+ fragmentShaderData = raw_mono_1x_packed_frag;
+ vertexShaderData = identity_vert;
+ glFormat_ = GL_LUMINANCE;
+ bytesPerPixel_ = 1;
+ shaderStridePixels_ = inputConfig_.stride;
+ }
+ break;
+ }
case libcamera::formats::SBGGR8:
case libcamera::formats::SGBRG8:
case libcamera::formats::SGRBG8:
@@ -516,6 +561,13 @@ eGLImage *DebayerEGL::getCachedInputFrameBuffer(FrameBuffer *input, std::optiona
eglImageInCache_.emplace_back(fd, std::make_unique<eGLImage>(glFormat_, inputConfig_.stride / bytesPerPixel_, height_, inputConfig_.stride, GL_TEXTURE0, 0));
eglImageIn = eglImageInCache_.back().second.get();
+ /* Force GL_NEAREST to preserve LSBs of packed monochrome format */
+ BayerFormat bayerFormat = BayerFormat::fromPixelFormat(inputPixelFormat_);
+ if (bayerFormat.order == BayerFormat::Order::MONO && bayerFormat.packing == BayerFormat::Packing::CSI2) {
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ }
+
if (egl_.createInputDMABufTexture2D(*eglImageIn, input->planes()[0].fd.get()) == 0)
return eglImageIn;
@@ -494,6 +494,38 @@ int SwStatsCpu::configure(const StreamConfiguration &inputCfg, unsigned int stat
uint8_t bitDepth = bayerFormat.bitDepth;
+ if (bayerFormat.order == BayerFormat::Order::MONO) {
+ if (bayerFormat.packing == BayerFormat::Packing::None) {
+ patternSize_.width = 2;
+ patternSize_.height = 2;
+
+ if (bitDepth == 8) {
+ stats0_ = &SwStatsCpu::statsBGGR8Line0;
+ sumShift_ = 0;
+ } else if (bitDepth == 10) {
+ stats0_ = &SwStatsCpu::statsBGGR10Line0;
+ sumShift_ = 2;
+ } else { /* 12 bits */
+ stats0_ = &SwStatsCpu::statsBGGR12Line0;
+ sumShift_ = 4;
+ }
+ }
+ else if (bayerFormat.packing == BayerFormat::Packing::CSI2) {
+ patternSize_.width = (bitDepth == 10) ? 4 : 2;
+ patternSize_.height = 2;
+
+ stats0_ = (bitDepth == 10) ? &SwStatsCpu::statsBGGR10PLine0 : &SwStatsCpu::statsBGGR12PLine0;
+ sumShift_ = 0;
+ }
+
+ ySkipMask_ = 0x02; /* Skip every 3th and 4th line */
+ xShift_ = 0;
+ swapLines_ = false;
+ processFrame_ = &SwStatsCpu::processBayerFrame2;
+
+ return 0;
+ }
+
if ((bitDepth == 10 || bitDepth == 12) &&
bayerFormat.packing == BayerFormat::Packing::CSI2) {
if (bitDepth == 10)