{"id":26886,"url":"https://patchwork.libcamera.org/api/1.1/patches/26886/?format=json","web_url":"https://patchwork.libcamera.org/patch/26886/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/1.1/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20260615-libipa-algorithms-v1-9-e949c937422e@ideasonboard.com>","date":"2026-06-15T14:05:34","name":"[09/11] ipa: libipa: Introduce LscAlgorithm","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"67b0acb539a871b26416babcb3af1501b0828224","submitter":{"id":143,"url":"https://patchwork.libcamera.org/api/1.1/people/143/?format=json","name":"Jacopo Mondi","email":"jacopo.mondi@ideasonboard.com"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/26886/mbox/","series":[{"id":5992,"url":"https://patchwork.libcamera.org/api/1.1/series/5992/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=5992","date":"2026-06-15T14:05:25","name":"ipa: libipa: Introduce libipa algorithms","version":1,"mbox":"https://patchwork.libcamera.org/series/5992/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/26886/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/26886/checks/","tags":{},"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 9928EC3301\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 15 Jun 2026 14:06:06 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id D5EB7623FA;\n\tMon, 15 Jun 2026 16:06:02 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 65DF4623EC\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 15 Jun 2026 16:05:49 +0200 (CEST)","from [192.168.1.104] (net-93-65-100-155.cust.vodafonedsl.it\n\t[93.65.100.155])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 92D60241;\n\tMon, 15 Jun 2026 16:05:16 +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=\"PE92C4UM\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1781532316;\n\tbh=il6dW1yhdjqzh7pzp9AACFqt7c/EgOicybB1lNxWM78=;\n\th=From:Date:Subject:References:In-Reply-To:To:Cc:From;\n\tb=PE92C4UMpQ4vlJfJfj4Jz+jVxJnxDbkuIezpyvcX9SD3kphOiWPrEYEvSneEhD8H1\n\tHyICdpCeVthVXIxqeUbaC+uYJaf59XNTUTaFO8nd0baBp1cN6aJFO0KBcHpi4dRcC6\n\t+NdNm/nAdAEH4Xk4H9SVtC+jr5dLjKZFslTAnwig=","From":"Jacopo Mondi <jacopo.mondi@ideasonboard.com>","Date":"Mon, 15 Jun 2026 16:05:34 +0200","Subject":"[PATCH 09/11] ipa: libipa: Introduce LscAlgorithm","MIME-Version":"1.0","Content-Type":"text/plain; charset=\"utf-8\"","Content-Transfer-Encoding":"7bit","Message-Id":"<20260615-libipa-algorithms-v1-9-e949c937422e@ideasonboard.com>","References":"<20260615-libipa-algorithms-v1-0-e949c937422e@ideasonboard.com>","In-Reply-To":"<20260615-libipa-algorithms-v1-0-e949c937422e@ideasonboard.com>","To":"libcamera-devel@lists.libcamera.org","Cc":"Jacopo Mondi <jacopo.mondi@ideasonboard.com>","X-Mailer":"b4 0.14.3","X-Developer-Signature":"v=1; a=openpgp-sha256; l=58176;\n\ti=jacopo.mondi@ideasonboard.com; h=from:subject:message-id;\n\tbh=il6dW1yhdjqzh7pzp9AACFqt7c/EgOicybB1lNxWM78=;\n\tb=owEBbQKS/ZANAwAKAXI0Bo8WoVY8AcsmYgBqMAa5vp8qTM83epZioQu7EdJVoGDXEUalj+W3k\n\tDcHd68U2HaJAjMEAAEKAB0WIQS1xD1IgJogio9YOMByNAaPFqFWPAUCajAGuQAKCRByNAaPFqFW\n\tPDwJD/kBwzJ5KLtK/UtgWf4FD9/1ubii5IFd2pugmaLFoVRC8HSCb1V3gLR/xYED7odZVVrc+Nv\n\tzLtKtR9M9nnwN448By7xGAGY0AmPiRcwS1kgd/afyib9QZiIGtct6VmWZJ2QOI5aJF3F3rrfx5w\n\tlOgj6erZLGQoWtkmtyIolOiTgwb6bUua3IVSueU5LuhnXtS0IWV6M0xLnQbN752sQm1+FqxqpGS\n\thWmkUlJzjrA6UStXeCPQbrxyIUoWfqo8Bi+m9QlAPR1P+Y4X0InB1n/S96USzxIBYS4ATGIRmuG\n\tH2/5R8daXs9guSjk+IVNIvqreBfy7xvQ9ljoFhokISU2NTLhU/xLrTHOG+glFInJtdnijZmyNT0\n\tbLqecvE96/GNjf5jul/0V9fKyyr97mrrnD5UZFshy2rCMoUNZLcDUn38WD0azpv/7Vr1FB63msJ\n\tO8Y23R27KKelHPjxC5ms9W+YoVgBclVnEn3URJL/LUh28EUcxHjwhU3/3ynRPLG0vv8dX7L2Qtp\n\t0C4Ne5Qlx3oVZp/3oiWx0qqQdaRsFeKsYiA8pDv6D+m4+G99DMuoGSAAiAmbWTo462iJA/AwhyZ\n\tUrvCN1Bi6pe/rAe8VefC0PuYAR3//+/2L60LcxRpJB316YgILdzfoUQQ9zw8mPENW0KeQemJn3j\n\tw0CKJD+18NdWGtg==","X-Developer-Key":"i=jacopo.mondi@ideasonboard.com; a=openpgp;\n\tfpr=72392EDC88144A65C701EA9BA5826A2587AD026B","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>"},"content":"Introduce an Lsc algorithm implementation in libIPA.\n\nThe LscAlgorithm class supports two algorithms: grid-based lsc and\npolynomial-based lsc.\n\nBoth implementations have been generalized from the existing ones in the\nRkISP1 IPA.\n\nSigned-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>\n---\n src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++\n src/ipa/libipa/lsc.h              | 145 +++++++++++++\n src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++\n src/ipa/libipa/lsc_base.h         |  59 ++++++\n src/ipa/libipa/lsc_grid.cpp       |  33 +++\n src/ipa/libipa/lsc_grid.h         | 113 ++++++++++\n src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-\n src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----\n src/ipa/libipa/meson.build        |   5 +\n src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------\n src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--\n src/ipa/rkisp1/ipa_context.h      |  10 +-\n 12 files changed, 1126 insertions(+), 450 deletions(-)","diff":"diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp\nnew file mode 100644\nindex 000000000000..ae713a776778\n--- /dev/null\n+++ b/src/ipa/libipa/lsc.cpp\n@@ -0,0 +1,322 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2026 Ideas on Board Oy\n+ *\n+ * libIPA Lsc algorithms\n+ */\n+\n+#include \"lsc.h\"\n+\n+#include <libcamera/base/log.h>\n+\n+/**\n+ * \\file lsc.h\n+ * \\brief libipa lsc algorithm\n+ */\n+\n+namespace libcamera {\n+\n+LOG_DEFINE_CATEGORY(Lsc)\n+\n+namespace ipa {\n+\n+namespace lsc {\n+\n+/**\n+ * \\struct ActiveState\n+ * \\brief The lsc active state\n+ *\n+ * \\var ActiveState::enabled\n+ * \\brief Boolean flag for the LscAlgorithm enable status\n+ */\n+\n+/**\n+ * \\struct FrameContext\n+ * \\brief The lsc frame context\n+ *\n+ * \\var FrameContext::enabled\n+ * \\brief Boolean flag for the LscAlgorithm enable status\n+ *\n+ * \\var FrameContext::update\n+ * \\brief Boolean flag for the LscAlgorithm updated status\n+ */\n+\n+} /* namespace lsc */\n+\n+#ifndef __DOXYGEN__\n+template<>\n+void Interpolator<lsc::Components<uint16_t>>::\n+\tinterpolate(const lsc::Components<uint16_t> &a,\n+\t\t    const lsc::Components<uint16_t> &b,\n+\t\t    lsc::Components<uint16_t> &dest,\n+\t\t    double lambda)\n+{\n+\tfor (auto const &[k, v] : a)\n+\t\tinterpolateVector(v, b.at(k), dest[k], lambda);\n+}\n+#endif\n+\n+/**\n+ * \\class LscAlgorithmBase\n+ * \\brief Base class for LscAlgorithm\n+ *\n+ * Base class for LscAlgorithm for non-templated functions implementation\n+ */\n+\n+/**\n+ * \\brief Queue a request to the lsc algorithm\n+ * \\param[in] state The lsc active state\n+ * \\param[in] context The lsc frame context\n+ * \\param[in] controls The list of controls associated with a Request\n+ *\n+ * Queue a new list of \\a controls to the lsc algorithm.\n+ * The only supported control is controls::LensShadingCorrectionEnable.\n+ */\n+void LscAlgorithmBase::queueRequest(lsc::ActiveState &state,\n+\t\t\t\t    lsc::FrameContext &context,\n+\t\t\t\t    const ControlList &controls)\n+{\n+\tconst auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);\n+\tif (lscEnable && *lscEnable != state.enabled) {\n+\t\tstate.enabled = *lscEnable;\n+\n+\t\tLOG(Lsc, Debug)\n+\t\t\t<< (state.enabled ? \"Enabling\" : \"Disabling\") << \" Lsc\";\n+\n+\t\tcontext.update = true;\n+\t}\n+\n+\tcontext.enabled = state.enabled;\n+}\n+\n+/**\n+ * \\brief Populate the list of lsc metadata\n+ * \\param[in] context The lsc frame context\n+ * \\param[in] metadata The list of metadata\n+ *\n+ * Populates the list of \\a metadata with controls handled by the LscAlgorithm\n+ * class. The only supported metadata is controls::LensShadingCorrectionEnable.\n+ */\n+void LscAlgorithmBase::process(lsc::FrameContext &context, ControlList &metadata)\n+{\n+\tmetadata.set(controls::LensShadingCorrectionEnable, context.enabled);\n+}\n+\n+/**\n+ * \\class LscAlgorithm\n+ * \\brief libIPA lsc algorithm implementation\n+ * \\tparam T The type used to store the gain values when loaded from tuning file\n+ * \\tparam U The fixedpoint lsc engine register format\n+ *\n+ * Due to the optical characteristics of the lens, the light intensity received\n+ * by the sensor is not uniform. The Lens Shading Correction algorithm applies\n+ * multipliers to all pixels to compensate for the lens shading effect.\n+ *\n+ * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.\n+ * It provides support for parsing the tuning file content and generate tables\n+ * of per-colour temperature gains that IPA algorithms can use to program their\n+ * lsc engine.\n+ *\n+ * The init() function parses the tuning file and loads the gain tables either\n+ * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain\n+ * tables are organized per-colour temperature with per-colour components gain\n+ * vectors or polynomial coefficients. The colour components names are\n+ * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc\n+ * engine support 4 colour components (r, gr, gb, b), some only support 3 colour\n+ * components (r, g, b). The name (and number) of the expected colour components\n+ * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys\n+ * field.\n+ *\n+ * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour\n+ * components. The gain table has been here omitted, but the expected number\n+ * of entries has be equal to\n+ * LscDescriptor::numHCells * LscDescriptor::numVCells.\n+ *\n+ * \\code{.yaml}\n+ * - Lsc:\n+ *    sets:\n+ *      - ct: 2500\n+ *        r: [\n+ *        \t.. gains table omitted..\n+ *        ]\n+ *        g: [\n+ *        \t.. gains table omitted..\n+ *        ]\n+ *        b: [\n+ *        \t.. gains table omitted..\n+ *        ]\n+ *      - ct: 6500\n+ *        r: [\n+ *        \t.. gains table omitted..\n+ *        ]\n+ *        g: [\n+ *        \t.. gains table omitted..\n+ *        ]\n+ *        b: [\n+ *        \t.. gains table omitted..\n+ *        ]\n+ * \\endcode\n+ *\n+ * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'\n+ * colour components:\n+ *\n+ * \\code{.yaml}\n+ * - Lsc:\n+ *    type: \"polynomial\"\n+ *    sets:\n+ *      - ct: 2500\n+ *        r:\n+ *          cx: 0.5006571711950275\n+ *          cy: 0.510093737499277\n+ *          k0: 1.5393282208428813\n+ *          k1: -1.1434559757908016\n+ *          k2: 4.332602305814554\n+ *          k3: 0.0\n+ *          k4: 0.0\n+ *        gr:\n+ *          cx: 0.5009320529087338\n+ *          cy: 0.511208038949085\n+ *          k0: 1.5634738574805407\n+ *          k1: -1.5623484259968348\n+ *          k2: 4.846686073656501\n+ *          k3: 0.0\n+ *          k4: 0.0\n+ *        gb:\n+ *          cx: 0.5012013290343839\n+ *          cy: 0.5128251541578288\n+ *          k0: 1.526147944919103\n+ *          k1: -1.4316976083689723\n+ *          k2: 4.792604063222728\n+ *          k3: 0.0\n+ *          k4: 0.0\n+ *        b:\n+ *          cx: 0.49864139511067784\n+ *          cy: 0.5162095081739346\n+ *          k0: 1.0405245474038738\n+ *          k1: 0.05618339879447103\n+ *          k2: 1.8792813594001752\n+ *          k3: 0.0\n+ *          k4: 0.0\n+ *      - ct: 6000\n+ *        r:\n+ *          cx: 0.5006202239353942\n+ *          cy: 0.5099531318307661\n+ *          k0: 1.4702946023945032\n+ *          k1: -0.8893767547927631\n+ *          k2: 3.920547732201387\n+ *          k3: 0.0\n+ *          k4: 0.0\n+ *        gr:\n+ *          cx: 0.500907874178317\n+ *          cy: 0.511084916024106\n+ *          k0: 1.5336172760559457\n+ *          k1: -1.39964026514435\n+ *          k2: 4.565487728954618\n+ *          k3: 0.0\n+ *          k4: 0.0\n+ *        gb:\n+ *          cx: 0.5011898608900477\n+ *          cy: 0.5126797906745105\n+ *          k0: 1.5013145790354843\n+ *          k1: -1.2747407173754124\n+ *          k2: 4.514682876897286\n+ *          k3: 0.0\n+ *          k4: 0.0\n+ *        b:\n+ *          cx: 0.4987561413116136\n+ *          cy: 0.5159619420778772\n+ *          k0: 1.0102986422191802\n+ *          k1: 0.13263449763985727\n+ *          k2: 1.686556107316064\n+ *          k3: 0.0\n+ *          k4: 0.0\n+ * \\endcode\n+ *\n+ * The lsc tables or the polynomial definition are generated at tuning time\n+ * using an image of known resolution which needs to be specified in\n+ * LscDescriptor::sensorSize.\n+ *\n+ * At LscAlgorithm::configure() time the lsc tables are re-sampled on the\n+ * sensor's crop rectangle in use to adapt them to the configuration in use for\n+ * a streaming session. Polynomial lsc tables support re-sampling and can be\n+ * applied to any sensor configuration. Grid-based lsc tables cannot be\n+ * re-sampled and the configuration as parsed from the tuning file is used for\n+ * all sensor configurations providing best-effort results.\n+ *\n+ * When the IPA algorithms wants to get access to the (resampled) tables to\n+ * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get\n+ * an lsc table interpolated by the LscAlgorithm class for the specified colour\n+ * temperature. If the algorithm wants to access the non-interpolated tables it\n+ * can retrieve them using LscAlgorithm::getComponents().\n+ */\n+\n+/**\n+ * \\fn LscAlgorithm::init()\n+ * \\param[in] tuningData The tuning data\n+ * \\param[in] controls The IPA list of supported controls\n+ * \\param[in] descriptor The lsc engine descriptor\n+ *\n+ * Parse \\a tuningData according to the settings specified in \\a descriptor to\n+ * populate the lsc data and registers lsc controls in \\a controls.\n+ *\n+ * \\return 0 on success, a negative error code otherwise\n+ */\n+\n+/**\n+ * \\fn LscAlgorithm::configure()\n+ * \\param[in] state The lsc active state\n+ * \\param[in] analogCrop The current sensor analog crop rectangle\n+ * \\param[in] xPos List of horizontal positions of the LSC grid nodes\n+ * \\param[in] yPos List of vertical positions of the LSC grid nodes\n+ *\n+ * Re-sample the lsc data for an \\a analogCrop.\n+ *\n+ * Lsc data are generated at tuning time using a known sensor configuration.\n+ * When a new streaming session is started, it might use a different sensor\n+ * configuration for which the lsc tables need to be adjusted to.\n+ *\n+ * This function re-generates the lsc tables to adapt them to a new sensor\n+ * configuration, specifically it re-samples the lsc data for a new \\a\n+ * analogCrop on a grid specified by \\a xPos and \\a yPos. Re-sampling of\n+ * lsc data is only supported by polynomial-based lsc tables.\n+ *\n+ * \\sa LscImplementation::resampleLscData\n+ *\n+ * \\return 0 on success, a negative error code otherwise\n+ */\n+\n+/**\n+ * \\fn LscAlgorithm::interpolateComponents\n+ * \\brief Interpolate the lsc tables for a given colour temperature\n+ * \\param[in] ct The colour temperature\n+ *\n+ * Lsc data are generated using different colour temperatures during the\n+ * tuning phase.\n+ *\n+ * This function returns the interpolated lsc data for a given \\a ct\n+ * colour temperature.\n+ *\n+ * IPA algorithm can use this function to obtain a list of gains per-colour\n+ * component to program their lsc engines with every time a significant enough\n+ * change in colour temperature is detected.\n+ *\n+ * Calling this function is only valid after LscAlgorithm::configure() has been\n+ * called. An empty components list is returned otherwise.\n+ *\n+ * \\return The lsc gains table interpolated for temperature \\a ct\n+ */\n+\n+/**\n+ * \\fn LscAlgorithm::getComponents\n+ *\n+ * Return the map of lsc data per colour temperature.\n+ *\n+ * Calling this function is only valid after LscAlgorithm::configure() has been\n+ * called. An empty components list is returned otherwise.\n+ *\n+ * \\return The map of lsc gains tables per colour-temperature\n+ */\n+\n+} /* namespace ipa */\n+\n+} /* namespace libcamera */\ndiff --git a/src/ipa/libipa/lsc.h b/src/ipa/libipa/lsc.h\nnew file mode 100644\nindex 000000000000..8bfa3a7c4ecf\n--- /dev/null\n+++ b/src/ipa/libipa/lsc.h\n@@ -0,0 +1,145 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2026 Ideas on Board Oy\n+ *\n+ * libIPA Lsc algorithms\n+ */\n+\n+#pragma once\n+\n+#include <memory>\n+\n+#include <libcamera/control_ids.h>\n+\n+#include \"interpolator.h\"\n+#include \"lsc_grid.h\"\n+#include \"lsc_polynomial.h\"\n+\n+namespace libcamera {\n+\n+LOG_DECLARE_CATEGORY(Lsc)\n+\n+namespace ipa {\n+\n+namespace lsc {\n+\n+struct ActiveState {\n+\tbool enabled;\n+};\n+\n+struct FrameContext {\n+\tbool enabled;\n+\tbool update;\n+};\n+\n+} /* namespace lsc */\n+\n+#ifndef __DOXYGEN__\n+template<typename T>\n+void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,\n+\t\t       std::vector<T> &dest, double lambda)\n+{\n+\tASSERT(a.size() == b.size());\n+\tdest.resize(a.size());\n+\tfor (size_t i = 0; i < a.size(); i++)\n+\t\tdest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;\n+}\n+\n+template<>\n+void Interpolator<lsc::Components<uint16_t>>::\n+\tinterpolate(const lsc::Components<uint16_t> &a,\n+\t\t    const lsc::Components<uint16_t> &b,\n+\t\t    lsc::Components<uint16_t> &dest,\n+\t\t    double lambda);\n+#endif /* __DOXYGEN__ */\n+\n+class LscAlgorithmBase\n+{\n+public:\n+\tvoid queueRequest(lsc::ActiveState &state, lsc::FrameContext &context,\n+\t\t\t  const ControlList &controls);\n+\tvoid process(lsc::FrameContext &context, ControlList &metadata);\n+};\n+\n+template<typename T, typename U>\n+class LscAlgorithm : public LscAlgorithmBase\n+{\n+public:\n+\tint init(const ValueNode &tuningData, ControlInfoMap::Map &controls,\n+\t\t LscDescriptor descriptor)\n+\t{\n+\t\tpolynomial_ = false;\n+\n+\t\tstd::string type = tuningData[\"type\"].get<std::string>(\"table\");\n+\t\tif (type == \"table\") {\n+\t\t\timpl_ = std::make_unique<LscGrid<T, U>>();\n+\t\t\tLOG(Lsc, Debug) << \"Using grid-based Lsc\";\n+\t\t} else if (type == \"polynomial\") {\n+\t\t\timpl_ = std::make_unique<LscPolynomial<T, U>>();\n+\t\t\tpolynomial_ = true;\n+\t\t\tLOG(Lsc, Debug) << \"Using polynomial Lsc\";\n+\t\t} else {\n+\t\t\tLOG(Lsc, Error) << \"Unsupported Lsc algorithm '\"\n+\t\t\t\t\t<< type << \"'\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tconst ValueNode &yamlSets = tuningData[\"sets\"];\n+\t\tif (!yamlSets.isList()) {\n+\t\t\tLOG(Lsc, Error) << \"'sets' parameter not found in tuning file\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tint ret = impl_->parseLscData(yamlSets, descriptor);\n+\t\tif (ret)\n+\t\t\treturn ret;\n+\n+\t\tcontrols[&controls::LensShadingCorrectionEnable] =\n+\t\t\tControlInfo(false, true, true);\n+\n+\t\treturn 0;\n+\t}\n+\n+\tint configure(lsc::ActiveState &state, const Rectangle &analogCrop,\n+\t\t      const std::vector<double> &xPos,\n+\t\t      const std::vector<double> &yPos)\n+\t{\n+\t\tLOG(Lsc, Debug) << \"Sample Lsc data for \" << analogCrop;\n+\t\tlsc::ComponentsMap<T> lscData =\n+\t\t\timpl_->resampleLscData(analogCrop, xPos, yPos);\n+\n+\t\t/*\n+\t\t * Retain a copy of the components table.\n+\t\t *\n+\t\t * We could avoid a copy here if getComponents() could\n+\t\t * return sets_.data() but I wasn't able to work around the\n+\t\t * compiler refusing it.\n+\t\t */\n+\t\tlscData_ = lscData;\n+\n+\t\tsets_.setData(std::move(lscData));\n+\t\tstate.enabled = true;\n+\n+\t\treturn 0;\n+\t}\n+\n+\tconst lsc::Components<T> interpolateComponents(unsigned int ct)\n+\t{\n+\t\treturn sets_.getInterpolated(ct);\n+\t}\n+\n+\tconst lsc::ComponentsMap<T> getComponents()\n+\t{\n+\t\treturn lscData_;\n+\t}\n+\n+private:\n+\tstd::unique_ptr<LscImplementation<T, U>> impl_;\n+\tInterpolator<lsc::Components<T>> sets_;\n+\tlsc::ComponentsMap<T> lscData_;\n+\tbool polynomial_;\n+};\n+\n+} /* namespace ipa */\n+\n+} /* namespace libcamera */\ndiff --git a/src/ipa/libipa/lsc_base.cpp b/src/ipa/libipa/lsc_base.cpp\nnew file mode 100644\nindex 000000000000..fba8d2f00d54\n--- /dev/null\n+++ b/src/ipa/libipa/lsc_base.cpp\n@@ -0,0 +1,152 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2026 Ideas on Board Oy\n+ *\n+ * Base classes and types for LSC algorithms implementations\n+ */\n+\n+#include \"lsc_base.h\"\n+\n+/**\n+ * \\file lsc_base.h\n+ * \\brief Base types and definitions for LscImplementation class hierarchy\n+ *\n+ * Split the common classes and types definitions to a dedicated file to avoid\n+ * circular inclusions.\n+ *\n+ * Both LscGrid and LscPolynomial inherit from LscImplementation and they are\n+ * instantiated from lsc.h as the LscAlgorithm::init() function is there inlined.\n+ *\n+ * For this reason lsc.h needs to include lsc_grid.h and lsc_polynomial.h which\n+ * need the LscImplementation class definition themselves.\n+ *\n+ * Split the LscImplementation interface definition to this file so that it can\n+ * be included by both lsc_grid.h and lsc_polynomial.h, which are then included\n+ * by lsc.h.\n+ */\n+\n+namespace libcamera {\n+\n+namespace ipa {\n+\n+namespace lsc {\n+\n+/**\n+ * \\class Components\n+ * \\brief Associate a colour components with a list of gains\n+ * \\tparam T The type used to store the gain values when loaded from tuning file\n+ *\n+ * Lsc tables are defined as a list of gain values associated to a colour\n+ * component.\n+ *\n+ * As different ISP support different colour components (usually 'r', 'gr',\n+ * 'gb', 'b' or just 'r', 'g', 'b') this class associates a string\n+ * identifier for the colour component to a list of gains of type \\a T.\n+ *\n+ * Each key name shall match an entry in the tuning file and the type \\a T\n+ * shall match the size of the registers where gains are stored.\n+ *\n+ * The list of keys is provided to the LscAlgorithm class using \\a\n+ * LscDescriptor::keys.\n+ */\n+\n+/**\n+ * \\class ComponentsMap\n+ * \\brief Associate a colour temperature to a lsc table\n+ * \\tparam T The type used to store the gain values when loaded from tuning file\n+ *\n+ * An lsc table is generated during the tuning phase for a specific light\n+ * temperature, and a tuning file usually contains lsc tables generated for\n+ * several different colour temperatures.\n+ *\n+ * This class associates an lsc table to the colour temperature used when the\n+ * table has been generated.\n+ */\n+\n+} /* namespace lsc */\n+\n+/**\n+ * \\struct LscDescriptor\n+ * \\brief Describe the ISP lsc engine\n+ *\n+ * \\var LscDescriptor::keys\n+ * \\brief The list of colour components to which a list of gains is associated\n+ * with in the tuning file. Used for parsing the tuning file\n+ *\n+ * \\var LscDescriptor::numHCells\n+ * \\brief Number of horizontal cells of the ISP lsc grid. Used for validating\n+ * the list of gains parsed from tuning file\n+ *\n+ * \\var LscDescriptor::numVCells\n+ * \\brief Number of vertical cells of the ISP lsc grid. Used for validating\n+ * the list of gains parsed from tuning file\n+ *\n+ * \\var LscDescriptor::sensorSize\n+ * \\brief The physical sensor size. This is the largest frame size used to\n+ * generate the lsc table. Only used by the polynomial lsc algorithm\n+ */\n+\n+/**\n+ * \\class LscImplementation\n+ * \\brief Pure virtual base class for lsc algorithm implementations\n+ * \\tparam T The type used to store the gain values when loaded from tuning file\n+ * \\tparam U The fixedpoint format used to convert gain values generated by\n+ * polynomial expansion to the register format\n+ *\n+ * Defines the interface for the lsc algorithm implementation. Currently\n+ * implemented by LscGrid and LscPolynomial.\n+ */\n+\n+/**\n+ * \\fn LscImplementation::~LscImplementation\n+ * \\brief Virtual class destructor\n+ */\n+\n+/**\n+ * \\fn LscImplementation::parseLscData\n+ * \\brief Parse \\a tuningData using \\a descriptor\n+ * \\param[in] tuningData The tuning data\n+ * \\param[in] descriptor The lsc engine descriptor\n+ *\n+ * Parse the tuning file using the \\a descriptor to identify the colour\n+ * components in the tuning data and validate the size of the loaded gains\n+ * tables.\n+ *\n+ * \\return 0 on success, a negative error number otherwise\n+ */\n+\n+/**\n+ * \\fn LscImplementation::resampleLscData\n+ * \\brief Re-sample the lsc components for \\a cropRectangle\n+ * \\param[in] cropRectangle The sensor analogue crop rectangle\n+ * \\param[in] xPos List of horizontal positions of the lsc grid nodes\n+ * \\param[in] yPos List of vertical positions of the lsc grid nodes\n+ *\n+ * Lsc tables are expressed in two formats:\n+ * - A list of gain values (LscGrid)\n+ * - A radial polynomial (LscPolynomial)\n+ *\n+ * Grid-based lsc data are generated using an image at a fixed resolution and\n+ * can't at the moment be re-sampled when a different resolution is used for a\n+ * streaming session. Re-sampling a grid lsc table will return the same table\n+ * as loaded from the tuning file.\n+ *\n+ * Polynomial are more flexible and can be re-sampled for a given resolution\n+ * using a list of horizontal and vertical nodes that define the lsc grid.\n+ * Polynomial lsc tables have to be re-sampled every time a new configuration is\n+ * applied, as each streaming session might use a different sensor crop\n+ * rectangle.\n+ *\n+ * \\a cropRectangle represents the size of the frame on which the polynomial Lsc\n+ * has to be re-sampled on.\n+ *\n+ * \\a xPos and \\a yPos represent the position of the grid nodes vertexes in\n+ * the [0, 1] interval. In example an equally spaced grid of 16 nodes will have\n+ * each segment of size 0.0625 and the list of nodes position will be\n+ * [0, 0.0625, 0.125, 0.1875, ... , 1]. It is expected that the first position\n+ * is 0 and the last position is 1.\n+ */\n+\n+} /* namespace ipa */\n+\n+} /* namespace libcamera */\ndiff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h\nnew file mode 100644\nindex 000000000000..aecae305aa9c\n--- /dev/null\n+++ b/src/ipa/libipa/lsc_base.h\n@@ -0,0 +1,59 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2026 Ideas on Board Oy\n+ *\n+ * Base classes and types for LSC algorithms implementations\n+ */\n+\n+#pragma once\n+\n+#include <map>\n+#include <string>\n+#include <vector>\n+\n+#include <libcamera/geometry.h>\n+\n+#include \"libcamera/internal/value_node.h\"\n+\n+namespace libcamera {\n+\n+namespace ipa {\n+\n+namespace lsc {\n+\n+template<typename T>\n+class Components : public std::map<std::string, std::vector<T>>\n+{\n+};\n+\n+template<typename T>\n+class ComponentsMap : public std::map<unsigned int, Components<T>>\n+{\n+};\n+\n+} /* namespace lsc */\n+\n+struct LscDescriptor {\n+\tstd::vector<std::string> keys;\n+\tunsigned int numHCells;\n+\tunsigned int numVCells;\n+\tSize sensorSize;\n+};\n+\n+template<typename T, typename U>\n+class LscImplementation\n+{\n+public:\n+\tvirtual ~LscImplementation() = default;\n+\n+\tvirtual int parseLscData(const ValueNode &tuningData,\n+\t\t\t\t const LscDescriptor &descriptor) = 0;\n+\n+\tvirtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,\n+\t\t\t\t\t\t      const std::vector<double> &xPos,\n+\t\t\t\t\t\t      const std::vector<double> &yPos) = 0;\n+};\n+\n+} /* namespace ipa */\n+\n+} /* namespace libcamera */\ndiff --git a/src/ipa/libipa/lsc_grid.cpp b/src/ipa/libipa/lsc_grid.cpp\nnew file mode 100644\nindex 000000000000..878994acc8c9\n--- /dev/null\n+++ b/src/ipa/libipa/lsc_grid.cpp\n@@ -0,0 +1,33 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2026, Ideas On Board\n+ *\n+ * Grid based lens shading correction\n+ */\n+\n+#include \"lsc_grid.h\"\n+\n+/**\n+ * \\file lsc_grid.h\n+ * \\brief LscGrid class\n+ */\n+\n+namespace libcamera {\n+\n+LOG_DEFINE_CATEGORY(LscGrid)\n+\n+namespace ipa {\n+\n+/**\n+ * \\class LscGrid\n+ * \\brief Grid based lsc algorithm implementation\n+ *\n+ * Grid based lsc algorithm implementation. The LscGrid class implements lsc\n+ * support using tabular lsc data.\n+ *\n+ * \\sa LscImplementation\n+ * \\sa LscAlgorithm\n+ */\n+\n+} /* namespace ipa */\n+} /* namespace libcamera */\ndiff --git a/src/ipa/libipa/lsc_grid.h b/src/ipa/libipa/lsc_grid.h\nnew file mode 100644\nindex 000000000000..e92cde95d18d\n--- /dev/null\n+++ b/src/ipa/libipa/lsc_grid.h\n@@ -0,0 +1,113 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2026, Ideas On Board\n+ *\n+ * Grid based lens shading correction\n+ */\n+\n+#pragma once\n+\n+#include <vector>\n+\n+#include <libcamera/base/log.h>\n+\n+#include \"lsc_base.h\"\n+\n+namespace libcamera {\n+\n+LOG_DECLARE_CATEGORY(LscGrid)\n+\n+namespace ipa {\n+\n+template<typename T, typename U>\n+class LscGrid : public LscImplementation<T, U>\n+{\n+public:\n+\t~LscGrid() {}\n+\n+\tint parseLscData(const ValueNode &yamlSets,\n+\t\t\t const LscDescriptor &descriptor) override\n+\t{\n+\t\tconst auto &sets = yamlSets.asList();\n+\n+\t\tfor (const auto &yamlSet : sets) {\n+\t\t\tuint32_t ct = yamlSet[\"ct\"].get<uint32_t>(0);\n+\n+\t\t\tint ret = parseLscComponent(yamlSet, ct, descriptor);\n+\t\t\tif (ret)\n+\t\t\t\treturn ret;\n+\t\t}\n+\n+\t\tif (lscData_.empty()) {\n+\t\t\tLOG(LscGrid, Error) << \"Failed to load any sets\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\treturn 0;\n+\t}\n+\n+\tlsc::ComponentsMap<T> resampleLscData([[maybe_unused]] const Rectangle &cropRectangle,\n+\t\t\t\t\t      [[maybe_unused]] const std::vector<double> &xPos,\n+\t\t\t\t\t      [[maybe_unused]] const std::vector<double> &yPos) override\n+\t{\n+\t\t/* No resampling for grid-based LSC algorithm. */\n+\t\treturn lscData_;\n+\t}\n+\n+private:\n+\tint parseLscComponent(const ValueNode &yamlSet,\n+\t\t\t      unsigned int ct, const LscDescriptor &descriptor)\n+\t{\n+\t\tlsc::Components<T> component;\n+\t\tfor (auto &k : descriptor.keys) {\n+\t\t\tauto [it, inserted] = component.emplace(\n+\t\t\t\tstd::piecewise_construct,\n+\t\t\t\tstd::forward_as_tuple(k.c_str()),\n+\t\t\t\tstd::forward_as_tuple(parseTable(yamlSet,\n+\t\t\t\t\t\t\t\t k.c_str(),\n+\t\t\t\t\t\t\t\t descriptor.numHCells,\n+\t\t\t\t\t\t\t\t descriptor.numVCells)));\n+\t\t\tif (!inserted || it->second.empty()) {\n+\t\t\t\tLOG(LscGrid, Error)\n+\t\t\t\t\t<< \"Set \" << k << \" for color temperature \"\n+\t\t\t\t\t<< ct << \" is missing\";\n+\t\t\t\treturn -EINVAL;\n+\t\t\t}\n+\t\t}\n+\n+\t\tauto [it, inserted] = lscData_.emplace(ct, component);\n+\t\tif (!inserted) {\n+\t\t\tLOG(LscGrid, Error)\n+\t\t\t\t<< \"Multiple sets found for color temperature \"\n+\t\t\t\t<< ct;\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\treturn 0;\n+\t}\n+\n+\tstd::vector<T> parseTable(const ValueNode &tuningData,\n+\t\t\t\t  const char *prop, unsigned int numHCells,\n+\t\t\t\t  unsigned int numVCells)\n+\t{\n+\t\tunsigned int kLscNumSamples = numHCells * numVCells;\n+\n+\t\tstd::vector<T> table =\n+\t\t\ttuningData[prop].get<std::vector<T>>().value_or(utils::defopt);\n+\t\tif (table.size() != kLscNumSamples) {\n+\t\t\tLOG(LscGrid, Error)\n+\t\t\t\t<< \"Invalid '\" << prop << \"' values: expected \"\n+\t\t\t\t<< kLscNumSamples\n+\t\t\t\t<< \" elements, got \" << table.size();\n+\t\t\treturn {};\n+\t\t}\n+\n+\t\treturn table;\n+\t}\n+\n+\tlsc::ComponentsMap<T> lscData_;\n+};\n+\n+} /* namespace ipa */\n+\n+} /* namespace libcamera */\ndiff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp\nindex f607d86c54c3..8d3f1883d6a5 100644\n--- a/src/ipa/libipa/lsc_polynomial.cpp\n+++ b/src/ipa/libipa/lsc_polynomial.cpp\n@@ -1,12 +1,14 @@\n /* SPDX-License-Identifier: LGPL-2.1-or-later */\n /*\n- * Copyright (C) 2024, Ideas On Board\n+ * Copyright (C) 2026, Ideas On Board\n  *\n- * Polynomial class to represent lens shading correction\n+ * Polynomial based lens shading correction\n  */\n \n #include \"lsc_polynomial.h\"\n \n+#include <assert.h>\n+\n #include <libcamera/base/log.h>\n \n /**\n@@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)\n namespace ipa {\n \n /**\n- * \\class LscPolynomial\n+ * \\class Polynomial\n  * \\brief Class for handling even polynomials used in lens shading correction\n  *\n  * Shading artifacts of camera lenses can be modeled using even radial\n@@ -31,7 +33,7 @@ namespace ipa {\n  */\n \n /**\n- * \\fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,\n+ * \\fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,\n \t\t      double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,\n \t\t      double k4 = 0.0)\n  * \\brief Construct a polynomial using the given coefficients\n@@ -45,7 +47,13 @@ namespace ipa {\n  */\n \n /**\n- * \\fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)\n+ * \\fn Polynomial::Polynomial(const Polynomial &other)\n+ * \\brief Construct a Polynomial by copy\n+ * \\param[in] other The Polynomial to copy construct from\n+ */\n+\n+/**\n+ * \\fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)\n  * \\brief Sample the polynomial at the given normalized pixel position\n  *\n  * This functions samples the polynomial at the given pixel position divided by\n@@ -55,9 +63,21 @@ namespace ipa {\n  * \\param y y position in normalized coordinates\n  * \\return The sampled value\n  */\n+double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const\n+{\n+\tdouble dx = x - cnx_;\n+\tdouble dy = y - cny_;\n+\tdouble r = sqrt(dx * dx + dy * dy);\n+\tdouble res = 1.0;\n+\n+\tfor (unsigned int i = 0; i < coefficients_.size(); i++)\n+\t\tres += coefficients_[i] * std::pow(r, (i + 1) * 2);\n+\n+\treturn res;\n+}\n \n /**\n- * \\fn LscPolynomial::getM()\n+ * \\fn Polynomial::getM()\n  * \\brief Get the value m as described in the dng specification\n  *\n  * Returns m according to dng spec. m represents the Euclidean distance\n@@ -66,9 +86,18 @@ namespace ipa {\n  *\n  * \\return The sampled value\n  */\n+double Polynomial::getM() const\n+{\n+\tdouble cpx = imageSize_.width * cx_;\n+\tdouble cpy = imageSize_.height * cy_;\n+\tdouble mx = std::max(cpx, std::fabs(imageSize_.width - cpx));\n+\tdouble my = std::max(cpy, std::fabs(imageSize_.height - cpy));\n+\n+\treturn sqrt(mx * mx + my * my);\n+}\n \n /**\n- * \\fn LscPolynomial::setReferenceImageSize(const Size &size)\n+ * \\fn Polynomial::setReferenceImageSize(const Size &size)\n  * \\brief Set the reference image size\n  *\n  * Set the reference image size that is used for subsequent calls to getM() and\n@@ -76,6 +105,92 @@ namespace ipa {\n  *\n  * \\param size The size of the reference image\n  */\n+void Polynomial::setReferenceImageSize(const Size &size)\n+{\n+\tassert(!size.isNull());\n+\timageSize_ = size;\n+\n+\t/* Calculate normalized centers */\n+\tdouble m = getM();\n+\tcnx_ = (size.width * cx_) / m;\n+\tcny_ = (size.height * cy_) / m;\n+}\n+\n+/**\n+ * \\class LscPolynomialBase\n+ * \\brief Base class for LscPolynomial\n+ *\n+ * Base class for LscPolynomial for non-templated functions.\n+ */\n+\n+/**\n+ * \\brief Parse polynomial lsc data\n+ * \\param[in] yamlSets The tuning file content\n+ * \\param[in] descriptor The lsc engine descriptor\n+ *\n+ * Parse the lsc data in polyomial form from the \\a yamlSet tuning data.\n+ */\n+int LscPolynomialBase::parseLscData(const ValueNode &yamlSets,\n+\t\t\t\t    const LscDescriptor &descriptor)\n+{\n+\tconst auto &sets = yamlSets.asList();\n+\tfor (const auto &yamlSet : sets) {\n+\t\tuint32_t ct = yamlSet[\"ct\"].get<uint32_t>(0);\n+\n+\t\tComponents components;\n+\t\tfor (auto &k : descriptor.keys) {\n+\t\t\tauto polynomial = yamlSet[k.c_str()].get<Polynomial>();\n+\t\t\tif (!polynomial) {\n+\t\t\t\tLOG(LscPolynomial, Error)\n+\t\t\t\t\t<< \"Missing polynomial for component \"\n+\t\t\t\t\t<< k;\n+\t\t\t\treturn -EINVAL;\n+\t\t\t}\n+\n+\t\t\tauto [it, inserted] =\n+\t\t\t\tcomponents.emplace(std::piecewise_construct,\n+\t\t\t\t\t\t   std::forward_as_tuple(k.c_str()),\n+\t\t\t\t\t\t   std::forward_as_tuple(*polynomial));\n+\n+\t\t\tit->second.setReferenceImageSize(descriptor.sensorSize);\n+\t\t}\n+\n+\t\tauto [it, inserted] = lscData_.emplace(ct, components);\n+\t\tif (!inserted) {\n+\t\t\tLOG(LscPolynomial, Error)\n+\t\t\t\t<< \"Multiple sets found for \"\n+\t\t\t\t<< \"color temperature \" << ct;\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\t}\n+\n+\tif (lscData_.empty()) {\n+\t\tLOG(LscPolynomial, Error) << \"Failed to load any sets\";\n+\t\treturn -EINVAL;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+/**\n+ * \\var LscPolynomialBase::lscData_\n+ * \\brief The polynomial lsc data\n+ *\n+ * Maps colour temperatures to per-colour radial polynomial definitions.\n+ */\n+\n+/**\n+ * \\class LscPolynomial\n+ * \\brief Radial Polynomial lsc algorithm implementation\n+ *\n+ * Polynomial-based lsc algorithm implementation. The LscPolynomial class\n+ * implements lsc support using Polynomial to represent the shading artifacts\n+ * map.\n+ *\n+ * \\sa LscImplementation\n+ * \\sa LscAlgorithm\n+ *\n+ */\n \n-} // namespace ipa\n-} // namespace libcamera\n+} /* namespace ipa */\n+} /* namespace libcamera */\ndiff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h\nindex d7d9ae42e360..72cca591f260 100644\n--- a/src/ipa/libipa/lsc_polynomial.h\n+++ b/src/ipa/libipa/lsc_polynomial.h\n@@ -1,15 +1,17 @@\n /* SPDX-License-Identifier: LGPL-2.1-or-later */\n /*\n- * Copyright (C) 2024, Ideas On Board\n+ * Copyright (C) 2026, Ideas On Board\n  *\n- * Helper for radial polynomial used in lens shading correction.\n+ * Polynomial based lens shading correction\n  */\n+\n #pragma once\n \n #include <algorithm>\n #include <array>\n-#include <assert.h>\n #include <cmath>\n+#include <optional>\n+#include <tuple>\n \n #include <libcamera/base/log.h>\n #include <libcamera/base/span.h>\n@@ -18,64 +20,117 @@\n \n #include \"libcamera/internal/value_node.h\"\n \n+#include \"lsc_base.h\"\n+\n namespace libcamera {\n \n LOG_DECLARE_CATEGORY(LscPolynomial)\n \n namespace ipa {\n \n-class LscPolynomial\n+class Polynomial\n {\n public:\n-\tLscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,\n-\t\t      double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,\n-\t\t      double k4 = 0.0)\n+\tPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,\n+\t\t   double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,\n+\t\t   double k4 = 0.0)\n \t\t: cx_(cx), cy_(cy), cnx_(0), cny_(0),\n \t\t  coefficients_({ k0, k1, k2, k3, k4 })\n \t{\n \t}\n \n-\tdouble sampleAtNormalizedPixelPos(double x, double y) const\n+\tPolynomial(const Polynomial &other) = default;\n+\n+\tdouble sampleAtNormalizedPixelPos(double x, double y) const;\n+\tdouble getM() const;\n+\tvoid setReferenceImageSize(const Size &size);\n+\n+private:\n+\tdouble cx_;\n+\tdouble cy_;\n+\tdouble cnx_;\n+\tdouble cny_;\n+\tstd::array<double, 5> coefficients_;\n+\tSize imageSize_;\n+};\n+\n+class LscPolynomialBase\n+{\n+private:\n+\tusing Components = std::map<std::string, Polynomial>;\n+\tusing ComponentsMap = std::map<unsigned int, Components>;\n+\n+public:\n+\tint parseLscData(const ValueNode &yamlSets,\n+\t\t\t const LscDescriptor &descriptor);\n+\n+protected:\n+\tComponentsMap lscData_;\n+};\n+\n+template<typename T, typename U>\n+class LscPolynomial : public LscPolynomialBase, public LscImplementation<T, U>\n+{\n+public:\n+\t~LscPolynomial() {}\n+\n+\tint parseLscData(const ValueNode &yamlSets,\n+\t\t\t const LscDescriptor &descriptor) override\n \t{\n-\t\tdouble dx = x - cnx_;\n-\t\tdouble dy = y - cny_;\n-\t\tdouble r = sqrt(dx * dx + dy * dy);\n-\t\tdouble res = 1.0;\n-\t\tfor (unsigned int i = 0; i < coefficients_.size(); i++) {\n-\t\t\tres += coefficients_[i] * std::pow(r, (i + 1) * 2);\n-\t\t}\n-\t\treturn res;\n+\t\treturn LscPolynomialBase::parseLscData(yamlSets, descriptor);\n \t}\n \n-\tdouble getM() const\n+\tlsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,\n+\t\t\t\t\t      const std::vector<double> &xPos,\n+\t\t\t\t\t      const std::vector<double> &yPos) override\n \t{\n-\t\tdouble cpx = imageSize_.width * cx_;\n-\t\tdouble cpy = imageSize_.height * cy_;\n-\t\tdouble mx = std::max(cpx, std::fabs(imageSize_.width - cpx));\n-\t\tdouble my = std::max(cpy, std::fabs(imageSize_.height - cpy));\n+\t\tlsc::ComponentsMap<T> components;\n \n-\t\treturn sqrt(mx * mx + my * my);\n-\t}\n+\t\tfor (auto &[t, c] : lscData_) {\n+\t\t\tlsc::Components<T> comp;\n \n-\tvoid setReferenceImageSize(const Size &size)\n-\t{\n-\t\tassert(!size.isNull());\n-\t\timageSize_ = size;\n+\t\t\tfor (auto &[k, p] : c) {\n+\t\t\t\tcomp.emplace(std::piecewise_construct,\n+\t\t\t\t\t     std::forward_as_tuple(k),\n+\t\t\t\t\t     std::forward_as_tuple(samplePolynomial(p, xPos, yPos,\n+\t\t\t\t\t\t\t\t\t\t    cropRectangle)));\n+\t\t\t}\n \n-\t\t/* Calculate normalized centers */\n-\t\tdouble m = getM();\n-\t\tcnx_ = (size.width * cx_) / m;\n-\t\tcny_ = (size.height * cy_) / m;\n+\t\t\tcomponents[t] = comp;\n+\t\t}\n+\n+\t\treturn components;\n \t}\n \n private:\n-\tdouble cx_;\n-\tdouble cy_;\n-\tdouble cnx_;\n-\tdouble cny_;\n-\tstd::array<double, 5> coefficients_;\n+\tstd::vector<T> samplePolynomial(const Polynomial &poly,\n+\t\t\t\t\tSpan<const double> xPositions,\n+\t\t\t\t\tSpan<const double> yPositions,\n+\t\t\t\t\tconst Rectangle &cropRectangle)\n \n-\tSize imageSize_;\n+\t{\n+\t\tdouble m = poly.getM();\n+\t\tdouble x0 = cropRectangle.x / m;\n+\t\tdouble y0 = cropRectangle.y / m;\n+\t\tdouble w = cropRectangle.width / m;\n+\t\tdouble h = cropRectangle.height / m;\n+\t\tstd::vector<T> samples;\n+\n+\t\tsamples.reserve(xPositions.size() * yPositions.size());\n+\n+\t\tfor (double y : yPositions) {\n+\t\t\tfor (double x : xPositions) {\n+\t\t\t\tdouble xp = x0 + x * w;\n+\t\t\t\tdouble yp = y0 + y * h;\n+\t\t\t\tfloat sample = static_cast<float>\n+\t\t\t\t\t\t(poly.sampleAtNormalizedPixelPos(xp, yp));\n+\n+\t\t\t\tsamples.push_back(U(sample).quantized());\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn samples;\n+\t}\n };\n \n } /* namespace ipa */\n@@ -83,8 +138,8 @@ private:\n #ifndef __DOXYGEN__\n \n template<>\n-struct ValueNode::Accessor<ipa::LscPolynomial> {\n-\tstd::optional<ipa::LscPolynomial> get(const ValueNode &obj) const\n+struct ValueNode::Accessor<ipa::Polynomial> {\n+\tstd::optional<ipa::Polynomial> get(const ValueNode &obj) const\n \t{\n \t\tstd::optional<double> cx = obj[\"cx\"].get<double>();\n \t\tstd::optional<double> cy = obj[\"cy\"].get<double>();\n@@ -98,7 +153,7 @@ struct ValueNode::Accessor<ipa::LscPolynomial> {\n \t\t\tLOG(LscPolynomial, Error)\n \t\t\t\t<< \"Polynomial is missing a parameter\";\n \n-\t\treturn ipa::LscPolynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);\n+\t\treturn ipa::Polynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);\n \t}\n };\n \ndiff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build\nindex edf8eabd8b78..da6ea0c5e130 100644\n--- a/src/ipa/libipa/meson.build\n+++ b/src/ipa/libipa/meson.build\n@@ -14,6 +14,9 @@ libipa_headers = files([\n     'fixedpoint.h',\n     'histogram.h',\n     'interpolator.h',\n+    'lsc.h',\n+    'lsc_base.h',\n+    'lsc_grid.h',\n     'lsc_polynomial.h',\n     'lux.h',\n     'module.h',\n@@ -36,6 +39,8 @@ libipa_sources = files([\n     'fixedpoint.cpp',\n     'histogram.cpp',\n     'interpolator.cpp',\n+    'lsc.cpp',\n+    'lsc_grid.cpp',\n     'lsc_polynomial.cpp',\n     'lux.cpp',\n     'module.cpp',\ndiff --git a/src/ipa/rkisp1/algorithms/lsc.cpp b/src/ipa/rkisp1/algorithms/lsc.cpp\nindex 25f7c512b35c..51076737ffa5 100644\n--- a/src/ipa/rkisp1/algorithms/lsc.cpp\n+++ b/src/ipa/rkisp1/algorithms/lsc.cpp\n@@ -2,7 +2,7 @@\n /*\n  * Copyright (C) 2021-2022, Ideas On Board\n  *\n- * RkISP1 Lens Shading Correction control\n+ * RkISP1 Lens Shading Correction algorithm\n  */\n \n #include \"lsc.h\"\n@@ -14,56 +14,17 @@\n #include <libcamera/base/log.h>\n #include <libcamera/base/utils.h>\n \n-#include \"libcamera/internal/value_node.h\"\n-\n-#include \"libipa/lsc_polynomial.h\"\n-#include \"linux/rkisp1-config.h\"\n-\n /**\n  * \\file lsc.h\n  */\n \n namespace libcamera {\n \n-namespace ipa {\n-\n-template<typename T>\n-void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,\n-\t\t       std::vector<T> &dest, double lambda)\n-{\n-\tASSERT(a.size() == b.size());\n-\tdest.resize(a.size());\n-\tfor (size_t i = 0; i < a.size(); i++)\n-\t\tdest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;\n-}\n-\n-template<>\n-void Interpolator<rkisp1::algorithms::LensShadingCorrection::Components>::\n-\tinterpolate(const rkisp1::algorithms::LensShadingCorrection::Components &a,\n-\t\t    const rkisp1::algorithms::LensShadingCorrection::Components &b,\n-\t\t    rkisp1::algorithms::LensShadingCorrection::Components &dest,\n-\t\t    double lambda)\n-{\n-\tinterpolateVector(a.r, b.r, dest.r, lambda);\n-\tinterpolateVector(a.gr, b.gr, dest.gr, lambda);\n-\tinterpolateVector(a.gb, b.gb, dest.gb, lambda);\n-\tinterpolateVector(a.b, b.b, dest.b, lambda);\n-}\n-\n-} /* namespace ipa */\n-\n namespace ipa::rkisp1::algorithms {\n \n /**\n  * \\class LensShadingCorrection\n  * \\brief RkISP1 Lens Shading Correction control\n- *\n- * Due to the optical characteristics of the lens, the light intensity received\n- * by the sensor is not uniform.\n- *\n- * The Lens Shading Correction algorithm applies multipliers to all pixels\n- * to compensate for the lens shading effect. The coefficients are\n- * specified in a downscaled table in the YAML tuning file.\n  */\n \n LOG_DEFINE_CATEGORY(RkISP1Lsc)\n@@ -72,265 +33,20 @@ namespace {\n \n constexpr int kColourTemperatureQuantization = 10;\n \n-class LscPolynomialShadingDescriptor : public LensShadingCorrection::ShadingDescriptor\n-{\n-public:\n-\tLscPolynomialShadingDescriptor(const LscPolynomial &pr, const LscPolynomial &pgr,\n-\t\t\t\t       const LscPolynomial &pgb, const LscPolynomial &pb)\n-\t\t: pr_(pr), pgr_(pgr), pgb_(pgb), pb_(pb)\n-\t{\n-\t}\n-\n-\tLensShadingCorrection::Components sampleForCrop(const Rectangle &cropRectangle,\n-\t\t\t\t\t\t\tSpan<const double> xSizes,\n-\t\t\t\t\t\t\tSpan<const double> ySizes) override;\n-\n-private:\n-\tstd::vector<uint16_t> samplePolynomial(const LscPolynomial &poly,\n-\t\t\t\t\t       Span<const double> xPositions,\n-\t\t\t\t\t       Span<const double> yPositions,\n-\t\t\t\t\t       const Rectangle &cropRectangle);\n-\n-\tstd::vector<double> sizesListToPositions(Span<const double> sizes);\n-\n-\tLscPolynomial pr_;\n-\tLscPolynomial pgr_;\n-\tLscPolynomial pgb_;\n-\tLscPolynomial pb_;\n-};\n-\n-LensShadingCorrection::Components\n-LscPolynomialShadingDescriptor::sampleForCrop(const Rectangle &cropRectangle,\n-\t\t\t\t\t      Span<const double> xSizes,\n-\t\t\t\t\t      Span<const double> ySizes)\n-{\n-\tstd::vector<double> xPos = sizesListToPositions(xSizes);\n-\tstd::vector<double> yPos = sizesListToPositions(ySizes);\n-\n-\treturn {\n-\t\t.r = samplePolynomial(pr_, xPos, yPos, cropRectangle),\n-\t\t.gr = samplePolynomial(pgr_, xPos, yPos, cropRectangle),\n-\t\t.gb = samplePolynomial(pgb_, xPos, yPos, cropRectangle),\n-\t\t.b = samplePolynomial(pb_, xPos, yPos, cropRectangle)\n-\t};\n-}\n-\n-std::vector<uint16_t>\n-LscPolynomialShadingDescriptor::samplePolynomial(const LscPolynomial &poly,\n-\t\t\t\t\t\t Span<const double> xPositions,\n-\t\t\t\t\t\t Span<const double> yPositions,\n-\t\t\t\t\t\t const Rectangle &cropRectangle)\n-{\n-\tdouble m = poly.getM();\n-\tdouble x0 = cropRectangle.x / m;\n-\tdouble y0 = cropRectangle.y / m;\n-\tdouble w = cropRectangle.width / m;\n-\tdouble h = cropRectangle.height / m;\n-\tstd::vector<uint16_t> samples;\n-\n-\tsamples.reserve(xPositions.size() * yPositions.size());\n-\n-\tfor (double y : yPositions) {\n-\t\tfor (double x : xPositions) {\n-\t\t\tdouble xp = x0 + x * w;\n-\t\t\tdouble yp = y0 + y * h;\n-\t\t\t/*\n-\t\t\t * The hardware uses 2.10 fixed point format and limits\n-\t\t\t * the legal values to [1..3.999]. Scale and clamp the\n-\t\t\t * sampled value accordingly.\n-\t\t\t */\n-\t\t\tint v = static_cast<int>(\n-\t\t\t\tpoly.sampleAtNormalizedPixelPos(xp, yp) *\n-\t\t\t\t1024);\n-\t\t\tv = std::clamp(v, 1024, 4095);\n-\t\t\tsamples.push_back(v);\n-\t\t}\n-\t}\n-\treturn samples;\n-}\n-\n-/*\n- * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left\n- * quadrant of the grid. This is then mirrored in hardware to the other\n- * quadrants. See parseSizes() for further details. For easier handling, this\n- * function converts the cell sizes of half the grid to a list of position of\n- * the whole grid (on one axis). Example:\n- *\n- * input:   | 0.2 | 0.3 |\n- * output: 0.0   0.2   0.5   0.8   1.0\n- */\n-std::vector<double>\n-LscPolynomialShadingDescriptor::sizesListToPositions(Span<const double> sizes)\n-{\n-\tconst int half = sizes.size();\n-\tstd::vector<double> positions(half * 2 + 1);\n-\tdouble x = 0.0;\n-\n-\tpositions[half] = 0.5;\n-\tfor (int i = 1; i <= half; i++) {\n-\t\tx += sizes[half - i];\n-\t\tpositions[half - i] = 0.5 - x;\n-\t\tpositions[half + i] = 0.5 + x;\n-\t}\n-\n-\treturn positions;\n-}\n-\n-class LscPolynomialLoader\n-{\n-public:\n-\tLscPolynomialLoader(const Size &sensorSize)\n-\t\t: sensorSize_(sensorSize)\n-\t{\n-\t}\n-\n-\tint parseLscData(const ValueNode &yamlSets,\n-\t\t\t LensShadingCorrection::ShadingDescriptorMap &lscData);\n-\n-private:\n-\tSize sensorSize_;\n-};\n-\n-int LscPolynomialLoader::parseLscData(const ValueNode &yamlSets,\n-\t\t\t\t      LensShadingCorrection::ShadingDescriptorMap &lscData)\n+unsigned int quantize(unsigned int value, unsigned int step)\n {\n-\tconst auto &sets = yamlSets.asList();\n-\tfor (const auto &yamlSet : sets) {\n-\t\tstd::optional<LscPolynomial> pr, pgr, pgb, pb;\n-\t\tuint32_t ct = yamlSet[\"ct\"].get<uint32_t>(0);\n-\n-\t\tif (lscData.count(ct)) {\n-\t\t\tLOG(RkISP1Lsc, Error)\n-\t\t\t\t<< \"Multiple sets found for \"\n-\t\t\t\t<< \"color temperature \" << ct;\n-\t\t\treturn -EINVAL;\n-\t\t}\n-\n-\t\tpr = yamlSet[\"r\"].get<LscPolynomial>();\n-\t\tpgr = yamlSet[\"gr\"].get<LscPolynomial>();\n-\t\tpgb = yamlSet[\"gb\"].get<LscPolynomial>();\n-\t\tpb = yamlSet[\"b\"].get<LscPolynomial>();\n-\n-\t\tif (!(pr || pgr || pgb || pb)) {\n-\t\t\tLOG(RkISP1Lsc, Error)\n-\t\t\t\t<< \"Failed to parse polynomial for \"\n-\t\t\t\t<< \"colour temperature \" << ct;\n-\t\t\treturn -EINVAL;\n-\t\t}\n-\n-\t\tpr->setReferenceImageSize(sensorSize_);\n-\t\tpgr->setReferenceImageSize(sensorSize_);\n-\t\tpgb->setReferenceImageSize(sensorSize_);\n-\t\tpb->setReferenceImageSize(sensorSize_);\n-\n-\t\tlscData.emplace(\n-\t\t\tct, std::make_unique<LscPolynomialShadingDescriptor>(\n-\t\t\t\t    *pr, *pgr, *pgb, *pb));\n-\t}\n-\n-\tif (lscData.empty()) {\n-\t\tLOG(RkISP1Lsc, Error) << \"Failed to load any sets\";\n-\t\treturn -EINVAL;\n-\t}\n-\n-\treturn 0;\n+\treturn std::lround(value / static_cast<double>(step)) * step;\n }\n \n-class LscTableShadingDescriptor : public LensShadingCorrection::ShadingDescriptor\n-{\n-public:\n-\tLscTableShadingDescriptor(LensShadingCorrection::Components components)\n-\t\t: lscData_(std::move(components))\n-\t{\n-\t}\n-\n-\tLensShadingCorrection::Components\n-\tsampleForCrop([[maybe_unused]] const Rectangle &cropRectangle,\n-\t\t      [[maybe_unused]] Span<const double> xSizes,\n-\t\t      [[maybe_unused]] Span<const double> ySizes) override\n-\t{\n-\t\tLOG(RkISP1Lsc, Warning)\n-\t\t\t<< \"Tabular LSC data doesn't support resampling\";\n-\t\treturn lscData_;\n-\t}\n-\n-private:\n-\tLensShadingCorrection::Components lscData_;\n-};\n-\n-class LscTableLoader\n-{\n-public:\n-\tint parseLscData(const ValueNode &yamlSets,\n-\t\t\t LensShadingCorrection::ShadingDescriptorMap &lscData);\n-\n-private:\n-\tstd::vector<uint16_t> parseTable(const ValueNode &tuningData,\n-\t\t\t\t\t const char *prop);\n-};\n-\n-int LscTableLoader::parseLscData(const ValueNode &yamlSets,\n-\t\t\t\t LensShadingCorrection::ShadingDescriptorMap &lscData)\n-{\n-\tconst auto &sets = yamlSets.asList();\n-\n-\tfor (const auto &yamlSet : sets) {\n-\t\tuint32_t ct = yamlSet[\"ct\"].get<uint32_t>(0);\n-\n-\t\tif (lscData.count(ct)) {\n-\t\t\tLOG(RkISP1Lsc, Error)\n-\t\t\t\t<< \"Multiple sets found for color temperature \"\n-\t\t\t\t<< ct;\n-\t\t\treturn -EINVAL;\n-\t\t}\n-\n-\t\tLensShadingCorrection::Components set;\n-\t\tset.r = parseTable(yamlSet, \"r\");\n-\t\tset.gr = parseTable(yamlSet, \"gr\");\n-\t\tset.gb = parseTable(yamlSet, \"gb\");\n-\t\tset.b = parseTable(yamlSet, \"b\");\n-\n-\t\tif (set.r.empty() || set.gr.empty() ||\n-\t\t    set.gb.empty() || set.b.empty()) {\n-\t\t\tLOG(RkISP1Lsc, Error)\n-\t\t\t\t<< \"Set for color temperature \" << ct\n-\t\t\t\t<< \" is missing tables\";\n-\t\t\treturn -EINVAL;\n-\t\t}\n-\n-\t\tlscData.emplace(\n-\t\t\tct, std::make_unique<LscTableShadingDescriptor>(std::move(set)));\n-\t}\n-\n-\tif (lscData.empty()) {\n-\t\tLOG(RkISP1Lsc, Error) << \"Failed to load any sets\";\n-\t\treturn -EINVAL;\n-\t}\n-\n-\treturn 0;\n-}\n+} /* namespace */\n \n-std::vector<uint16_t> LscTableLoader::parseTable(const ValueNode &tuningData,\n-\t\t\t\t\t\t const char *prop)\n+LensShadingCorrection::LensShadingCorrection()\n+\t: lastAppliedCt_(0), lastAppliedQuantizedCt_(0)\n {\n-\tstatic constexpr unsigned int kLscNumSamples =\n-\t\tRKISP1_CIF_ISP_LSC_SAMPLES_MAX * RKISP1_CIF_ISP_LSC_SAMPLES_MAX;\n-\n-\tstd::vector<uint16_t> table =\n-\t\ttuningData[prop].get<std::vector<uint16_t>>().value_or(utils::defopt);\n-\tif (table.size() != kLscNumSamples) {\n-\t\tLOG(RkISP1Lsc, Error)\n-\t\t\t<< \"Invalid '\" << prop << \"' values: expected \"\n-\t\t\t<< kLscNumSamples\n-\t\t\t<< \" elements, got \" << table.size();\n-\t\treturn {};\n-\t}\n-\n-\treturn table;\n }\n \n-std::vector<double> parseSizes(const ValueNode &tuningData,\n-\t\t\t       const char *prop)\n+std::vector<double> LensShadingCorrection::parseSizes(const ValueNode &tuningData,\n+\t\t\t\t\t\t      const char *prop)\n {\n \tstd::vector<double> sizes =\n \t\ttuningData[prop].get<std::vector<double>>().value_or(utils::defopt);\n@@ -359,22 +75,36 @@ std::vector<double> parseSizes(const ValueNode &tuningData,\n \treturn sizes;\n }\n \n-unsigned int quantize(unsigned int value, unsigned int step)\n+/*\n+ * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left\n+ * quadrant of the grid. This is then mirrored in hardware to the other\n+ * quadrants. See parseSizes() for further details. For easier handling, this\n+ * function converts the cell sizes of half the grid to a list of position of\n+ * the whole grid (on one axis). Example:\n+ *\n+ * input:   | 0.2 | 0.3 |\n+ * output: 0.0   0.2   0.5   0.8   1.0\n+ */\n+std::vector<double> LensShadingCorrection::sizesToPositions(Span<const double> sizes)\n {\n-\treturn std::lround(value / static_cast<double>(step)) * step;\n-}\n+\tconst int half = sizes.size();\n+\tstd::vector<double> positions(half * 2 + 1);\n+\tdouble x = 0.0;\n \n-} /* namespace */\n+\tpositions[half] = 0.5;\n+\tfor (int i = 1; i <= half; i++) {\n+\t\tx += sizes[half - i];\n+\t\tpositions[half - i] = 0.5 - x;\n+\t\tpositions[half + i] = 0.5 + x;\n+\t}\n \n-LensShadingCorrection::LensShadingCorrection()\n-\t: lastAppliedCt_(0), lastAppliedQuantizedCt_(0)\n-{\n+\treturn positions;\n }\n \n /**\n  * \\copydoc libcamera::ipa::Algorithm::init\n  */\n-int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,\n+int LensShadingCorrection::init(IPAContext &context,\n \t\t\t\tconst ValueNode &tuningData)\n {\n \txSize_ = parseSizes(tuningData, \"x-size\");\n@@ -383,56 +113,27 @@ int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,\n \tif (xSize_.empty() || ySize_.empty())\n \t\treturn -EINVAL;\n \n-\t/* Get all defined sets to apply. */\n-\tconst ValueNode &yamlSets = tuningData[\"sets\"];\n-\tif (!yamlSets.isList()) {\n-\t\tLOG(RkISP1Lsc, Error)\n-\t\t\t<< \"'sets' parameter not found in tuning file\";\n-\t\treturn -EINVAL;\n-\t}\n-\n-\tShadingDescriptorMap lscData;\n-\tint ret = 0;\n-\n-\tstd::string type = tuningData[\"type\"].get<std::string>(\"table\");\n-\tif (type == \"table\") {\n-\t\tLOG(RkISP1Lsc, Debug) << \"Loading tabular LSC data.\";\n-\t\tauto loader = LscTableLoader();\n-\t\tret = loader.parseLscData(yamlSets, lscData);\n-\t} else if (type == \"polynomial\") {\n-\t\tLOG(RkISP1Lsc, Debug) << \"Loading polynomial LSC data.\";\n-\t\t/*\n-\t\t * \\todo: Most likely the reference frame should be native_size.\n-\t\t * Let's wait how the internal discussions progress.\n-\t\t */\n-\t\tauto loader = LscPolynomialLoader(context.sensorInfo.activeAreaSize);\n-\t\tret = loader.parseLscData(yamlSets, lscData);\n-\t} else {\n-\t\tLOG(RkISP1Lsc, Error) << \"Unsupported LSC data type '\"\n-\t\t\t\t      << type << \"'\";\n-\t\tret = -EINVAL;\n-\t}\n-\n-\tif (ret)\n-\t\treturn ret;\n+\txPos_ = sizesToPositions(xSize_);\n+\tyPos_ = sizesToPositions(ySize_);\n \n-\tcontext.ctrlMap[&controls::LensShadingCorrectionEnable] =\n-\t\tControlInfo(false, true, true);\n-\n-\tshadingDescriptors_ = std::move(lscData);\n-\n-\treturn 0;\n+\treturn lscAlgo_.init(tuningData, context.ctrlMap, {\n+\t\t\t\t.keys = { \"r\", \"gr\", \"gb\", \"b\" },\n+\t\t\t\t.numHCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,\n+\t\t\t\t.numVCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,\n+\t\t\t\t.sensorSize = context.sensorInfo.activeAreaSize\n+\t\t\t     });\n }\n \n /**\n  * \\copydoc libcamera::ipa::Algorithm::configure\n  */\n int LensShadingCorrection::configure(IPAContext &context,\n-\t\t\t\t     [[maybe_unused]] const IPACameraSensorInfo &configInfo)\n+\t\t\t\t     const IPACameraSensorInfo &configInfo)\n {\n \tconst Size &size = context.configuration.sensor.size;\n \tSize totalSize{};\n \n+\t/* Calculate gradients. */\n \tfor (unsigned int i = 0; i < RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE; ++i) {\n \t\txSizes_[i] = xSize_[i] * size.width;\n \t\tySizes_[i] = ySize_[i] * size.height;\n@@ -455,16 +156,8 @@ int LensShadingCorrection::configure(IPAContext &context,\n \t\tyGrad_[i] = std::round(32768 / ySizes_[i]);\n \t}\n \n-\tLOG(RkISP1Lsc, Debug) << \"Sample LSC data for \" << configInfo.analogCrop;\n-\tstd::map<unsigned int, LensShadingCorrection::Components> shadingData;\n-\tfor (const auto &[t, descriptor] : shadingDescriptors_)\n-\t\tshadingData[t] = descriptor->sampleForCrop(configInfo.analogCrop,\n-\t\t\t\t\t\t\t   xSize_, ySize_);\n-\n-\tsets_.setData(std::move(shadingData));\n-\n-\tcontext.activeState.lsc.enabled = true;\n-\treturn 0;\n+\treturn lscAlgo_.configure(context.activeState.lsc, configInfo.analogCrop,\n+\t\t\t\t  xPos_, yPos_);\n }\n \n void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)\n@@ -476,12 +169,16 @@ void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)\n }\n \n void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,\n-\t\t\t\t      const Components &set)\n+\t\t\t\t      const ipa::lsc::Components<uint16_t> &set)\n {\n-\tstd::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);\n-\tstd::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);\n-\tstd::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);\n-\tstd::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);\n+\tconst auto &r = set.at(\"r\");\n+\tstd::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);\n+\tconst auto &gr = set.at(\"gr\");\n+\tstd::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);\n+\tconst auto &gb = set.at(\"gb\");\n+\tstd::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);\n+\tconst auto &b = set.at(\"b\");\n+\tstd::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);\n }\n \n /**\n@@ -492,19 +189,8 @@ void LensShadingCorrection::queueRequest(IPAContext &context,\n \t\t\t\t\t IPAFrameContext &frameContext,\n \t\t\t\t\t const ControlList &controls)\n {\n-\tauto &lsc = context.activeState.lsc;\n-\n-\tconst auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);\n-\tif (lscEnable && *lscEnable != lsc.enabled) {\n-\t\tlsc.enabled = *lscEnable;\n-\n-\t\tLOG(RkISP1Lsc, Debug)\n-\t\t\t<< (lsc.enabled ? \"Enabling\" : \"Disabling\") << \" Lsc\";\n-\n-\t\tframeContext.lsc.update = true;\n-\t}\n-\n-\tframeContext.lsc.enabled = lsc.enabled;\n+\tlscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc,\n+\t\t\t      controls);\n }\n \n /**\n@@ -542,7 +228,7 @@ void LensShadingCorrection::prepare([[maybe_unused]] IPAContext &context,\n \n \tsetParameters(*config);\n \n-\tconst Components &set = sets_.getInterpolated(quantizedCt);\n+\tconst auto &set = lscAlgo_.interpolateComponents(quantizedCt);\n \tcopyTable(*config, set);\n \n \tlastAppliedCt_ = ct;\n@@ -562,7 +248,7 @@ void LensShadingCorrection::process([[maybe_unused]] IPAContext &context,\n \t\t\t\t    [[maybe_unused]] const rkisp1_stat_buffer *stats,\n \t\t\t\t    ControlList &metadata)\n {\n-\tmetadata.set(controls::LensShadingCorrectionEnable, frameContext.lsc.enabled);\n+\tlscAlgo_.process(frameContext.lsc, metadata);\n }\n \n REGISTER_IPA_ALGORITHM(LensShadingCorrection, \"LensShadingCorrection\")\ndiff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h\nindex 0a256e225327..7c656d95acfb 100644\n--- a/src/ipa/rkisp1/algorithms/lsc.h\n+++ b/src/ipa/rkisp1/algorithms/lsc.h\n@@ -2,17 +2,23 @@\n /*\n  * Copyright (C) 2021-2022, Ideas On Board\n  *\n- * RkISP1 Lens Shading Correction control\n+ * RkISP1 Lens Shading Correction algorithm\n  */\n \n #pragma once\n \n-#include <map>\n-#include <memory>\n+#include <vector>\n \n-#include \"libipa/interpolator.h\"\n+#include <linux/rkisp1-config.h>\n+\n+#include \"libcamera/internal/value_node.h\"\n+\n+#include \"libipa/fixedpoint.h\"\n+#include \"libipa/lsc.h\"\n \n #include \"algorithm.h\"\n+#include \"ipa_context.h\"\n+#include \"params.h\"\n \n namespace libcamera {\n \n@@ -37,39 +43,28 @@ public:\n \t\t     const rkisp1_stat_buffer *stats,\n \t\t     ControlList &metadata) override;\n \n-\tstruct Components {\n-\t\tstd::vector<uint16_t> r;\n-\t\tstd::vector<uint16_t> gr;\n-\t\tstd::vector<uint16_t> gb;\n-\t\tstd::vector<uint16_t> b;\n-\t};\n-\n-\tclass ShadingDescriptor\n-\t{\n-\tpublic:\n-\t\tvirtual ~ShadingDescriptor() = default;\n-\t\tvirtual Components sampleForCrop(const Rectangle &cropRectangle,\n-\t\t\t\t\t\t Span<const double> xSizes,\n-\t\t\t\t\t\t Span<const double> ySizes) = 0;\n-\t};\n-\n-\tusing ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;\n-\n private:\n+\tstd::vector<double> parseSizes(const ValueNode &tuningData,\n+\t\t\t\t       const char *prop);\n+\tstd::vector<double> sizesToPositions(Span<const double> sizes);\n+\n \tvoid setParameters(rkisp1_cif_isp_lsc_config &config);\n-\tvoid copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);\n+\tvoid copyTable(rkisp1_cif_isp_lsc_config &config,\n+\t\t       const ipa::lsc::Components<uint16_t> &set0);\n \n-\tShadingDescriptorMap shadingDescriptors_;\n-\tipa::Interpolator<Components> sets_;\n \tstd::vector<double> xSize_;\n \tstd::vector<double> ySize_;\n \tuint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];\n \tuint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];\n \tuint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];\n \tuint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];\n+\tstd::vector<double> xPos_;\n+\tstd::vector<double> yPos_;\n \n \tunsigned int lastAppliedCt_;\n \tunsigned int lastAppliedQuantizedCt_;\n+\n+\tLscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;\n };\n \n } /* namespace ipa::rkisp1::algorithms */\ndiff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h\nindex cd97e10bcf2b..005f4102b4f6 100644\n--- a/src/ipa/rkisp1/ipa_context.h\n+++ b/src/ipa/rkisp1/ipa_context.h\n@@ -30,6 +30,7 @@\n #include \"libipa/ccm.h\"\n #include \"libipa/fc_queue.h\"\n #include \"libipa/fixedpoint.h\"\n+#include \"libipa/lsc.h\"\n \n namespace libcamera {\n \n@@ -138,9 +139,7 @@ struct IPAActiveState {\n \t\tdouble strength;\n \t} wdr;\n \n-\tstruct {\n-\t\tbool enabled;\n-\t} lsc;\n+\tipa::lsc::ActiveState lsc;\n };\n \n struct IPAFrameContext : public FrameContext {\n@@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {\n \t\tdouble gain;\n \t} wdr;\n \n-\tstruct {\n-\t\tbool enabled;\n-\t\tbool update;\n-\t} lsc;\n+\tipa::lsc::FrameContext lsc;\n };\n \n struct IPAContext {\n","prefixes":["09/11"]}