[{"id":39500,"web_url":"https://patchwork.libcamera.org/comment/39500/","msgid":"<178272801223.36676.18254696781518456381@ping.linuxembedded.co.uk>","date":"2026-06-29T10:13:32","subject":"Re: [PATCH v2 12/12] ipa: ipu3: Add Lens Shading Correction\n\talgorithm","submitter":{"id":4,"url":"https://patchwork.libcamera.org/api/people/4/","name":"Kieran Bingham","email":"kieran.bingham@ideasonboard.com"},"content":"Quoting Daniel Scally (2026-06-26 14:05:59)\n> Add a lens shading correction algorithm for the IPU3 IPA, using the\n> recently implemented libipa base algorithm class.\n> \n> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>\n> Signed-off-by: Daniel Scally <dan.scally@ideasonboard.com>\n\nI think there might be some rebase updates, but I'm happy to see this\nprogress and get in:\n\n\nReviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>\n\n> ---\n> Changes in v2:\n> \n>         - Fixed includes\n>         - Updated some documentation comments\n>         - Check tuning file for \"type\" key, not \"polynomial\"\n> ---\n>  src/ipa/ipu3/algorithms/lsc.cpp     | 296 ++++++++++++++++++++++++++++++++++++\n>  src/ipa/ipu3/algorithms/lsc.h       |  60 ++++++++\n>  src/ipa/ipu3/algorithms/meson.build |   1 +\n>  src/ipa/ipu3/ipa_context.cpp        |  10 ++\n>  src/ipa/ipu3/ipa_context.h          |   3 +\n>  5 files changed, 370 insertions(+)\n> \n> diff --git a/src/ipa/ipu3/algorithms/lsc.cpp b/src/ipa/ipu3/algorithms/lsc.cpp\n> new file mode 100644\n> index 0000000000000000000000000000000000000000..ab9775e6a64ed3d32e7ae3dd90f55f4128da40be\n> --- /dev/null\n> +++ b/src/ipa/ipu3/algorithms/lsc.cpp\n> @@ -0,0 +1,296 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2026, Ideas on Board, Oy\n> + *\n> + * IPU3 Lens Shading Correction algorithm\n> + */\n> +\n> +#include \"lsc.h\"\n> +\n> +#include <bit>\n> +#include <cmath>\n> +\n> +#include <libcamera/control_ids.h>\n> +\n> +/**\n> + * \\file lsc.h\n> + * \\brief IPU3 Lens Shading Correction\n> + */\n> +\n> +namespace libcamera {\n> +\n> +namespace ipa::ipu3::algorithms {\n> +\n> +/**\n> + * \\class Lsc\n> + * \\brief IPU3 Lens Shading Correction algorithm\n> + */\n> +\n> +LOG_DEFINE_CATEGORY(IPU3Lsc)\n> +\n> +static constexpr unsigned int kMaxNumHCells = 73;\n> +static constexpr unsigned int kMaxNumVCells = 56;\n> +static constexpr int kColourTemperatureQuantization = 10;\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::init\n> + */\n> +int Lsc::init(IPAContext &context, const ValueNode &tuningData)\n> +{\n> +       /*\n> +        * The IPU3 lens shading block expects a table of data that isn't of a\n> +        * fixed size, but rather is configurable based on 4 parameters:\n> +        *\n> +        * block_width_log2:    The log2 of the horizontal pixel count per cell\n> +        * block_height_log2:   The log2 of the vertical pixel count per cell\n> +        * width:               The number of horizontal cells\n> +        * height:              The number of vertical cells\n> +        *\n> +        * The constructed grid should be capable of covering the image, but\n> +        * ideally won't extend past the edges of the image. Fixing either set\n> +        * of parameters for the algorithm as a whole is likely to result in\n> +        * suboptimal situations for some sensors, so let's determine them\n> +        * programmatically instead.\n> +        *\n> +        * What we want is the densest possible grid that ideally doesn't extend\n> +        * past the edges of the image at all. The maximum grid size is 73x56,\n> +        * which gives us the lower bounds on cell size. Unfortunately we can\n> +        * only specify sizes in powers of two, which can have the effect of\n> +        * making the grid much more coarse. For example for a 2592x1944 input\n> +        * image, 2592 / 73 = 35.5...which means we need to set blockWidthLog2\n> +        * to 6 (I.E. 64) and have just 40.5 (or rather 41) cells horizontally.\n> +        */\n> +       sensorWidth_ = context.sensorInfo.activeAreaSize.width;\n> +       sensorHeight_ = context.sensorInfo.activeAreaSize.height;\n> +\n> +       unsigned int cellWidth = (sensorWidth_ + kMaxNumHCells - 1) / kMaxNumHCells;\n> +       unsigned int cellHeight = (sensorHeight_ + kMaxNumVCells - 1) / kMaxNumVCells;\n> +\n> +       unsigned int minCellWidth = std::bit_ceil(cellWidth);\n> +       unsigned int minCellHeight = std::bit_ceil(cellHeight);\n> +\n> +       numHCells_ = (sensorWidth_ + minCellWidth - 1) / minCellWidth;\n> +       numVCells_ = (sensorHeight_ + minCellHeight - 1) / minCellHeight;\n> +\n> +       blockWidthLog2_ = std::bit_width(minCellWidth) - 1;\n> +       blockHeightLog2_ = std::bit_width(minCellHeight) - 1;\n> +\n> +       LOG(IPU3Lsc, Debug) << \"Calculated Grid configuration: \"\n> +                           << numHCells_ << \"x\" << numVCells_ << \" cells of \"\n> +                           << minCellWidth << \"x\" << minCellHeight << \" pixels\";\n> +\n> +       /*\n> +        * We need to know if we're running the polynomial algorithm or not as\n> +        * things will behave slightly differently.\n> +        */\n> +       polynomial_ = tuningData[\"type\"].get<bool>(false);\n> +\n> +       return lscAlgo_.init(tuningData, context.ctrlMap, {\n> +                            .keys = { \"r\", \"gr\", \"gb\", \"b\" },\n> +                            .numHCells = numHCells_,\n> +                            .numVCells = numVCells_,\n> +                            .sensorSize = context.sensorInfo.activeAreaSize\n> +       });\n> +}\n> +\n> +std::vector<double> Lsc::calculatePositions(unsigned int dimension)\n> +{\n> +       std::vector<double> positions(dimension);\n> +       for (double i = 0.0; i < dimension; i++)\n> +               positions[i] = i / (dimension - 1);\n> +\n> +       return positions;\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::configure\n> + */\n> +int Lsc::configure(IPAContext &context, const IPAConfigInfo &configInfo)\n> +{\n> +       cropWidth_ = configInfo.sensorInfo.analogCrop.width;\n> +       cropHeight_ = configInfo.sensorInfo.analogCrop.height;\n> +       std::vector<double> xPos = calculatePositions(numHCells_);\n> +       std::vector<double> yPos = calculatePositions(numVCells_);\n> +\n> +       return lscAlgo_.configure(context.activeState.lsc,\n> +                                 configInfo.sensorInfo.analogCrop, xPos, yPos);\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::queueRequest\n> + */\n> +void Lsc::queueRequest(IPAContext &context, const uint32_t frame,\n> +                      IPAFrameContext &frameContext,\n> +                      const ControlList &controls)\n> +{\n> +       /*\n> +        * The base algorithm defines the LensShadingCorrectionEnable control\n> +        * with a default value of true, but actually the IPU3 driver defaults\n> +        * it to off. If this is the first frame, check for the control, but if\n> +        * there isn't one, force it on to fulfil the advertised default.\n> +        */\n> +       if (frame == 0) {\n> +               const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);\n> +               if (!lscEnable) {\n> +                       frameContext.lsc.enabled = true;\n> +                       frameContext.lsc.update = true;\n> +               }\n> +       }\n> +\n> +       lscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc, controls);\n> +}\n> +\n> +static unsigned int quantize(unsigned int value, unsigned int step)\n> +{\n> +       return std::lround(value / static_cast<double>(step)) * step;\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::prepare\n> + */\n> +void Lsc::prepare([[maybe_unused]] IPAContext &context, [[maybe_unused]] const uint32_t frame,\n> +                 IPAFrameContext &frameContext, ipu3_uapi_params *params)\n> +{\n> +       uint32_t ct = frameContext.awb.temperatureK;\n> +       unsigned int quantizedCt = quantize(ct, kColourTemperatureQuantization);\n> +\n> +       if (!frameContext.lsc.update) {\n> +               if (!frameContext.lsc.enabled)\n> +                       return;\n> +\n> +               /*\n> +                * Add a threshold so that oscillations around a quantization\n> +                * step don't lead to constant changes.\n> +                */\n> +               if (utils::abs_diff(ct, lastAppliedCt_) < kColourTemperatureQuantization / 2)\n> +                       return;\n> +\n> +               if (quantizedCt == lastAppliedQuantizedCt_)\n> +                       return;\n> +       }\n> +\n> +       /*\n> +        * This flag tells the kernel driver that it should read the LSC params\n> +        * passed from userspace instead of using its cached copy.\n> +        */\n> +       params->use.acc_shd = 1;\n> +\n> +       /*\n> +        * Pass the enabled flag. If we're not enabled, we can then just bail\n> +        * out.\n> +        */\n> +       ipu3_uapi_shd_config_static *config = &params->acc_param.shd.shd;\n> +       config->general.shd_enable = frameContext.lsc.enabled;\n> +\n> +       if (!frameContext.lsc.enabled)\n> +               return;\n> +\n> +       config->grid.width = numHCells_;\n> +       config->grid.height = numVCells_;\n> +       config->grid.block_width_log2 = blockWidthLog2_;\n> +       config->grid.block_height_log2 = blockHeightLog2_;\n> +       config->grid.grid_height_per_slice = IPU3_UAPI_SHD_MAX_CELLS_PER_SET / numHCells_;\n> +\n> +       /*\n> +        * The IPU3's documentation describes the x_start and y_start members\n> +        * as follows:\n> +        *\n> +        * \"[X/Y] value of top left corner of sensor relative to ROI\n> +        * s13, [-4096, 0], default 0, only negative values.\"\n> +        *\n> +        * I interpret that as allowing us to configure the cropped rectangle\n> +        * relative to the full grid. That's useful if we're running the tabular\n> +        * algorithm, which would otherwise apply the full grid inappropriately.\n> +        * If we're running the polynomial one though the calculated grid is\n> +        * probably more appropriate than a coarse application of the full grid\n> +        * so let's tell the hardware not to bother correcting in that case.\n> +        */\n> +       if (polynomial_) {\n> +               config->grid.x_start = 0;\n> +               config->grid.y_start = 0;\n> +       } else {\n> +               config->grid.x_start = (cropWidth_ - sensorWidth_) / 2;\n> +               config->grid.y_start = (cropHeight_ - sensorHeight_) / 2;\n> +       }\n> +\n> +       /* No idea what this is, but the docs say it should be set as so */\n> +       config->general.init_set_vrt_offst_ul =\n> +               config->grid.y_start >> (config->grid.block_height_log2 %\n> +                                        config->grid.grid_height_per_slice);\n> +\n> +       /*\n> +        * Values in the LUT cease taking effect at 4096, and a value of 0.0 is\n> +        * \"no correction\" rather than black. The gain factor is described by\n> +        * the documentation like so:\n> +        *\n> +        * \"Shift calculated anti shading value. Precision u2. 0x0 - gain factor\n> +        * [1, 5], means no shift interpolated value. 0x1 - gain factor [1, 9],\n> +        * means shift interpolated by 1. 0x2 - gain factor [1, 17], means shift\n> +        * interpolated by 2.\"\n> +        *\n> +        * The simplest interpretation for those pieces of information is I\n> +        * think that the LUT stores 12-bit Q numbers who's represented values\n> +        * depend on the gain_factor setting like so:\n> +        *\n> +        * 0: UQ<2, 10> representing values in range [0, 4)\n> +        * 1: UQ<3, 9> representing values in range [0, 8)\n> +        * 2: UQ<4, 8> representing values in range [0, 16)\n> +        *\n> +        * And that a base gain of 1.0 is added to those configured values. As a\n> +        * gain of more than 5.0 is fairly unlikely, let's fix gain_factor to 0\n> +        * for now and revisit if needed.\n> +        */\n> +       config->general.gain_factor = 0;\n> +\n> +       /*\n> +        * Disable the black level settings here - we do that through another\n> +        * parameters block.\n> +        */\n> +       config->black_level.bl_r = 0;\n> +       config->black_level.bl_gr = 0;\n> +       config->black_level.bl_gb = 0;\n> +       config->black_level.bl_b = 0;\n> +\n> +       ipu3_uapi_shd_lut *lut = &params->acc_param.shd.shd_lut;\n> +\n> +       const auto &set = lscAlgo_.interpolateComponents(quantizedCt);\n> +\n> +       unsigned int totalCells = numHCells_ * numVCells_;\n> +       unsigned int cellsPerSet = numHCells_ * config->grid.grid_height_per_slice;\n> +       unsigned int numSets = (numHCells_ + config->grid.grid_height_per_slice - 1) /\n> +                              config->grid.grid_height_per_slice;\n> +       unsigned int i = 0;\n> +\n> +       for (unsigned int s = 0; s < numSets; s++) {\n> +               for (unsigned int c = 0; c < cellsPerSet && i < totalCells; c++, i++) {\n> +                       lut->sets[s].r_and_gr[c].r = set.at(\"r\")[i];\n> +                       lut->sets[s].r_and_gr[c].gr = set.at(\"gr\")[i];\n> +                       lut->sets[s].gb_and_b[c].gb = set.at(\"gb\")[i];\n> +                       lut->sets[s].gb_and_b[c].b = set.at(\"b\")[i];\n> +               }\n> +       }\n> +\n> +       lastAppliedCt_ = ct;\n> +       lastAppliedQuantizedCt_ = quantizedCt;\n> +       LOG(IPU3Lsc, Debug)\n> +               << \"ct is \" << ct << \", quantized to \"\n> +               << quantizedCt;\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::process\n> + */\n> +void Lsc::process([[maybe_unused]] IPAContext &context,\n> +                 [[maybe_unused]] const uint32_t frame,\n> +                 IPAFrameContext &frameContext,\n> +                 [[maybe_unused]] const ipu3_uapi_stats_3a *stats,\n> +                 ControlList &metadata)\n> +{\n> +       lscAlgo_.process(frameContext.lsc, metadata);\n> +}\n> +\n> +REGISTER_IPA_ALGORITHM(Lsc, \"Lsc\")\n> +\n> +} /* ipa::ipu3::algorithms */\n> +\n> +} /* namespace libcamera */\n> diff --git a/src/ipa/ipu3/algorithms/lsc.h b/src/ipa/ipu3/algorithms/lsc.h\n> new file mode 100644\n> index 0000000000000000000000000000000000000000..050ce91ad115a93224efb161ae0a3536fe32cc7d\n> --- /dev/null\n> +++ b/src/ipa/ipu3/algorithms/lsc.h\n> @@ -0,0 +1,60 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2026, Ideas on Board, Oy\n> + *\n> + * IPU3 Lens Shading Correction algorithm\n> + */\n> +\n> +#pragma once\n> +\n> +#include <vector>\n> +\n> +#include \"libipa/fixedpoint.h\"\n> +#include \"libipa/lsc.h\"\n> +\n> +#include \"algorithm.h\"\n> +\n> +namespace libcamera {\n> +\n> +namespace ipa::ipu3::algorithms {\n> +\n> +class Lsc : public Algorithm\n> +{\n> +public:\n> +       int init(IPAContext &context, const ValueNode &tuningData) override;\n> +       void queueRequest(IPAContext &context, const uint32_t frame,\n> +                         IPAFrameContext &frameContext,\n> +                         const ControlList &controls) override;\n> +       int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;\n> +       void prepare(IPAContext &context, const uint32_t frame,\n> +                    IPAFrameContext &frameContext,\n> +                    ipu3_uapi_params *params) override;\n> +       void process(IPAContext &context, const uint32_t frame,\n> +                    IPAFrameContext &frameContext,\n> +                    const ipu3_uapi_stats_3a *stats,\n> +                    ControlList &metadata) override;\n> +\n> +private:\n> +       std::vector<double> calculatePositions(unsigned int dimension);\n> +\n> +       unsigned int numHCells_;\n> +       unsigned int numVCells_;\n> +       unsigned int blockWidthLog2_;\n> +       unsigned int blockHeightLog2_;\n> +\n> +       unsigned int lastAppliedCt_;\n> +       unsigned int lastAppliedQuantizedCt_;\n> +\n> +       unsigned int sensorWidth_;\n> +       unsigned int sensorHeight_;\n> +       unsigned int cropWidth_;\n> +       unsigned int cropHeight_;\n> +\n> +       bool polynomial_;\n> +\n> +       LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;\n> +};\n> +\n> +} /* ipa::ipu3::algorithms */\n> +\n> +} /* namespace libcamera */\n> diff --git a/src/ipa/ipu3/algorithms/meson.build b/src/ipa/ipu3/algorithms/meson.build\n> index 3dafd2fda9897942cf87d9640665c4fcff383859..70177f50415259e0c2bd207205c385dde07d6624 100644\n> --- a/src/ipa/ipu3/algorithms/meson.build\n> +++ b/src/ipa/ipu3/algorithms/meson.build\n> @@ -6,5 +6,6 @@ ipu3_ipa_algorithms = files([\n>      'awb.cpp',\n>      'blc.cpp',\n>      'ccm.cpp',\n> +    'lsc.cpp',\n>      'tone_mapping.cpp',\n>  ])\n> diff --git a/src/ipa/ipu3/ipa_context.cpp b/src/ipa/ipu3/ipa_context.cpp\n> index 9537802ceca5018118bdf4c71ef361c20fc44bfb..935264279a8a6346cb3e58ddb5577811b46163e8 100644\n> --- a/src/ipa/ipu3/ipa_context.cpp\n> +++ b/src/ipa/ipu3/ipa_context.cpp\n> @@ -127,6 +127,11 @@ namespace libcamera::ipa::ipu3 {\n>   * \\brief Active gamma correction parameters for the IPA\n>   */\n>  \n> +/**\n> + * \\var IPAActiveState::lsc\n> + * \\brief Active lens shading correction parameters for the IPA\n> + */\n> +\n>  /**\n>   * \\var IPASessionConfiguration::sensor\n>   * \\brief Sensor-specific configuration of the IPA\n> @@ -186,4 +191,9 @@ namespace libcamera::ipa::ipu3 {\n>   * \\brief Per-frame gamma correction parameters for the IPA\n>   */\n>  \n> +/**\n> + * \\var IPAFrameContext::lsc\n> + * \\brief Per-frame lens shading correction parameters for the IPA\n> + */\n> +\n>  } /* namespace libcamera::ipa::ipu3 */\n> diff --git a/src/ipa/ipu3/ipa_context.h b/src/ipa/ipu3/ipa_context.h\n> index d650f2fe1ad8eab91b7128a47c9690e42b1595f1..d4bf2091a64fb1aad5c715a2d9d265ae3aecacb4 100644\n> --- a/src/ipa/ipu3/ipa_context.h\n> +++ b/src/ipa/ipu3/ipa_context.h\n> @@ -21,6 +21,7 @@\n>  #include <libipa/ccm.h>\n>  #include <libipa/fc_queue.h>\n>  #include <libipa/gamma.h>\n> +#include <libipa/lsc.h>\n>  \n>  namespace libcamera {\n>  \n> @@ -68,6 +69,7 @@ struct IPAActiveState {\n>         ipa::awb::ActiveState awb;\n>         ipa::ccm::ActiveState ccm;\n>         ipa::gamma::ActiveState gamma;\n> +       ipa::lsc::ActiveState lsc;\n>  };\n>  \n>  struct IPAFrameContext : public FrameContext {\n> @@ -79,6 +81,7 @@ struct IPAFrameContext : public FrameContext {\n>         ipa::awb::FrameContext awb;\n>         ipa::ccm::FrameContext ccm;\n>         ipa::gamma::FrameContext gamma;\n> +       ipa::lsc::FrameContext lsc;\n>  };\n>  \n>  struct IPAContext {\n> \n> -- \n> 2.43.0\n>","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 0A86FC3264\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 29 Jun 2026 10:13:38 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id C29B965F27;\n\tMon, 29 Jun 2026 12:13:36 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id E222265F04\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 29 Jun 2026 12:13:34 +0200 (CEST)","from monstersaurus.ideasonboard.com\n\t(cpc89244-aztw30-2-0-cust6594.18-1.cable.virginm.net [86.31.185.195])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id A9EB434;\n\tMon, 29 Jun 2026 12:12:51 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"IMlHKlpL\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1782727971;\n\tbh=K7reIpqwv7kyUM9qhulnbvJ8NR2Z3bN/yopUVDrXd+4=;\n\th=In-Reply-To:References:Subject:From:Cc:To:Date:From;\n\tb=IMlHKlpLQp9eKDo3BBnuH9yHH+B0gGZwA0WpVPpX9wyjN2LS0Uo+qZsf3Gizqh3Dq\n\tAHhL40sTrpCQe0pq1U9Zd5w5GDKLKZ/0ggTcPMvhJAEPlQ9DtELkuS29EFPyiZjP1P\n\tqUhrK1YxuTgaJiIcRclspYMVk5dK11Lf+iQ0qkPI=","Content-Type":"text/plain; charset=\"utf-8\"","MIME-Version":"1.0","Content-Transfer-Encoding":"quoted-printable","In-Reply-To":"<20260626-ipu3-libipa-rework-v2-12-41546e23de3e@ideasonboard.com>","References":"<20260626-ipu3-libipa-rework-v2-0-41546e23de3e@ideasonboard.com>\n\t<20260626-ipu3-libipa-rework-v2-12-41546e23de3e@ideasonboard.com>","Subject":"Re: [PATCH v2 12/12] ipa: ipu3: Add Lens Shading Correction\n\talgorithm","From":"Kieran Bingham <kieran.bingham@ideasonboard.com>","Cc":"Daniel Scally <dan.scally@ideasonboard.com>,\n\tJacopo Mondi <jacopo.mondi@ideasonboard.com>","To":"Daniel Scally <dan.scally@ideasonboard.com>,\n\tlibcamera-devel@lists.libcamera.org","Date":"Mon, 29 Jun 2026 11:13:32 +0100","Message-ID":"<178272801223.36676.18254696781518456381@ping.linuxembedded.co.uk>","User-Agent":"alot/0.9.1","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]