Commit fa8575cc for libheif
commit fa8575cc76b0f1181f0623d9e03c6b868989538a
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Thu Feb 26 11:45:04 2026 +0100
add debayering color conversion
diff --git a/examples/heif_gen_bayer.cc b/examples/heif_gen_bayer.cc
index adc17e6e..73106afb 100644
--- a/examples/heif_gen_bayer.cc
+++ b/examples/heif_gen_bayer.cc
@@ -252,7 +252,7 @@ int main(int argc, char* argv[])
heif_image* bayer_img = nullptr;
err = heif_image_create(width, height,
- heif_colorspace_monochrome,
+ heif_colorspace_filter_array,
heif_chroma_monochrome,
&bayer_img);
if (err.code != heif_error_Ok) {
diff --git a/libheif/CMakeLists.txt b/libheif/CMakeLists.txt
index 4d00325a..ca0a0154 100644
--- a/libheif/CMakeLists.txt
+++ b/libheif/CMakeLists.txt
@@ -172,6 +172,8 @@ set(libheif_sources
color-conversion/alpha.h
color-conversion/chroma_sampling.cc
color-conversion/chroma_sampling.h
+ color-conversion/bayer_bilinear.cc
+ color-conversion/bayer_bilinear.h
sequences/seq_boxes.h
sequences/seq_boxes.cc
sequences/chunk.h
diff --git a/libheif/codecs/uncompressed/unc_codec.cc b/libheif/codecs/uncompressed/unc_codec.cc
index 76ea42bf..f9d54f8c 100644
--- a/libheif/codecs/uncompressed/unc_codec.cc
+++ b/libheif/codecs/uncompressed/unc_codec.cc
@@ -131,7 +131,7 @@ Error UncompressedImageCodec::get_heif_chroma_uncompressed(const std::shared_ptr
if (componentSet == (1 << heif_uncompressed_component_type_filter_array)) {
// TODO - we should look up the components
*out_chroma = heif_chroma_monochrome;
- *out_colourspace = heif_colorspace_monochrome;
+ *out_colourspace = heif_colorspace_filter_array;
}
// TODO: more combinations
diff --git a/libheif/codecs/uncompressed/unc_decoder.cc b/libheif/codecs/uncompressed/unc_decoder.cc
index 78659e6b..eade4360 100644
--- a/libheif/codecs/uncompressed/unc_decoder.cc
+++ b/libheif/codecs/uncompressed/unc_decoder.cc
@@ -455,7 +455,19 @@ Result<std::shared_ptr<HeifPixelImage> > unc_decoder::decode_full_image(
auto img = *createImgResult;
if (properties.cpat) {
- img->set_bayer_pattern(properties.cpat->get_pattern());
+ // Resolve cpat component indices to actual component types from cmpd.
+ BayerPattern pattern = properties.cpat->get_pattern();
+ const auto& cmpd_components = cmpd->get_components();
+ for (auto& pixel : pattern.pixels) {
+ uint16_t idx = pixel.component_type;
+ if (idx >= cmpd_components.size()) {
+ return Error(heif_error_Invalid_input,
+ heif_suberror_Invalid_parameter_value,
+ "cpat component index out of range");
+ }
+ pixel.component_type = cmpd_components[idx].component_type;
+ }
+ img->set_bayer_pattern(pattern);
}
auto decoderResult = unc_decoder_factory::get_unc_decoder(width, height, cmpd, uncC);
diff --git a/libheif/color-conversion/bayer_bilinear.cc b/libheif/color-conversion/bayer_bilinear.cc
new file mode 100644
index 00000000..d6d08666
--- /dev/null
+++ b/libheif/color-conversion/bayer_bilinear.cc
@@ -0,0 +1,211 @@
+/*
+ * HEIF codec.
+ * Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+ *
+ * This file is part of libheif.
+ *
+ * libheif is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * libheif is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libheif. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "bayer_bilinear.h"
+#include <libheif/heif_uncompressed.h>
+#include <array>
+#include <cassert>
+
+
+std::vector<ColorStateWithCost>
+Op_bayer_bilinear_to_RGB24_32::state_after_conversion(const ColorState& input_state,
+ const ColorState& target_state,
+ const heif_color_conversion_options& options,
+ const heif_color_conversion_options_ext& options_ext) const
+{
+ if (input_state.colorspace != heif_colorspace_filter_array ||
+ input_state.chroma != heif_chroma_monochrome) {
+ return {};
+ }
+
+ std::vector<ColorStateWithCost> states;
+
+ ColorState output_state;
+ output_state.colorspace = heif_colorspace_RGB;
+ output_state.has_alpha = false;
+
+ if (input_state.bits_per_pixel == 8) {
+ output_state.chroma = heif_chroma_interleaved_RGB;
+ output_state.bits_per_pixel = 8;
+ }
+ else if (input_state.bits_per_pixel > 8 && input_state.bits_per_pixel <= 16) {
+ output_state.chroma = heif_chroma_interleaved_RRGGBB_LE;
+ output_state.bits_per_pixel = input_state.bits_per_pixel;
+ }
+ else {
+ return {};
+ }
+
+ states.emplace_back(output_state, SpeedCosts_Unoptimized);
+
+ return states;
+}
+
+
+// Map uncompressed component types to R/G/B output channel indices.
+// Returns -1 for unknown component types.
+static int component_type_to_rgb_index(uint16_t component_type)
+{
+ switch (component_type) {
+ case heif_uncompressed_component_type_red:
+ return 0;
+ case heif_uncompressed_component_type_green:
+ return 1;
+ case heif_uncompressed_component_type_blue:
+ return 2;
+ default:
+ return -1;
+ }
+}
+
+
+Result<std::shared_ptr<HeifPixelImage>>
+Op_bayer_bilinear_to_RGB24_32::convert_colorspace(const std::shared_ptr<const HeifPixelImage>& input,
+ const ColorState& input_state,
+ const ColorState& target_state,
+ const heif_color_conversion_options& options,
+ const heif_color_conversion_options_ext& options_ext,
+ const heif_security_limits* limits) const
+{
+ uint32_t width = input->get_width();
+ uint32_t height = input->get_height();
+
+ if (!input->has_bayer_pattern()) {
+ return Error::InternalError;
+ }
+
+ const BayerPattern& pattern = input->get_bayer_pattern();
+ uint16_t pw = pattern.pattern_width;
+ uint16_t ph = pattern.pattern_height;
+
+ if (pw == 0 || ph == 0) {
+ return Error::InternalError;
+ }
+
+ int bpp = input->get_bits_per_pixel(heif_channel_filter_array);
+ bool hdr = bpp > 8;
+
+ heif_chroma out_chroma = hdr ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RGB;
+
+ auto outimg = std::make_shared<HeifPixelImage>();
+
+ outimg->create(width, height, heif_colorspace_RGB, out_chroma);
+
+ if (auto err = outimg->add_plane(heif_channel_interleaved, width, height, bpp, limits)) {
+ return err;
+ }
+
+ size_t in_stride = 0;
+ const uint8_t* in_p = input->get_plane(heif_channel_filter_array, &in_stride);
+
+ size_t out_stride = 0;
+ uint8_t* out_p = outimg->get_plane(heif_channel_interleaved, &out_stride);
+
+ // Build a lookup table: for each pattern position, which RGB channel (0=R,1=G,2=B) does it provide?
+ std::vector<int> pattern_channel(pw * ph);
+ for (int i = 0; i < pw * ph; i++) {
+ pattern_channel[i] = component_type_to_rgb_index(pattern.pixels[i].component_type);
+ if (pattern_channel[i] < 0) {
+ return Error(heif_error_Unsupported_feature,
+ heif_suberror_Unsupported_data_version,
+ "Bayer pattern contains component types that we currently cannot convert to RGB");
+ }
+ }
+
+ // Precompute neighbor offset tables for each pattern position and channel.
+ // neighbor_offsets[py * pw + px][ch] = list of (dx, dy) offsets to average.
+ // For the channel this position directly provides: single entry (0, 0).
+ // For other channels: all neighbor offsets within the search radius that provide that channel.
+ std::vector<std::array<std::vector<std::pair<int, int>>, 3>> neighbor_offsets(pw * ph);
+
+ for (int py = 0; py < ph; py++) {
+ for (int px = 0; px < pw; px++) {
+ int this_ch = pattern_channel[py * pw + px];
+ auto& offsets = neighbor_offsets[py * pw + px];
+
+ // The channel this position directly provides: just read from (0,0)
+ offsets[this_ch].emplace_back(0, 0);
+
+ // For the other two channels: collect neighbor offsets
+ int search_radius_x = pw - 1;
+ int search_radius_y = ph - 1;
+
+ for (int dy = -search_radius_y; dy <= search_radius_y; dy++) {
+ for (int dx = -search_radius_x; dx <= search_radius_x; dx++) {
+ if (dx == 0 && dy == 0) {
+ continue;
+ }
+
+ int npx = (((px + dx) % pw) + pw) % pw;
+ int npy = (((py + dy) % ph) + ph) % ph;
+ int neighbor_ch = pattern_channel[npy * pw + npx];
+
+ if (neighbor_ch != this_ch) {
+ offsets[neighbor_ch].emplace_back(dx, dy);
+ }
+ }
+ }
+ }
+ }
+
+ // Bilinear demosaicing using precomputed offset tables.
+ auto demosaic = [&]<typename Pixel>(const Pixel* in, Pixel* out,
+ size_t in_str, size_t out_str) {
+ for (uint32_t y = 0; y < height; y++) {
+ for (uint32_t x = 0; x < width; x++) {
+ const auto& offsets = neighbor_offsets[(y % ph) * pw + (x % pw)];
+
+ Pixel* out_pixel = &out[y * out_str + x * 3];
+
+ for (int ch = 0; ch < 3; ch++) {
+ const auto& ch_offsets = offsets[ch];
+ int sum = 0;
+ int count = 0;
+
+ for (const auto& [dx, dy] : ch_offsets) {
+ int nx = static_cast<int>(x) + dx;
+ int ny = static_cast<int>(y) + dy;
+
+ if (nx < 0 || nx >= static_cast<int>(width) ||
+ ny < 0 || ny >= static_cast<int>(height)) {
+ continue;
+ }
+
+ sum += in[ny * in_str + nx];
+ count++;
+ }
+
+ out_pixel[ch] = count > 0 ? static_cast<Pixel>((sum + count / 2) / count) : 0;
+ }
+ }
+ }
+ };
+
+ if (hdr) {
+ demosaic(reinterpret_cast<const uint16_t*>(in_p),
+ reinterpret_cast<uint16_t*>(out_p),
+ in_stride / 2, out_stride / 2);
+ }
+ else {
+ demosaic(in_p, out_p, in_stride, out_stride);
+ }
+
+ return outimg;
+}
diff --git a/libheif/color-conversion/bayer_bilinear.h b/libheif/color-conversion/bayer_bilinear.h
new file mode 100644
index 00000000..53b22853
--- /dev/null
+++ b/libheif/color-conversion/bayer_bilinear.h
@@ -0,0 +1,47 @@
+/*
+ * HEIF codec.
+ * Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+ *
+ * This file is part of libheif.
+ *
+ * libheif is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * libheif is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libheif. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef LIBHEIF_COLORCONVERSION_BAYER_BILINEAR_H
+#define LIBHEIF_COLORCONVERSION_BAYER_BILINEAR_H
+
+#include "colorconversion.h"
+#include <vector>
+#include <memory>
+
+
+class Op_bayer_bilinear_to_RGB24_32 : public ColorConversionOperation
+{
+public:
+ std::vector<ColorStateWithCost>
+ state_after_conversion(const ColorState& input_state,
+ const ColorState& target_state,
+ const heif_color_conversion_options& options,
+ const heif_color_conversion_options_ext& options_ext) const override;
+
+ Result<std::shared_ptr<HeifPixelImage>>
+ convert_colorspace(const std::shared_ptr<const HeifPixelImage>& input,
+ const ColorState& input_state,
+ const ColorState& target_state,
+ const heif_color_conversion_options& options,
+ const heif_color_conversion_options_ext& options_ext,
+ const heif_security_limits* limits) const override;
+};
+
+#endif //LIBHEIF_COLORCONVERSION_BAYER_BILINEAR_H
diff --git a/libheif/color-conversion/colorconversion.cc b/libheif/color-conversion/colorconversion.cc
index db6b0b5b..0a36ad6b 100644
--- a/libheif/color-conversion/colorconversion.cc
+++ b/libheif/color-conversion/colorconversion.cc
@@ -39,6 +39,7 @@
#include "alpha.h"
#include "hdr_sdr.h"
#include "chroma_sampling.h"
+#include "bayer_bilinear.h"
#if ENABLE_MULTITHREADING_SUPPORT
@@ -67,6 +68,9 @@ std::ostream& operator<<(std::ostream& ostr, heif_colorspace c)
case heif_colorspace_undefined:
ostr << "undefined";
break;
+ case heif_colorspace_filter_array:
+ ostr << "filter_array";
+ break;
default:
assert(false);
}
@@ -230,6 +234,7 @@ void ColorConversionPipeline::init_ops()
ops.emplace_back(std::make_shared<Op_RGB_to_RRGGBBaa_BE>());
ops.emplace_back(std::make_shared<Op_mono_to_YCbCr420>());
ops.emplace_back(std::make_shared<Op_mono_to_RGB24_32>());
+ ops.emplace_back(std::make_shared<Op_bayer_bilinear_to_RGB24_32>());
ops.emplace_back(std::make_shared<Op_RRGGBBaa_swap_endianness>());
ops.emplace_back(std::make_shared<Op_RRGGBBaa_BE_to_RGB_HDR>());
ops.emplace_back(std::make_shared<Op_RGB24_32_to_YCbCr>());
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index 0cc6c700..14edd291 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -321,7 +321,7 @@ Result<Encoder::CodedImageData> ImageItem::encode_to_bitstream_and_boxes(const s
// --- write PIXI property
std::shared_ptr<Box_pixi> pixi = std::make_shared<Box_pixi>();
- if (colorspace == heif_colorspace_monochrome && image->has_channel(heif_channel_filter_array)) {
+ if (colorspace == heif_colorspace_filter_array) {
// Skip pixi for filter array images — bit depth info is in uncC
}
else if (colorspace == heif_colorspace_monochrome) {
diff --git a/libheif/image-items/unc_image.cc b/libheif/image-items/unc_image.cc
index 89f2a69d..b166de85 100644
--- a/libheif/image-items/unc_image.cc
+++ b/libheif/image-items/unc_image.cc
@@ -158,7 +158,7 @@ Result<std::shared_ptr<ImageItem_uncompressed>> ImageItem_uncompressed::add_unci
// Add cpat property if Bayer pattern is set
- if ((*uncEncoder)->get_cpat()) {
+ if (unci_image->m_unc_encoder->get_cpat()) {
unci_image->add_property((*uncEncoder)->get_cpat(), true);
}
diff --git a/libheif/pixelimage.cc b/libheif/pixelimage.cc
index 588e68d7..b7a0201f 100644
--- a/libheif/pixelimage.cc
+++ b/libheif/pixelimage.cc
@@ -388,6 +388,9 @@ std::vector<heif_chroma> get_valid_chroma_values_for_colorspace(heif_colorspace
case heif_colorspace_nonvisual:
return {heif_chroma_undefined};
+ case heif_colorspace_filter_array:
+ return {heif_chroma_monochrome};
+
default:
return {};
}