[{"id":39157,"web_url":"https://patchwork.libcamera.org/comment/39157/","msgid":"<ajJu7BkyWsxqKmLa@zed>","date":"2026-06-17T10:09:29","subject":"Re: [PATCH 10/10] ipa: ipu3: Add Lens Shading Correction algorithm","submitter":{"id":143,"url":"https://patchwork.libcamera.org/api/people/143/","name":"Jacopo Mondi","email":"jacopo.mondi@ideasonboard.com"},"content":"On Tue, Jun 16, 2026 at 07:41:44AM +0100, Daniel Scally wrote:\n> Add a lens shading correction algorithm for the IPU3 IPA, using the\n> recently implemented libipa base algorithm class.\n>\n> Signed-off-by: Daniel Scally <dan.scally@ideasonboard.com>\n> ---\n>  src/ipa/ipu3/algorithms/lsc.cpp     | 290 ++++++++++++++++++++++++++++++++++++\n>  src/ipa/ipu3/algorithms/lsc.h       |  58 ++++++++\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, 362 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..70f8e59b9a9238c44f96d4a2a31bd23a8377f662\n> --- /dev/null\n> +++ b/src/ipa/ipu3/algorithms/lsc.cpp\n> @@ -0,0 +1,290 @@\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\nmissing <bit> for std::bit_ceil and std::bit_width\nmissing <vector> (but it should go in the header)\nmissing <cmath> for std::lround\n\n> +\n> +/**\n> + * \\file lsc.h\n\n\\brief IPU3 Lens Shading Correction\n\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> +\t/*\n> +\t * The IPU3 lens shading block expects a table of data that isn't of a\n> +\t * fixed size, but rather is configurable based on 4 parameters:\n> +\t *\n> +\t * block_width_log2:\tThe log2 of the horizontal pixel count per cell\n> +\t * block_height_log2:\tThe log2 of the vertical pixel count per cell\n> +\t * width:\t\tThe number of horizontal cells\n> +\t * height:\t\tThe number of vertical cells\n> +\t *\n> +\t * The constructed grid should be capable of covering the image, but\n> +\t * ideally won't extend past the edges of the image. Fixing either set\n> +\t * of parameters for the algorithm as a whole is likely to result in\n> +\t * suboptimal situations for some sensors, so let's determine them\n> +\t * programmatically instead.\n> +\t *\n> +\t * What we want is the densest possible grid that ideally doesn't extend\n> +\t * past the edges of the image at all. The maximum grid size is 73x56,\n> +\t * which gives us the lower bounds on cell size. Unfortunately we can\n> +\t * only specify sizes in powers of two, which can have the effect of\n> +\t * making the grid much more coarse. For example for a 2592x1944 input\n> +\t * image, 2592 / 73 = 35.5...which means we need to set blockWidthLog2\n> +\t * to 6 (I.E. 64) and have just 40.5 (or rather 41) cells horizontally.\n> +\t */\n> +\tsensorWidth_ = context.sensorInfo.activeAreaSize.width;\n> +\tsensorHeight_ = context.sensorInfo.activeAreaSize.height;\n> +\n> +\tunsigned int cellWidth = (sensorWidth_ + kMaxNumHCells - 1) / kMaxNumHCells;\n> +\tunsigned int cellHeight = (sensorHeight_ + kMaxNumVCells - 1) / kMaxNumVCells;\n> +\n> +\tunsigned int minCellWidth = std::bit_ceil(cellWidth);\n> +\tunsigned int minCellHeight = std::bit_ceil(cellHeight);\n> +\n> +\tnumHCells_ = (sensorWidth_ + minCellWidth - 1) / minCellWidth;\n> +\tnumVCells_ = (sensorHeight_ + minCellHeight - 1) / minCellHeight;\n> +\n> +\tblockWidthLog2_ = std::bit_width(minCellWidth) - 1;\n> +\tblockHeightLog2_ = std::bit_width(minCellHeight) - 1;\n\nneat!\n\n> +\n> +\tLOG(IPU3Lsc, Debug) << \"Calculated Grid configuration: \"\n> +\t\t\t    << numHCells_ << \"x\" << numVCells_ << \" cells of \"\n> +\t\t\t    << minCellWidth << \"x\" << minCellHeight << \" pixels\";\n> +\n> +\t/*\n> +\t * We need to know if we're running the polynomial algorithm or not as\n> +\t * things will behave slightly differently.\n> +\t */\n> +\tpolynomial_ = tuningData[\"polynomial\"].get<bool>(false);\n> +\n> +\treturn lscAlgo_.init(tuningData, context.ctrlMap, {\n> +\t\t\t     .keys = { \"r\", \"gr\", \"gb\", \"b\" },\n> +\t\t\t     .numHCells = numHCells_,\n> +\t\t\t     .numVCells = numVCells_,\n> +\t\t\t     .sensorSize = context.sensorInfo.activeAreaSize\n> +\t});\n> +}\n> +\n> +std::vector<double> Lsc::calculatePositions(unsigned int dimension)\n> +{\n> +\tstd::vector<double> positions(dimension);\n> +\tfor (double i = 0.0; i < dimension; i++)\n> +\t\tpositions[i] = i / (dimension - 1);\n> +\n> +\treturn positions;\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::configure\n> + */\n> +int Lsc::configure(IPAContext &context, const IPAConfigInfo &configInfo)\n> +{\n> +\tcropWidth_ = configInfo.sensorInfo.analogCrop.width;\n> +\tcropHeight_ = configInfo.sensorInfo.analogCrop.height;\n> +\tstd::vector<double> xPos = calculatePositions(numHCells_);\n> +\tstd::vector<double> yPos = calculatePositions(numVCells_);\n> +\n> +\treturn lscAlgo_.configure(context.activeState.lsc,\n> +\t\t\t\t  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> +\t\t       IPAFrameContext &frameContext,\n> +\t\t       const ControlList &controls)\n> +{\n> +\t/*\n> +\t * The base algorithm defines the LensShadingCorrectionEnable control\n> +\t * with a default value of true, but actually the IPU3 driver defaults\n> +\t * it to off. If this is the first frame, check for the control, but if\n> +\t * there isn't one, force it on to fulfil the advertised default.\n> +\t */\n> +\tif (frame == 0) {\n> +\t\tconst auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);\n> +\t\tif (!lscEnable) {\n> +\t\t\tframeContext.lsc.enabled = true;\n\nThis will be set to true by lscAlgo_ if I'm not mistaknen..\nDoesn't hurt though\n\n> +\t\t\tframeContext.lsc.update = true;\n> +\t\t}\n> +\t}\n> +\n> +\tlscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc, controls);\n> +}\n> +\n> +static unsigned int quantize(unsigned int value, unsigned int step)\n> +{\n> +\treturn 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> +\t\t  IPAFrameContext &frameContext, ipu3_uapi_params *params)\n> +{\n> +\tuint32_t ct = frameContext.awb.temperatureK;\n> +\tunsigned int quantizedCt = quantize(ct, kColourTemperatureQuantization);\n\nI wonder if this is worth a dedicated function. Not a big deal though\n\n> +\n> +\tif (!frameContext.lsc.update) {\n> +\t\tif (!frameContext.lsc.enabled)\n> +\t\t\treturn;\n> +\n> +\t\t/*\n> +\t\t * Add a threshold so that oscillations around a quantization\n> +\t\t * step don't lead to constant changes.\n> +\t\t */\n> +\t\tif (utils::abs_diff(ct, lastAppliedCt_) < kColourTemperatureQuantization / 2)\n> +\t\t\treturn;\n> +\n> +\t\tif (quantizedCt == lastAppliedQuantizedCt_)\n> +\t\t\treturn;\n> +\t}\n> +\n> +\t/*\n> +\t * This flag tells the kernel driver that it should read the LSC params\n> +\t * passed from userspace instead of using its cached copy.\n> +\t */\n> +\tparams->use.acc_shd = 1;\n> +\n> +\t/*\n> +\t * Pass the enabled flag. If we're not enabled, we can then just bail\n> +\t * out.\n> +\t */\n> +\tipu3_uapi_shd_config_static *config = &params->acc_param.shd.shd;\n> +\tconfig->general.shd_enable = frameContext.lsc.enabled;\n> +\n> +\tif (!frameContext.lsc.enabled)\n> +\t\treturn;\n> +\n> +\tconfig->grid.width = numHCells_;\n> +\tconfig->grid.height = numVCells_;\n> +\tconfig->grid.block_width_log2 = blockWidthLog2_;\n> +\tconfig->grid.block_height_log2 = blockHeightLog2_;\n> +\tconfig->grid.grid_height_per_slice = IPU3_UAPI_SHD_MAX_CELLS_PER_SET / numHCells_;\n> +\n> +\t/*\n> +\t * The IPU3's documentation describes the x_start and y_start members\n> +\t * as follows:\n> +\t *\n> +\t * \"[X/Y] value of top left corner of sensor relative to ROI\n> +\t * s13, [-4096, 0], default 0, only negative values.\"\n> +\t *\n> +\t * I interpret that as allowing us to configure the cropped rectangle\n> +\t * relative to the full grid. That's useful if we're running the tabular\n> +\t * algorithm, which would otherwise apply the full grid inappropriately.\n> +\t * If we're running the polynomial one though the calculated grid is\n> +\t * probably more appropriate than a coarse application of the full grid\n> +\t * so let's tell the hardware not to bother correcting in that case.\n> +\t */\n> +\tif (polynomial_) {\n> +\t\tconfig->grid.x_start = 0;\n> +\t\tconfig->grid.y_start = 0;\n> +\t} else {\n> +\t\tconfig->grid.x_start = (cropWidth_ - sensorWidth_) / 2;\n> +\t\tconfig->grid.y_start = (cropHeight_ - sensorHeight_) / 2;\n> +\t}\n> +\n> +\t/* No idea what this is, but the docs say it should be set as so */\n> +\tconfig->general.init_set_vrt_offst_ul =\n> +\t\tconfig->grid.y_start >> (config->grid.block_height_log2 %\n> +\t\t\t\t\t config->grid.grid_height_per_slice);\n> +\n> +\t/*\n> +\t * Values in the LUT cease taking effect at 4096, and a value of 0.0 is\n> +\t * \"no correction\" rather than black. The gain factor is described by\n> +\t * the documentation like so:\n> +\t *\n> +\t * \"Shift calculated anti shading value. Precision u2. 0x0 - gain factor\n> +\t * [1, 5], means no shift interpolated value. 0x1 - gain factor [1, 9],\n> +\t * means shift interpolated by 1. 0x2 - gain factor [1, 17], means shift\n> +\t * interpolated by 2.\"\n> +\t *\n> +\t * The simplest interpretation for those pieces of information is I\n> +\t * think that the LUT stores 12-bit Q numbers who's represented values\n> +\t * depend on the gain_factor setting like so:\n> +\t *\n> +\t * 0: UQ<2, 10> representing values in range [0, 4)\n> +\t * 1: UQ<3, 9> representing values in range [0, 8)\n> +\t * 2: UQ<4, 8> representing values in range [0, 16)\n> +\t *\n> +\t * And that a base gain of 1.0 is added to those configured values. As a\n> +\t * gain of more than 5.0 is fairly unlikely, let's fix gain_factor to 0\n> +\t * for now and revisit if needed.\n> +\t */\n> +\tconfig->general.gain_factor = 0;\n\nkudos for your reverse engineering effort here\n\n> +\n> +\t/*\n> +\t * Disable the black level settings here - we do that through another\n> +\t * parameters block.\n> +\t */\n> +\tconfig->black_level.bl_r = 0;\n> +\tconfig->black_level.bl_gr = 0;\n> +\tconfig->black_level.bl_gb = 0;\n> +\tconfig->black_level.bl_b = 0;\n> +\n> +\tipu3_uapi_shd_lut *lut = &params->acc_param.shd.shd_lut;\n> +\n> +\tconst auto &set = lscAlgo_.interpolateComponents(quantizedCt);\n> +\n> +\tunsigned int totalCells = numHCells_ * numVCells_;\n> +\tunsigned int cellsPerSet = numHCells_ * config->grid.grid_height_per_slice;\n> +\tunsigned int numSets = (numHCells_ + config->grid.grid_height_per_slice - 1) /\n> +\t\t\t       config->grid.grid_height_per_slice;\n> +\tunsigned int i = 0;\n> +\n> +\tfor (unsigned int s = 0; s < numSets; s++) {\n> +\t\tfor (unsigned int c = 0; c < cellsPerSet && i < totalCells; c++, i++) {\n> +\t\t\tlut->sets[s].r_and_gr[c].r = set.at(\"r\")[i];\n> +\t\t\tlut->sets[s].r_and_gr[c].gr = set.at(\"gr\")[i];\n> +\t\t\tlut->sets[s].gb_and_b[c].gb = set.at(\"gb\")[i];\n> +\t\t\tlut->sets[s].gb_and_b[c].b = set.at(\"b\")[i];\n> +\t\t}\n> +\t}\n> +\n> +\tlastAppliedCt_ = ct;\n> +\tlastAppliedQuantizedCt_ = quantizedCt;\n> +\tLOG(IPU3Lsc, Debug)\n> +\t\t<< \"ct is \" << ct << \", quantized to \"\n> +\t\t<< quantizedCt;\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::process\n> + */\n> +void Lsc::process([[maybe_unused]] IPAContext &context,\n> +\t\t  [[maybe_unused]] const uint32_t frame,\n> +\t\t  IPAFrameContext &frameContext,\n> +\t\t  [[maybe_unused]] const ipu3_uapi_stats_3a *stats,\n> +\t\t  ControlList &metadata)\n> +{\n> +\tlscAlgo_.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..f86505029f005c9832c3efa213b86848222ddf62\n> --- /dev/null\n> +++ b/src/ipa/ipu3/algorithms/lsc.h\n> @@ -0,0 +1,58 @@\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 \"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> +\tint init(IPAContext &context, const ValueNode &tuningData) override;\n> +\tvoid queueRequest(IPAContext &context, const uint32_t frame,\n> +\t\t\t  IPAFrameContext &frameContext,\n> +\t\t\t  const ControlList &controls) override;\n> +\tint configure(IPAContext &context, const IPAConfigInfo &configInfo) override;\n> +\tvoid prepare(IPAContext &context, const uint32_t frame,\n> +\t\t     IPAFrameContext &frameContext,\n> +\t\t     ipu3_uapi_params *params) override;\n> +\tvoid process(IPAContext &context, const uint32_t frame,\n> +\t\t     IPAFrameContext &frameContext,\n> +\t\t     const ipu3_uapi_stats_3a *stats,\n> +\t\t     ControlList &metadata) override;\n> +\n> +private:\n> +\tstd::vector<double> calculatePositions(unsigned int dimension);\n> +\n> +\tunsigned int numHCells_;\n> +\tunsigned int numVCells_;\n> +\tunsigned int blockWidthLog2_;\n> +\tunsigned int blockHeightLog2_;\n> +\n> +\tunsigned int lastAppliedCt_;\n> +\tunsigned int lastAppliedQuantizedCt_;\n> +\n> +\tunsigned int sensorWidth_;\n> +\tunsigned int sensorHeight_;\n> +\tunsigned int cropWidth_;\n> +\tunsigned int cropHeight_;\n> +\n> +\tbool polynomial_;\n\nI would be nice if we could avoid this, but for now I think this is\nfine\n\n> +\n> +\tLscAlgorithm<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 7fcfd5e0e4ade92521cc2914dd07113235af8e45..071a7415b61f574d1b525d04d4434ac48fccb23e 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> @@ -191,4 +196,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 f157f223cbb3119f108d768b14fca514ac5661ca..b517639b1e5878138f653918cd2dc71b49d97264 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> @@ -70,6 +71,7 @@ struct IPAActiveState {\n>  \tipa::awb::ActiveState awb;\n>  \tipa::ccm::ActiveState ccm;\n>  \tipa::gamma::ActiveState gamma;\n> +\tipa::lsc::ActiveState lsc;\n>  };\n>\n>  struct IPAFrameContext : public FrameContext {\n> @@ -81,6 +83,7 @@ struct IPAFrameContext : public FrameContext {\n>  \tipa::awb::FrameContext awb;\n>  \tipa::ccm::FrameContext ccm;\n>  \tipa::gamma::FrameContext gamma;\n> +\tipa::lsc::FrameContext lsc;\n>  };\n>\n>  struct IPAContext {\n\nAll minors! Thanks for the effort\nReviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>\n\nThanks\n  j\n\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 DC24FBF415\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 17 Jun 2026 10:09:35 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id DCB9962894;\n\tWed, 17 Jun 2026 12:09:34 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 07CA2623D9\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 17 Jun 2026 12:09:33 +0200 (CEST)","from ideasonboard.com (mob-109-113-4-199.net.vodafone.it\n\t[109.113.4.199])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 6A4BE227;\n\tWed, 17 Jun 2026 12:08:58 +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=\"WhrSznci\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1781690938;\n\tbh=Z8fIUAR7Tt7CYcqzdDBHcyyjhclv0K80aRmbOlinh3Q=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=WhrSznciria1uDa1JZUqqbUeLvr68F6dFy1gOgDsyj7R26JFlBSuEtp63yzX3jmbt\n\tcGT/+kqIOmuIcz9pBmftVFIEP0tWA/8toYlyLVH/nlHeWeMdUryR9EM1H6rVxZZg1W\n\tlv7FgW406JwNgzJp7ax5rBKEZfk/PzeoZKTHbD54=","Date":"Wed, 17 Jun 2026 12:09:29 +0200","From":"Jacopo Mondi <jacopo.mondi@ideasonboard.com>","To":"Daniel Scally <dan.scally@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Subject":"Re: [PATCH 10/10] ipa: ipu3: Add Lens Shading Correction algorithm","Message-ID":"<ajJu7BkyWsxqKmLa@zed>","References":"<20260616-ipu3-libipa-rework-v1-0-d4448b54f1d8@ideasonboard.com>\n\t<20260616-ipu3-libipa-rework-v1-10-d4448b54f1d8@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20260616-ipu3-libipa-rework-v1-10-d4448b54f1d8@ideasonboard.com>","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>"}},{"id":39440,"web_url":"https://patchwork.libcamera.org/comment/39440/","msgid":"<e51d7edb-c276-4ed3-a56b-934517957e2d@ideasonboard.com>","date":"2026-06-26T10:51:46","subject":"Re: [PATCH 10/10] ipa: ipu3: Add Lens Shading Correction algorithm","submitter":{"id":156,"url":"https://patchwork.libcamera.org/api/people/156/","name":"Dan Scally","email":"dan.scally@ideasonboard.com"},"content":"Hi Jacopo, thanks for the review\n\nOn 17/06/2026 11:09, Jacopo Mondi wrote:\n> On Tue, Jun 16, 2026 at 07:41:44AM +0100, Daniel Scally wrote:\n>> Add a lens shading correction algorithm for the IPU3 IPA, using the\n>> recently implemented libipa base algorithm class.\n>>\n>> Signed-off-by: Daniel Scally <dan.scally@ideasonboard.com>\n>> ---\n>>   src/ipa/ipu3/algorithms/lsc.cpp     | 290 ++++++++++++++++++++++++++++++++++++\n>>   src/ipa/ipu3/algorithms/lsc.h       |  58 ++++++++\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, 362 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..70f8e59b9a9238c44f96d4a2a31bd23a8377f662\n>> --- /dev/null\n>> +++ b/src/ipa/ipu3/algorithms/lsc.cpp\n>> @@ -0,0 +1,290 @@\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> missing <bit> for std::bit_ceil and std::bit_width\n> missing <vector> (but it should go in the header)\n> missing <cmath> for std::lround\n> \n>> +\n>> +/**\n>> + * \\file lsc.h\n> \n> \\brief IPU3 Lens Shading Correction\n> \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>> +\t/*\n>> +\t * The IPU3 lens shading block expects a table of data that isn't of a\n>> +\t * fixed size, but rather is configurable based on 4 parameters:\n>> +\t *\n>> +\t * block_width_log2:\tThe log2 of the horizontal pixel count per cell\n>> +\t * block_height_log2:\tThe log2 of the vertical pixel count per cell\n>> +\t * width:\t\tThe number of horizontal cells\n>> +\t * height:\t\tThe number of vertical cells\n>> +\t *\n>> +\t * The constructed grid should be capable of covering the image, but\n>> +\t * ideally won't extend past the edges of the image. Fixing either set\n>> +\t * of parameters for the algorithm as a whole is likely to result in\n>> +\t * suboptimal situations for some sensors, so let's determine them\n>> +\t * programmatically instead.\n>> +\t *\n>> +\t * What we want is the densest possible grid that ideally doesn't extend\n>> +\t * past the edges of the image at all. The maximum grid size is 73x56,\n>> +\t * which gives us the lower bounds on cell size. Unfortunately we can\n>> +\t * only specify sizes in powers of two, which can have the effect of\n>> +\t * making the grid much more coarse. For example for a 2592x1944 input\n>> +\t * image, 2592 / 73 = 35.5...which means we need to set blockWidthLog2\n>> +\t * to 6 (I.E. 64) and have just 40.5 (or rather 41) cells horizontally.\n>> +\t */\n>> +\tsensorWidth_ = context.sensorInfo.activeAreaSize.width;\n>> +\tsensorHeight_ = context.sensorInfo.activeAreaSize.height;\n>> +\n>> +\tunsigned int cellWidth = (sensorWidth_ + kMaxNumHCells - 1) / kMaxNumHCells;\n>> +\tunsigned int cellHeight = (sensorHeight_ + kMaxNumVCells - 1) / kMaxNumVCells;\n>> +\n>> +\tunsigned int minCellWidth = std::bit_ceil(cellWidth);\n>> +\tunsigned int minCellHeight = std::bit_ceil(cellHeight);\n>> +\n>> +\tnumHCells_ = (sensorWidth_ + minCellWidth - 1) / minCellWidth;\n>> +\tnumVCells_ = (sensorHeight_ + minCellHeight - 1) / minCellHeight;\n>> +\n>> +\tblockWidthLog2_ = std::bit_width(minCellWidth) - 1;\n>> +\tblockHeightLog2_ = std::bit_width(minCellHeight) - 1;\n> \n> neat!\n> \n>> +\n>> +\tLOG(IPU3Lsc, Debug) << \"Calculated Grid configuration: \"\n>> +\t\t\t    << numHCells_ << \"x\" << numVCells_ << \" cells of \"\n>> +\t\t\t    << minCellWidth << \"x\" << minCellHeight << \" pixels\";\n>> +\n>> +\t/*\n>> +\t * We need to know if we're running the polynomial algorithm or not as\n>> +\t * things will behave slightly differently.\n>> +\t */\n>> +\tpolynomial_ = tuningData[\"polynomial\"].get<bool>(false);\n>> +\n>> +\treturn lscAlgo_.init(tuningData, context.ctrlMap, {\n>> +\t\t\t     .keys = { \"r\", \"gr\", \"gb\", \"b\" },\n>> +\t\t\t     .numHCells = numHCells_,\n>> +\t\t\t     .numVCells = numVCells_,\n>> +\t\t\t     .sensorSize = context.sensorInfo.activeAreaSize\n>> +\t});\n>> +}\n>> +\n>> +std::vector<double> Lsc::calculatePositions(unsigned int dimension)\n>> +{\n>> +\tstd::vector<double> positions(dimension);\n>> +\tfor (double i = 0.0; i < dimension; i++)\n>> +\t\tpositions[i] = i / (dimension - 1);\n>> +\n>> +\treturn positions;\n>> +}\n>> +\n>> +/**\n>> + * \\copydoc libcamera::ipa::Algorithm::configure\n>> + */\n>> +int Lsc::configure(IPAContext &context, const IPAConfigInfo &configInfo)\n>> +{\n>> +\tcropWidth_ = configInfo.sensorInfo.analogCrop.width;\n>> +\tcropHeight_ = configInfo.sensorInfo.analogCrop.height;\n>> +\tstd::vector<double> xPos = calculatePositions(numHCells_);\n>> +\tstd::vector<double> yPos = calculatePositions(numVCells_);\n>> +\n>> +\treturn lscAlgo_.configure(context.activeState.lsc,\n>> +\t\t\t\t  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>> +\t\t       IPAFrameContext &frameContext,\n>> +\t\t       const ControlList &controls)\n>> +{\n>> +\t/*\n>> +\t * The base algorithm defines the LensShadingCorrectionEnable control\n>> +\t * with a default value of true, but actually the IPU3 driver defaults\n>> +\t * it to off. If this is the first frame, check for the control, but if\n>> +\t * there isn't one, force it on to fulfil the advertised default.\n>> +\t */\n>> +\tif (frame == 0) {\n>> +\t\tconst auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);\n>> +\t\tif (!lscEnable) {\n>> +\t\t\tframeContext.lsc.enabled = true;\n> \n> This will be set to true by lscAlgo_ if I'm not mistaknen..\n> Doesn't hurt though\n\nI think this only happens in lscAlgo_.queueRequest() if the control is present in the ControlList, \nwhich it isn't necessarily\n\n> \n>> +\t\t\tframeContext.lsc.update = true;\n>> +\t\t}\n>> +\t}\n>> +\n>> +\tlscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc, controls);\n>> +}\n>> +\n>> +static unsigned int quantize(unsigned int value, unsigned int step)\n>> +{\n>> +\treturn 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>> +\t\t  IPAFrameContext &frameContext, ipu3_uapi_params *params)\n>> +{\n>> +\tuint32_t ct = frameContext.awb.temperatureK;\n>> +\tunsigned int quantizedCt = quantize(ct, kColourTemperatureQuantization);\n> \n> I wonder if this is worth a dedicated function. Not a big deal though\n\nI copied it from one of the other implementations actually; I was wondering if it might be something \nthat could go in the LscAlgorith class perhaps\n> \n>> +\n>> +\tif (!frameContext.lsc.update) {\n>> +\t\tif (!frameContext.lsc.enabled)\n>> +\t\t\treturn;\n>> +\n>> +\t\t/*\n>> +\t\t * Add a threshold so that oscillations around a quantization\n>> +\t\t * step don't lead to constant changes.\n>> +\t\t */\n>> +\t\tif (utils::abs_diff(ct, lastAppliedCt_) < kColourTemperatureQuantization / 2)\n>> +\t\t\treturn;\n>> +\n>> +\t\tif (quantizedCt == lastAppliedQuantizedCt_)\n>> +\t\t\treturn;\n>> +\t}\n>> +\n>> +\t/*\n>> +\t * This flag tells the kernel driver that it should read the LSC params\n>> +\t * passed from userspace instead of using its cached copy.\n>> +\t */\n>> +\tparams->use.acc_shd = 1;\n>> +\n>> +\t/*\n>> +\t * Pass the enabled flag. If we're not enabled, we can then just bail\n>> +\t * out.\n>> +\t */\n>> +\tipu3_uapi_shd_config_static *config = &params->acc_param.shd.shd;\n>> +\tconfig->general.shd_enable = frameContext.lsc.enabled;\n>> +\n>> +\tif (!frameContext.lsc.enabled)\n>> +\t\treturn;\n>> +\n>> +\tconfig->grid.width = numHCells_;\n>> +\tconfig->grid.height = numVCells_;\n>> +\tconfig->grid.block_width_log2 = blockWidthLog2_;\n>> +\tconfig->grid.block_height_log2 = blockHeightLog2_;\n>> +\tconfig->grid.grid_height_per_slice = IPU3_UAPI_SHD_MAX_CELLS_PER_SET / numHCells_;\n>> +\n>> +\t/*\n>> +\t * The IPU3's documentation describes the x_start and y_start members\n>> +\t * as follows:\n>> +\t *\n>> +\t * \"[X/Y] value of top left corner of sensor relative to ROI\n>> +\t * s13, [-4096, 0], default 0, only negative values.\"\n>> +\t *\n>> +\t * I interpret that as allowing us to configure the cropped rectangle\n>> +\t * relative to the full grid. That's useful if we're running the tabular\n>> +\t * algorithm, which would otherwise apply the full grid inappropriately.\n>> +\t * If we're running the polynomial one though the calculated grid is\n>> +\t * probably more appropriate than a coarse application of the full grid\n>> +\t * so let's tell the hardware not to bother correcting in that case.\n>> +\t */\n>> +\tif (polynomial_) {\n>> +\t\tconfig->grid.x_start = 0;\n>> +\t\tconfig->grid.y_start = 0;\n>> +\t} else {\n>> +\t\tconfig->grid.x_start = (cropWidth_ - sensorWidth_) / 2;\n>> +\t\tconfig->grid.y_start = (cropHeight_ - sensorHeight_) / 2;\n>> +\t}\n>> +\n>> +\t/* No idea what this is, but the docs say it should be set as so */\n>> +\tconfig->general.init_set_vrt_offst_ul =\n>> +\t\tconfig->grid.y_start >> (config->grid.block_height_log2 %\n>> +\t\t\t\t\t config->grid.grid_height_per_slice);\n>> +\n>> +\t/*\n>> +\t * Values in the LUT cease taking effect at 4096, and a value of 0.0 is\n>> +\t * \"no correction\" rather than black. The gain factor is described by\n>> +\t * the documentation like so:\n>> +\t *\n>> +\t * \"Shift calculated anti shading value. Precision u2. 0x0 - gain factor\n>> +\t * [1, 5], means no shift interpolated value. 0x1 - gain factor [1, 9],\n>> +\t * means shift interpolated by 1. 0x2 - gain factor [1, 17], means shift\n>> +\t * interpolated by 2.\"\n>> +\t *\n>> +\t * The simplest interpretation for those pieces of information is I\n>> +\t * think that the LUT stores 12-bit Q numbers who's represented values\n>> +\t * depend on the gain_factor setting like so:\n>> +\t *\n>> +\t * 0: UQ<2, 10> representing values in range [0, 4)\n>> +\t * 1: UQ<3, 9> representing values in range [0, 8)\n>> +\t * 2: UQ<4, 8> representing values in range [0, 16)\n>> +\t *\n>> +\t * And that a base gain of 1.0 is added to those configured values. As a\n>> +\t * gain of more than 5.0 is fairly unlikely, let's fix gain_factor to 0\n>> +\t * for now and revisit if needed.\n>> +\t */\n>> +\tconfig->general.gain_factor = 0;\n> \n> kudos for your reverse engineering effort here\n\nThanks very much!\n> \n>> +\n>> +\t/*\n>> +\t * Disable the black level settings here - we do that through another\n>> +\t * parameters block.\n>> +\t */\n>> +\tconfig->black_level.bl_r = 0;\n>> +\tconfig->black_level.bl_gr = 0;\n>> +\tconfig->black_level.bl_gb = 0;\n>> +\tconfig->black_level.bl_b = 0;\n>> +\n>> +\tipu3_uapi_shd_lut *lut = &params->acc_param.shd.shd_lut;\n>> +\n>> +\tconst auto &set = lscAlgo_.interpolateComponents(quantizedCt);\n>> +\n>> +\tunsigned int totalCells = numHCells_ * numVCells_;\n>> +\tunsigned int cellsPerSet = numHCells_ * config->grid.grid_height_per_slice;\n>> +\tunsigned int numSets = (numHCells_ + config->grid.grid_height_per_slice - 1) /\n>> +\t\t\t       config->grid.grid_height_per_slice;\n>> +\tunsigned int i = 0;\n>> +\n>> +\tfor (unsigned int s = 0; s < numSets; s++) {\n>> +\t\tfor (unsigned int c = 0; c < cellsPerSet && i < totalCells; c++, i++) {\n>> +\t\t\tlut->sets[s].r_and_gr[c].r = set.at(\"r\")[i];\n>> +\t\t\tlut->sets[s].r_and_gr[c].gr = set.at(\"gr\")[i];\n>> +\t\t\tlut->sets[s].gb_and_b[c].gb = set.at(\"gb\")[i];\n>> +\t\t\tlut->sets[s].gb_and_b[c].b = set.at(\"b\")[i];\n>> +\t\t}\n>> +\t}\n>> +\n>> +\tlastAppliedCt_ = ct;\n>> +\tlastAppliedQuantizedCt_ = quantizedCt;\n>> +\tLOG(IPU3Lsc, Debug)\n>> +\t\t<< \"ct is \" << ct << \", quantized to \"\n>> +\t\t<< quantizedCt;\n>> +}\n>> +\n>> +/**\n>> + * \\copydoc libcamera::ipa::Algorithm::process\n>> + */\n>> +void Lsc::process([[maybe_unused]] IPAContext &context,\n>> +\t\t  [[maybe_unused]] const uint32_t frame,\n>> +\t\t  IPAFrameContext &frameContext,\n>> +\t\t  [[maybe_unused]] const ipu3_uapi_stats_3a *stats,\n>> +\t\t  ControlList &metadata)\n>> +{\n>> +\tlscAlgo_.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..f86505029f005c9832c3efa213b86848222ddf62\n>> --- /dev/null\n>> +++ b/src/ipa/ipu3/algorithms/lsc.h\n>> @@ -0,0 +1,58 @@\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 \"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>> +\tint init(IPAContext &context, const ValueNode &tuningData) override;\n>> +\tvoid queueRequest(IPAContext &context, const uint32_t frame,\n>> +\t\t\t  IPAFrameContext &frameContext,\n>> +\t\t\t  const ControlList &controls) override;\n>> +\tint configure(IPAContext &context, const IPAConfigInfo &configInfo) override;\n>> +\tvoid prepare(IPAContext &context, const uint32_t frame,\n>> +\t\t     IPAFrameContext &frameContext,\n>> +\t\t     ipu3_uapi_params *params) override;\n>> +\tvoid process(IPAContext &context, const uint32_t frame,\n>> +\t\t     IPAFrameContext &frameContext,\n>> +\t\t     const ipu3_uapi_stats_3a *stats,\n>> +\t\t     ControlList &metadata) override;\n>> +\n>> +private:\n>> +\tstd::vector<double> calculatePositions(unsigned int dimension);\n>> +\n>> +\tunsigned int numHCells_;\n>> +\tunsigned int numVCells_;\n>> +\tunsigned int blockWidthLog2_;\n>> +\tunsigned int blockHeightLog2_;\n>> +\n>> +\tunsigned int lastAppliedCt_;\n>> +\tunsigned int lastAppliedQuantizedCt_;\n>> +\n>> +\tunsigned int sensorWidth_;\n>> +\tunsigned int sensorHeight_;\n>> +\tunsigned int cropWidth_;\n>> +\tunsigned int cropHeight_;\n>> +\n>> +\tbool polynomial_;\n> \n> I would be nice if we could avoid this, but for now I think this is\n> fine\n> \n>> +\n>> +\tLscAlgorithm<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 7fcfd5e0e4ade92521cc2914dd07113235af8e45..071a7415b61f574d1b525d04d4434ac48fccb23e 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>> @@ -191,4 +196,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 f157f223cbb3119f108d768b14fca514ac5661ca..b517639b1e5878138f653918cd2dc71b49d97264 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>> @@ -70,6 +71,7 @@ struct IPAActiveState {\n>>   \tipa::awb::ActiveState awb;\n>>   \tipa::ccm::ActiveState ccm;\n>>   \tipa::gamma::ActiveState gamma;\n>> +\tipa::lsc::ActiveState lsc;\n>>   };\n>>\n>>   struct IPAFrameContext : public FrameContext {\n>> @@ -81,6 +83,7 @@ struct IPAFrameContext : public FrameContext {\n>>   \tipa::awb::FrameContext awb;\n>>   \tipa::ccm::FrameContext ccm;\n>>   \tipa::gamma::FrameContext gamma;\n>> +\tipa::lsc::FrameContext lsc;\n>>   };\n>>\n>>   struct IPAContext {\n> \n> All minors! Thanks for the effort\n> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>\n\nAnd thanks again :)\n\nDan\n\n> \n> Thanks\n>    j\n> \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 38326BF415\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 26 Jun 2026 10:51:51 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id DEC26658F3;\n\tFri, 26 Jun 2026 12:51:50 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 5F514658E5\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 26 Jun 2026 12:51:49 +0200 (CEST)","from [192.168.0.43]\n\t(chfd-03-b2-v4wan-176392-cust229.vm15.cable.virginm.net\n\t[82.19.20.230])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 87F76174;\n\tFri, 26 Jun 2026 12:51:08 +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=\"PJAilklk\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1782471068;\n\tbh=GvJwUSM/U8aKAHhd34hZ4Vg3Lz8cBa8idYnnQfL0c3Q=;\n\th=Date:Subject:To:Cc:References:From:In-Reply-To:From;\n\tb=PJAilklk1KMaqCRbv2XRTsUa524g1BAvwsvRYx5q2PbcI15+x1A1ZpexKvssctqTW\n\tfLLneHqbHW4bdH6JqPdv01lkAn6SFx2drTkbjs3lqyyGMYK2PGW7RcW9zsx+Y09Pzm\n\tBtwLFiGR/nNo1Qa9TVJmy2nEXUNXWH3FSpnq7dxc=","Message-ID":"<e51d7edb-c276-4ed3-a56b-934517957e2d@ideasonboard.com>","Date":"Fri, 26 Jun 2026 11:51:46 +0100","MIME-Version":"1.0","User-Agent":"Mozilla Thunderbird","Subject":"Re: [PATCH 10/10] ipa: ipu3: Add Lens Shading Correction algorithm","To":"Jacopo Mondi <jacopo.mondi@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","References":"<20260616-ipu3-libipa-rework-v1-0-d4448b54f1d8@ideasonboard.com>\n\t<20260616-ipu3-libipa-rework-v1-10-d4448b54f1d8@ideasonboard.com>\n\t<ajJu7BkyWsxqKmLa@zed>","Content-Language":"en-US","From":"Dan Scally <dan.scally@ideasonboard.com>","In-Reply-To":"<ajJu7BkyWsxqKmLa@zed>","Content-Type":"text/plain; charset=UTF-8; format=flowed","Content-Transfer-Encoding":"7bit","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>"}}]